Taula de continguts

Módulo 3: Programación Orientada a Objetos - propiedades

Variables de instancia

En general, una clase puede equiparse con dos tipos diferentes de datos para formar las propiedades de una clase. Ya viste uno de ellos cuando estábamos estudiando pilas.

Este tipo de propiedad existe solo cuando se crea explícitamente y se agrega a un objeto. Como ya sabes, esto se puede hacer durante la inicialización del objeto, realizada por el constructor.

Además, se puede hacer en cualquier momento de la vida del objeto. Es importante mencionar también que cualquier propiedad existente se puede eliminar en cualquier momento.

Tal enfoque tiene algunas consecuencias importantes:

Tales variables (propiedades) se llaman variables de instancia.

La palabra instancia sugiere que están estrechamente conectadas a los objetos (que son instancias de clase), no a las clases mismas. Echemos un vistazo más de cerca.

Aquí hay un ejemplo:

class ExampleClass:
    def __init__(self, val = 1):
        self.first = val
 
    def set_second(self, val):
        self.second = val
 
 
example_object_1 = ExampleClass()
example_object_2 = ExampleClass(2)
 
example_object_2.set_second(3)
 
example_object_3 = ExampleClass(4)
example_object_3.third = 5
 
print(example_object_1.__dict__)
print(example_object_2.__dict__)
print(example_object_3.__dict__)

Se necesita una explicación adicional antes de entrar en más detalles. Echa un vistazo a las últimas tres líneas del código.

Los objetos de Python, cuando se crean, están dotados de un pequeño conjunto de propiedades y métodos predefinidos. Cada objeto los tiene, los quieras o no. Uno de ellos es una variable llamada __dict__ (es un diccionario).

La variable contiene los nombres y valores de todas las propiedades (variables) que el objeto contiene actualmente. Vamos a usarla para presentar de forma segura el contenido de un objeto.

Vamos a sumergirnos en el código ahora:

La salida del programa muestra claramente que nuestras suposiciones son correctas: aquí están:

{'first': 1}
{'second': 3, 'first': 2}
{'third': 5, 'first': 4}

Hay una conclusión adicional que debería mencionarse aquí: el modificar una variable de instancia de cualquier objeto no tiene impacto en todos los objetos restantes. Las variables de instancia están perfectamente aisladas unas de otras.

class ExampleClass:
    def __init__(self, val = 1):
        self.__first = val
 
    def set_second(self, val = 2):
        self.__second = val
 
 
example_object_1 = ExampleClass()
example_object_2 = ExampleClass(2)
 
example_object_2.set_second(3)
 
example_object_3 = ExampleClass(4)
example_object_3.__third = 5
 
 
print(example_object_1.__dict__)
print(example_object_2.__dict__)
print(example_object_3.__dict__)

Es casi lo mismo que el anterior. La única diferencia está en los nombres de las propiedades. Hemos antepuesto dos guiones bajos (__).

Como sabes, tal adición hace que la variable de instancia sea privada, se vuelve inaccesible desde el mundo exterior.

El comportamiento real de estos nombres es un poco más complicado, así que ejecutemos el programa. Esta es la salida:

{'_ExampleClass__first': 1}
{'_ExampleClass__first': 2, '_ExampleClass__second': 3}
{'_ExampleClass__first': 4, '__third': 5}

¿Puedes ver estos nombres extraños llenos de guiones bajos? ¿De dónde provienen?

Cuando Python ve que deseas agregar una variable de instancia a un objeto y lo vas a hacer dentro de cualquiera de los métodos del objeto, maneja la operación de la siguiente manera:

Es por ello que __first se convierte en _ExampleClass__first.

El nombre ahora es completamente accesible desde fuera de la clase. Puedes ejecutar un código como este:

print(example_object_1._ExampleClass__first)

Obtendrás un resultado válido sin errores ni excepciones.

Como puedes ver, hacer que una propiedad sea privada es limitado.

No funcionará si agregas una variable de instancia fuera del código de la clase. En este caso, se comportará como cualquier otra propiedad ordinaria.

Variables de clase

Una variable de clase es una propiedad que existe en una sola copia y se almacena fuera de cualquier objeto.

