20

¿Cuál es la diferencia entre un atributo/propiedad de instancia y un atributo/propiedad de clase? ¿Cuándo uso uno y cuándo otro en Python?.

Es decir, si tenemos:

class Foo:
    a = 5

    def print_a(self):
        print(self.a)

y:

class Foo:
    def __init__(self):
        self.a = 5

    def print_a(self):
        print(self.a)

¿Qué diferencias hay con respecto al atributo a?

FJSevilla
  • 55,603
  • 7
  • 35
  • 58
Diego Velasco
  • 325
  • 1
  • 2
  • 8

3 Answers3

44

La diferencia fundamental es que los atributos de clase son compartidos por todas las instancias de esa clase, mientras que los atributos de instancia son particulares para cada objeto creado con esa clase. Por tanto, las variables de instancia son para datos únicos y propios para cada objeto y las variables de clase son para atributos que deban ser compartidos por todas las instancias de esa clase.

  • Los atributos de instancia son creados usando la sintaxis instance.attr o self.attr (self es realmente una convención, simplemente es un identificador que hace referencia a la propia instancia). Como se ha dicho son locales para esa instancia y por tanto solo accesibles desde una de ellas.

    Por norma general son creados dentro de un método. Se pueden crear atributos de instancia en cualquier método de instancia, pero generalmente es buena práctica hacerlo en el inicializador de la clase (__init__) o el constructor (__new__) o como mucho en un método que sea directamente llamado desde uno de ellos. Si no se hace así (aunque solo sea inicializarlos a un valor nulo) el código pierde legibilidad y lo que es más importante, el atributo no existe hasta que el método que lo crea sea llamado, lo que crea incertidumbre en cuanto a si una instancia tiene o no cierto atributo en un momento dado.

    En cuanto a su utilidad, genéricamente podemos decir que permiten definir un determinado "estado" a ese objeto particular y que lo diferencia de otros objetos de su clase. Como puede ser el atributo "matrícula" en una clase "Automóvil".

    Son almacenados en un diccionario (diccionario de instancia u objeto) al cual puedes acceder mediante NombreInstancia.__dict__.

  • Los atributos de clase se definen fuera de cualquier método de la clase, normalmente justo debajo de la cadena de documentación. Se pueden referenciar directamente desde la clase y también se puede hacer referencia a ellas desde cualquier instancia de la clase. Usos comunes son crear una constante enumerada, como variable de control en un Singleton, para establecer valores predeterminados para variables de instancia, definir constantes, etc.

    Son almacenados en un diccionario distinto (diccionario de clase) e independiente al usado para los atributos de instancia, el cual puedes consultar mediante NombreClase.__dict__ o desde una instancia con NombreInstancia.__class__.__dict__. Las properties y los descriptores en general también están a nivel de clase, no de instancia.

    Nota: En Python 3 el diccionario de clase es de tipo mappingproxy, sin extenderse demasiado una consecuencia directa es hacer que el diccionario sea de solo lectura y en principio solo __setattr__ podrá modificarlo en su caso. Esto se hace por eficiencia y estabilidad, entre otras cosas se asegura que las claves solo puedan ser cadenas.

Es importante conocer el orden en el que se resuelve el acceso a un atributo, Python sigue el siguiente orden al buscar un atributo desde un objeto:

  1. Atributos especiales (__dict__, __slots__, __weakref__, etc.).
  2. Descriptores de datos (implementan __set__) en diccionario de clase y posibles clases padre.
  3. Atributos de instancia.
  4. Descriptores de no-datos (implementan __get__ pero no __set__) en diccionario de clase y padres.
  5. Finalmente intenta delegar en el método __getattr__.

si obviamos los descriptores de datos (y por tanto properties con setter) Python busca primero el atributo en los atributos de instancia, y de no encontrarlo busca en los de clase. Esto implica que si creas un atributo de instancia con el mismo nombre que un atributo de clase ya presente, el atributo de clase queda "oculto" si se intenta acceder a el vía self.atributo o instancia.atributo.

En el caso de asignación el orden es:

  1. Descriptores de datos en la clase y padres.
  2. Método __setattr__
  3. Atributos de instancia, si no existe el atributo y lo anterior falla se agrega el nuevo atributo como atributo de instancia automáticamente.

Lo anterior contesta básicamente a la pregunta, lo siguiente que es algo más extenso (principalmente por los ejemplos) e intenta explicar un error común causado por la diferencia que hay entre asignar a un atributo de clase desde su clase o desde una instancia y directamente relacionado con el orden de resolución explicado antes.

Si alguien no quiere leer más ツ, la moraleja es:

