¿Cuál es la diferencia entre los atributos de clase e instancia?

132

¿Hay alguna distinción significativa entre:

class A(object):
    foo = 5   # some default value

vs.

class B(object):
    def __init__(self, foo=5):
        self.foo = foo

Si está creando muchas instancias, ¿hay alguna diferencia en el rendimiento o los requisitos de espacio para los dos estilos? Cuando lee el código, ¿considera que el significado de los dos estilos es significativamente diferente?

Dan Homerick
fuente
1
Me acabo de dar cuenta de que se hizo una pregunta similar y se respondió aquí: stackoverflow.com/questions/206734/… ¿Debo eliminar esta pregunta?
Dan Homerick
2
Es tu pregunta, no dudes en eliminarla. Ya que es tuyo, ¿por qué pedir la opinión de alguien más?
S.Lott

Respuestas:

146

Más allá de las consideraciones de rendimiento, hay una diferencia semántica significativa . En el caso del atributo de clase, solo se hace referencia a un objeto. En el conjunto de atributos de instancia en la instanciación, puede haber varios objetos a los que se hace referencia. Por ejemplo

>>> class A: foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo
[5]
>>> class A:
...  def __init__(self): self.foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo    
[]
Alex Coventry
fuente
44
Solo se comparten los tipos mutables. Me gusta para inty strtodavía se adjuntan con cada instancia en lugar de clase.
Babu
11
@Babu: No, inty strson también compartidos, exactamente de la misma manera. Puede verificar eso fácilmente con iso id. O simplemente mire en cada instancia __dict__y en la clase __dict__. Por lo general, no importa mucho si los tipos inmutables se comparten o no.
abarnert
17
Sin embargo, tenga en cuenta que si lo hace a.foo = 5, en ambos casos verá b.fooregresar []. Esto se debe a que, en el primer caso, está sobrescribiendo el atributo de clase a.foocon un nuevo atributo de instancia del mismo nombre.
Konstantin Schubert
39

La diferencia es que el atributo en la clase es compartido por todas las instancias. El atributo en una instancia es exclusivo de esa instancia.

Si viene de C ++, los atributos en la clase son más como variables miembro estáticas.

Peter Shinners
fuente
1
¿No son solo los tipos mutables los que se comparten? La respuesta aceptada muestra una lista, que funciona, pero si es un int, parece ser lo mismo que una instancia attr: >>> class A(object): foo = 5 >>> a, b = A(), A() >>> a.foo = 10 >>> b.foo 5
Rafe
66
@Rafe: No, todos los tipos son compartidos. La razón por la que está confundido es porque lo que a.foo.append(5)está mutando el valor al que se a.foorefiere, mientras se a.foo = 5está convirtiendo a.fooen un nuevo nombre para el valor 5. Entonces, terminas con un atributo de instancia que oculta el atributo de clase. Pruebe lo mismo a.foo = 5en la versión de Alex y verá que no b.fooha cambiado.
abarnert
30

Aquí hay una muy buena publicación , y resumirla como a continuación

class Bar(object):
    ## No need for dot syntax
    class_var = 1

    def __init__(self, i_var):
        self.i_var = i_var

## Need dot syntax as we've left scope of class namespace
Bar.class_var
## 1
foo = MyClass(2)

## Finds i_var in foo's instance namespace
foo.i_var
## 2

## Doesn't find class_var in instance namespace…
## So look's in class namespace (Bar.__dict__)
foo.class_var
## 1

Y en forma visual

ingrese la descripción de la imagen aquí

Asignación de atributo de clase

  • Si un atributo de clase se establece accediendo a la clase, anulará el valor para todas las instancias

    foo = Bar(2)
    foo.class_var
    ## 1
    Bar.class_var = 2
    foo.class_var
    ## 2
  • Si se establece una variable de clase accediendo a una instancia, anulará el valor solo para esa instancia . Esto esencialmente anula la variable de clase y la convierte en una variable de instancia disponible, intuitivamente, solo para esa instancia .

    foo = Bar(2)
    foo.class_var
    ## 1
    foo.class_var = 2
    foo.class_var
    ## 2
    Bar.class_var
    ## 1