Nota: no existe una variable de instancia si no hay ningún objeto de la clase; solo existe una variable de clase en una copia, incluso si no hay objetos en la clase.

Las variables de clase se crean de manera diferente. El ejemplo te dirá más:

class ExampleClass:
    counter = 0
    def __init__(self, val = 1):
        self.__first = val
        ExampleClass.counter += 1
 
 
example_object_1 = ExampleClass()
example_object_2 = ExampleClass(2)
example_object_3 = ExampleClass(4)
 
print(example_object_1.__dict__, example_object_1.counter)
print(example_object_2.__dict__, example_object_2.counter)
print(example_object_3.__dict__, example_object_3.counter)

Observa:

Ejecutar el código provocará el siguiente resultado:

{'_ExampleClass__first': 1} 3
{'_ExampleClass__first': 2} 3
{'_ExampleClass__first': 4} 3

Dos conclusiones importantes se pueden sacar del ejemplo:

El cambiar el nombre de una variable de clase tiene los mismos efectos que aquellos con los que ya está familiarizado.

Mira el ejemplo en el editor. ¿Puedes adivinar su salida?

class ExampleClass:
    __counter = 0
    def __init__(self, val = 1):
        self.__first = val
        ExampleClass.__counter += 1
 
 
example_object_1 = ExampleClass()
example_object_2 = ExampleClass(2)
example_object_3 = ExampleClass(4)
 
print(example_object_1.__dict__, example_object_1._ExampleClass__counter)
print(example_object_2.__dict__, example_object_2._ExampleClass__counter)
print(example_object_3.__dict__, example_object_3._ExampleClass__counter)

Hemos dicho antes que las variables de clase existen incluso cuando no se creó ninguna instancia de clase (objeto).

Ahora aprovecharemos la oportunidad para mostrarte la diferencia entre estas dos variables __dict__, la de la clase y la del objeto.

class ExampleClass:
    varia = 1
    def __init__(self, val):
        ExampleClass.varia = val
 
 
print(ExampleClass.__dict__)
example_object = ExampleClass(2)
 
print(ExampleClass.__dict__)
print(example_object.__dict__)

Echemos un vistazo más de cerca:

  1. Definimos una clase llamada ExampleClass.
  2. La clase define una variable de clase llamada varia.
  3. El constructor de la clase establece la variable con el valor del parámetro.
  4. Nombrar la variable es el aspecto más importante del ejemplo porque:
    • El cambiar la asignación a self.varia = val crearía una variable de instancia con el mismo nombre que la de la clase.
    • El cambiar la asignación a varia = val operaría en la variable local de un método; (te recomendamos probar los dos casos anteriores; esto te facilitará recordar la diferencia).
  5. La primera línea del código fuera de la clase imprime el valor del atributo ExampleClass.varia . Nota: utilizamos el valor antes de instanciar el primer objeto de la clase.

Ejecuta el código en el editor y verifica su salida.

{'__module__': '__main__', 'varia': 1, '__init__': <function ExampleClass.__init__ at 0x7fc83922b0e0>, '__dict__': <attribute '__dict__' of 'ExampleClass' objects>, '__weakref__': <attribute '__weakref__' of 'ExampleClass' objects>, '__doc__': None}
{'__module__': '__main__', 'varia': 2, '__init__': <function ExampleClass.__init__ at 0x7fc83922b0e0>, '__dict__': <attribute '__dict__' of 'ExampleClass' objects>, '__weakref__': <attribute '__weakref__' of 'ExampleClass' objects>, '__doc__': None}

Como puedes ver __dict__ contiene muchos más datos que la contraparte de su objeto. La mayoría de ellos son inútiles ahora, el que queremos que verifiques cuidadosamente muestra el valor actual de varia.

Nota que el __dict__ del objeto está vacío, el objeto no tiene variables de instancia.

Comprobando la existencia de un atributo

La actitud de Python hacia la instanciación de objetos plantea una cuestión importante: en contraste con otros lenguajes de programación, es posible que no esperes que todos los objetos de la misma clase tengan los mismos conjuntos de propiedades.