Si se va a asignar un nuevo valor a un atributo de clase (que no sea un descriptor de datos) hacerlo siempre referenciando desde la clase, no desde la instancia. Es decir, vía NombreClase.atributo o usando un método de clase (@classmethod) no mediante self.atributo o nombre_instancia.atributo, ya que esto no modifica el atributo de clase sino que crea un nuevo atributo de instancia con el mismo nombre.

Cómo se comenta arriba podemos acceder a los atributos de clase de ambas formas:

>>> class Foo:
        n = 7

>>> inst = Foo()
>>> inst.n             # Acceso mediante nombre de una instancia
7
>>> Foo.n              # Acceso mediante nombre de la clase
7
>>> inst.__class__.n   # Acceso mediante el atributo __class__ de la instancia
7

Cuando en vez de solo acceder al valor queremos realizar una asignación, las cosas cambian:

  • Si asignamos a través de la clase el comportamiento es el esperado, el valor cambia para todas las instancias de la clase:

    >>> class Foo:
            cls_n = 13
    
            @classmethod
            def set_cls_n(cls, v):
                cls.cls_n = v
    
    >>> inst1 = Foo()
    >>> inst2 = Foo()
    >>> inst1.cls_n
    13
    >>> inst2.cls_n
    13
    >>> Foo.cls_n = 7
    >>> inst1.cls_n
    7
    >>> inst2.cls_n
    7
    >>> inst1.set_cls_n(23)
    >>> inst1.cls_n
    23
    >>> inst2.cls_n
    23
    

    Se puede observar como modificando el valor de n tanto usando Foo.n como mediante un método de clase (dónde cls hace referencia a Foo igual que __class__) el cambio se refleja en todas las instancias de la clase.

  • Si se asigna un nuevo valor a la variable de clase usando una instancia para hacer referencia a ella nos llevamos una sorpresa:

    >>> class Foo:
            cls_n = 13
    
    >>> inst1 = Foo()
    >>> inst2 = Foo()
    >>> inst1.cls_n
    13
    >>> inst2.cls_n
    13
    >>> inst1.cls_n = 7
    >>> inst1.cls_n
    7
    >>> inst2.cls_n
    13               # ????????????????????
    

    Al intentar asignar un nuevo objeto al atributo de clase usando instancia.atributo o desde un método de instancia vía self.atributo (siempre que no estemos tratando con un descriptor de datos que como vimos tiene preferencia y se comportaría como esperamos) no se modifica el atributo de clase, se crea un atributo de instancia con el mismo nombre y se le asigna el nuevo valor.

    Recordemos que los atributos (descriptores de datos aparte de nuevo) primero se buscan en __dict__ y después en __class__.__dict__ si no se encuentran, esto hace que el atributo de clase quede oculto detrás del de instancia, podemos verlo mediante el contenido de __dict__ y __class__.__dict__:

    >>> inst1 = Foo()
    >>> inst1.__dict__
    {}
    >>> inst1.__class__.__dict__
    mappingproxy({'__module__': '__main__',
                 'cls_n': 13,
                 '__dict__': <attribute '__dict__' of 'Foo' objects>,
                 '__weakref__': <attribute '__weakref__' of 'Foo' objects>,
                  '__doc__': None
                })
    
    
    >>> inst1.cls_n = 7
    >>> inst1.__dict__
    {'cls_n': 7}         # <<<< Nuestro nuevo atributo de instancia
    >>> inst1.__class__.__dict__
    mappingproxy({'__module__': '__main__',
                 'cls_n': 13,
                 '__dict__': <attribute '__dict__' of 'Foo' objects>,
                 '__weakref__': <attribute
                 '__weakref__' of 'Foo' objects>,
                 '__doc__': None
                })
    

Por último, recordar que en Python hay objetos mutables como (list, dict, set) e inmutables (int, float, bool, tuple, frozenset, str). Los objetos inmutables no pueden ser modificados una vez creados, cuando concatenamos una cadena se crea un nuevo objeto, cosa que no pasa con una lista:

    >>> a = "Hola"
    >>> b = "StackOverflow"
    >>> id(a)
    2105306070744
    >>> id(b)
    2105306118320
    >>> a += b
    >>> a
    'HolaStackOverflow'
    >>> id(a)       
    2105306110544    # a ahora hace referencia a otro objeto

    >>> a = ["Hola"]
    >>> b = ["StackOverflow"]
    >>> id(a)
    2105306091848
    >>> id(b)
    2105305993800
    >>> a += b
    >>> a 
    ['Hola', 'StackOverflow']
    >>> id(a)
    2105306091848    # a sigue haciendo referencia al mismo objeto

Python maneja la asignación por referencia, una variable o atributo no es más que un identificador, un nombre, que hace referencia a un objeto en memoria. Los atributos de clase se comparten entre todas las instancias porque hacen referencia al mismo objeto en memoria, por lo tanto cuando modificamos atributos de clase mutables a través de instancia.atributo o self.atributo el cambio si que se refleja en todas las instancias:

class Foo:
    a = [100]                # Atributo de clase

    def __init__(self, n):
        self.b = [n]         # Atributo de instancia


>>> inst1 = Foo(1)
>>> inst2 = Foo(2)

>>> inst1.a
[100]
>>> inst2.a
[100]
>>> inst1.a += [5]
>>> inst1.a
[100, 5]
>>> inst2.a
[100, 5]

Esto no ocurre con el atributo de instancia ya que es propio de cada objeto:

>>> inst1.b
[1]    
>>> inst2.b
[2] 
>>> inst1.b += [5]
>>> inst1.b
[1, 5]
>>> inst2.b
[2]

Si el atributo de clase hace referencia a un objeto inmutable aunque intentemos "modificarlo" en una instancia esto no implica hacerlo en el resto, ya que "modificar" un objeto inmutable implica siempre una asignación de por medio y como se ha visto antes, una asignación desde la instancia ocasiona la creación de un nuevo atributo de instancia, no en la modificación del atributo de clase (a no ser que sea un descriptor de datos):

>>> class Foo:
        s = "Hola"

>>> a = Foo()
>>> b = Foo()
>>> a.s += " StackOverflow"
>>> a.s
'Hola StackOverflow'
>>> b.s
'Hola'

A diferencia de lo que pasaba con la lista la concatenación de cadenas (o las operaciones con enteros, floats, etc) implica la creación de un nuevo objeto y un intento de asignación al atributo, lo cual implica la creación de un atributo de instancia como ya vimos antes y no la alteración del atributo de clase.


Aunque los descriptores están íntimamente ligados a todo esto y se mencionan repetidamente no se entra en ellos porque explicar el protocolo descriptor implicaría extender demasiado la respuesta cuando ya es posiblemente demasiado larga, podemos ver en la propia documentación (en inglés) una guía general acerca de esto: Descriptor HowTo Guide

FJSevilla
  • 55,603
  • 7
  • 35
  • 58
  • 4
    Ofrezco una recompensa para esta respuesta, pues -como siempre- resulta útil, didáctica y nos ayuda a todos. ¡Gracias por tan buen trabajo! – fedorqui Jun 19 '18 at 10:16
6

Una variable de clase es heredada por todas las instancias que derivan de ella. Si te fijas en este ejemplo:

pi@rp1:~ $ python3
Python 3.4.2 (default, Oct 19 2014, 13:31:11)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Perro:
...     numero_patas = 4
...
>>> pancho = Perro()
>>> fido = Perro()
>>> pancho.numero_patas
4
>>> fido.numero_patas
4
>>> Perro.numero_patas
4

pancho y fido son dos instancias de Perro que han heredado el valor de la variable numero_patas. Ahora cambiamos el valor de la variable en la instancia fido

>>> fido.numero_patas = 3
>>> fido.numero_patas
3
>>>

Ahora añadimos una nueva variable de instancia a fido:

>>> fido.cola = 'cortada'
>>> fido.cola
'cortada'

En Python, las variables de clase y de instancia se guardan en diccionarios diferentes. Si miramos en el diccionario de cada instancia creada:

>>> pancho.__dict__
{}
>>> fido.__dict__
{'cola': 'cortada', 'numero_patas': 3}
>>>

Vemos que pancho no tiene variables de instancia. Sin embargo, cuando hemos preguntado por numero_patas nos ha devuelto 4. Esto es debido a que existe un segundo diccionario, el diccionario de clase, al que se accede cuando no se encuentra la variable de instancia en su diccionario. Si accedemos a el:

>>> pancho.__class__.__dict__
mappingproxy({'__weakref__': <attribute '__weakref__' of 'Perro' objects>, '__dict__': <attribute '__dict__' of 'Perro' objects>, 'numero_patas': 4, '__module__': '__main__', '__doc__': None})
>>> fido.__class__.__dict__
mappingproxy({'__weakref__': <attribute '__weakref__' of 'Perro' objects>, '__dict__': <attribute '__dict__' of 'Perro' objects>, 'numero_patas': 4, '__module__': '__main__', '__doc__': None})
>>>

verás que son idénticos. Si vas a derivar una única instancia de una clase, lo suyo es que utilices variables de instancia, ya que al acceder al diccionario de variables de instancias en primer lugar, será un poco más rápido.

miimote
  • 171
  • 1
  • 6
3

La diferencia entre un atributo de clase y un atributo de instancia se encuentra en la forma de acceder a ellos, por lo tanto para los atributos de instancia solo es posible acceder a través de un objeto, para los atributos de clase se accede directamente desde la clase y/o también por medio de un objeto.