¿Cuándo usarías el atributo de clase?

  • Almacenamiento de constantes . Como se puede acceder a los atributos de clase como atributos de la clase en sí, a menudo es bueno usarlos para almacenar constantes específicas de clase y de toda la clase

    class Circle(object):
         pi = 3.14159
    
         def __init__(self, radius):
              self.radius = radius   
        def area(self):
             return Circle.pi * self.radius * self.radius
    
    Circle.pi
    ## 3.14159
    c = Circle(10)
    c.pi
    ## 3.14159
    c.area()
    ## 314.159
  • Definición de valores por defecto . Como ejemplo trivial, podríamos crear una lista acotada (es decir, una lista que solo puede contener un cierto número de elementos o menos) y elegir tener un límite predeterminado de 10 elementos.

    class MyClass(object):
        limit = 10
    
        def __init__(self):
            self.data = []
        def item(self, i):
            return self.data[i]
    
        def add(self, e):
            if len(self.data) >= self.limit:
                raise Exception("Too many elements")
            self.data.append(e)
    
     MyClass.limit
     ## 10
zangw
fuente
19

Dado que las personas en los comentarios aquí y en otras dos preguntas marcadas como dups parecen estar confundidas acerca de esto de la misma manera, creo que vale la pena agregar una respuesta adicional además de la de Alex Coventry .

El hecho de que Alex asigne un valor de tipo mutable, como una lista, no tiene nada que ver con si las cosas se comparten o no. Podemos ver esto con la idfunción o el isoperador:

>>> class A: foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
True
>>> class A:
...     def __init__(self): self.foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
False

(Si se pregunta por qué lo usé en object()lugar de, por ejemplo, 5eso es para evitar toparme con otros dos problemas que no quiero abordar aquí; por dos razones diferentes, los correos electrónicos creados por separado 5pueden terminar siendo misma instancia del número, 5pero object()no se pueden crear s completamente por separado ).


Entonces, ¿por qué a.foo.append(5)en el ejemplo de Alex afecta b.foo, pero a.foo = 5en mi ejemplo no afecta? Bueno, trate a.foo = 5en el ejemplo de Alex, y el aviso de que no afecta b.fooexiste tampoco .

a.foo = 5solo se está convirtiendo a.fooen un nombre para 5. Eso no afecta b.foo, ni ningún otro nombre para el valor anterior al que a.foosolía referirse. * Es un poco complicado que estemos creando un atributo de instancia que oculte un atributo de clase, ** pero una vez que lo obtienes, nada complicado es sucediendo aquí


Con suerte, ahora es obvio por qué Alex usó una lista: el hecho de que pueda mutar una lista significa que es más fácil mostrar que dos variables nombran la misma lista, y también significa que es más importante en el código de la vida real saber si tiene dos listas o no. Dos nombres para la misma lista.


* La confusión para las personas que provienen de un lenguaje como C ++ es que en Python, los valores no se almacenan en variables. Los valores viven en tierra de valor, por sí solos, las variables son solo nombres para valores, y la asignación solo crea un nuevo nombre para un valor. Si ayuda, piense en cada variable de Python como en shared_ptr<T>lugar de a T.

** Algunas personas aprovechan esto usando un atributo de clase como un "valor predeterminado" para un atributo de instancia que las instancias pueden o no establecer. Esto puede ser útil en algunos casos, pero también puede ser confuso, así que tenga cuidado con eso.

abarnert
fuente
0

Hay una situación más.

Los atributos de clase e instancia son Descriptor .

# -*- encoding: utf-8 -*-


class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        return self.val


class Base(object):
    attr_1 = RevealAccess(10, 'var "x"')

    def __init__(self):
        self.attr_2 = RevealAccess(10, 'var "x"')


def main():
    b = Base()
    print("Access to class attribute, return: ", Base.attr_1)
    print("Access to instance attribute, return: ", b.attr_2)

if __name__ == '__main__':
    main()

Arriba saldrá:

('Access to class attribute, return: ', 10)
('Access to instance attribute, return: ', <__main__.RevealAccess object at 0x10184eb50>)

¡El mismo tipo de acceso a la instancia a través de la clase o instancia devuelve un resultado diferente!

Y encontré en c.PyObject_GenericGetAttr definición , y una gran publicación .

Explique

Si el atributo se encuentra en el diccionario de las clases que lo componen. los objetos MRO, luego verifique si el atributo que se está buscando apunta a un Descriptor de datos (que no es más que una clase que implementa tanto __get__los __set__métodos como los métodos). Si lo hace, resuelva la búsqueda de atributos llamando al __get__método del Descriptor de datos (líneas 28–33).

SimonShyu
fuente