class ExampleClass:
    def __init__(self, val):
        if val % 2 != 0:
            self.a = 1
        else:
            self.b = 1
 
 
example_object = ExampleClass(1)
 
print(example_object.a)
print(example_object.b)

El objeto creado por el constructor solo puede tener uno de los dos atributos posibles: a o b.

La ejecución del código producirá el siguiente resultado:

1
Traceback (most recent call last):
  File ".main.py", line 11, in 
    print(example_object.b)
AttributeError: 'ExampleClass' object has no attribute 'b'

Como puedes ver, acceder a un atributo de objeto (clase) no existente genera una excepción AttributeError.

La instrucción try-except te brinda la oportunidad de evitar problemas con propiedades inexistentes.

class ExampleClass:
    def __init__(self, val):
        if val % 2 != 0:
            self.a = 1
        else:
            self.b = 1
 
 
example_object = ExampleClass(1)
print(example_object.a)
 
try:
    print(example_object.b)
except AttributeError:
    pass

Como puedes ver, esta acción no es muy sofisticada. Esencialmente, acabamos de barrer el tema debajo de la alfombra.

Afortunadamente, hay una forma más de hacer frente al problema.

Python proporciona una función que puede verificar con seguridad si algún objeto / clase contiene una propiedad específica. La función se llama hasattr, y espera que le pasen dos argumentos:

La función retorna True o False.

Así es como puedes utilizarla:

class ExampleClass:
    def __init__(self, val):
        if val % 2 != 0:
            self.a = 1
        else:
            self.b = 1
 
 
example_object = ExampleClass(1)
print(example_object.a)
 
if hasattr(example_object, 'b'):
    print(example_object.b)

No olvides que la función hasattr() también puede operar en clases. Puedes usarla para averiguar si una variable de clase está disponible, como en el ejemplo en el editor.

La función devuelve True si la clase especificada contiene un atributo dado, y False de lo contrario.

class ExampleClass:
    attr = 1
 
 
print(hasattr(ExampleClass, 'attr'))
print(hasattr(ExampleClass, 'prop'))

Un ejemplo más: analiza el código a continuación e intenta predecir su salida:

class ExampleClass:
    a = 1
    def __init__(self):
        self.b = 2
 
 
example_object = ExampleClass()
 
print(hasattr(example_object, 'b'))
print(hasattr(example_object, 'a'))
print(hasattr(ExampleClass, 'b'))
print(hasattr(ExampleClass, 'a'))

Bien, hemos llegado al final de esta sección. En la siguiente sección vamos a hablar sobre los métodos, ya que los métodos dirigen los objetos y los activan.

Puntos Clave

1. Una variable de instancia es una propiedad cuya existencia depende de la creación de un objeto. Cada objeto puede tener un conjunto diferente de variables de instancia.

Además, se pueden agregar y quitar libremente de los objetos durante su vida útil. Todas las variables de instancia de objeto se almacenan dentro de un diccionario dedicado llamado __dict__, contenido en cada objeto por separado.

2. Una variable de instancia puede ser privada cuando su nombre comienza con __, pero no olvides que dicha propiedad aún es accesible desde fuera de la clase usando un nombre modificado construido como < codel>_ClassName__PrivatePropertyName.

3. Una variable de clase es una propiedad que existe exactamente en una copia y no necesita ningún objeto creado para ser accesible. Estas variables no se muestran como contenido de __dict__.

Todas las variables de clase de una clase se almacenan dentro de un diccionario dedicado llamado __dict__, contenido en cada clase por separado.

4. Una función llamada hasattr() se puede utilizar para determinar si algún objeto o clase contiene cierta propiedad especificada.

Por ejemplo:

class Sample:
    gamma = 0 # Class variable.
    def __init__(self):
        self.alpha = 1 # Variable de instancia.
        self.__delta = 3 # Variable de instancia privada.
 
 
obj = Sample()
obj.beta = 2  # Otra variable de instancia (que existe solo dentro de la instancia "obj").
print(obj.__dict__)

El código da como salida:

{'alpha': 1, '_Sample__delta': 3, 'beta': 2}

https://edube.org/learn/python-essentials-2-esp/poo-m-eacute-todos-10