Qual é a diferença entre os atributos de classe e instância?

132

Existe alguma distinção significativa entre:

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

vs.

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

Se você estiver criando muitas instâncias, há alguma diferença nos requisitos de desempenho ou espaço para os dois estilos? Ao ler o código, você considera o significado dos dois estilos significativamente diferentes?

Dan Homerick
fonte
1
Acabei de perceber que uma pergunta semelhante foi feita e respondida aqui: stackoverflow.com/questions/206734/… Devo excluir esta pergunta?
Dan Homerick
2
É sua pergunta, fique à vontade para excluí-lo. Como é sua, por que perguntar a opinião de outra pessoa?
285/08 S.Lott

Respostas:

146

Além das considerações de desempenho, há uma diferença semântica significativa . No caso do atributo de classe, há apenas um objeto referido. Na instância-atributo-definido-na-instanciação, pode haver vários objetos referidos. Por exemplo

>>> 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
fonte
4
Somente os tipos mutáveis ​​são compartilhados. Como para inte streles ainda anexados a cada instância, em vez de classe.
Babu
11
@Babu: Não, inte tambémstr são compartilhados, exatamente da mesma maneira. Você pode verificar isso facilmente com ou . Ou apenas procure nas instâncias e nas classes . Geralmente, não importa muito se tipos imutáveis ​​são compartilhados ou não. isid__dict____dict__
abarnert
17
No entanto, observe que, se o fizer a.foo = 5, em ambos os casos você verá o b.fooretorno []. Isso ocorre porque, no primeiro caso, você está substituindo o atributo de classe a.foopor um novo atributo de instância com o mesmo nome.
27515 Konstantin Schubert
39

A diferença é que o atributo na classe é compartilhado por todas as instâncias. O atributo em uma instância é exclusivo para essa instância.

Se provenientes do C ++, os atributos na classe são mais parecidos com variáveis ​​de membro estáticas.

Peter Shinners
fonte
1
Não são apenas tipos mutáveis ​​que são compartilhados? A resposta aceita mostra uma lista, que funciona, mas se é um int, parece ser o mesmo que uma instância attr: >>> class A(object): foo = 5 >>> a, b = A(), A() >>> a.foo = 10 >>> b.foo 5
Rafe
6
@ Rafe: Não, todos os tipos são compartilhados. A razão pela qual você está confuso é que o que a.foo.append(5)está mudando o valor a que a.foose refere, enquanto a.foo = 5está transformando a.fooem um novo nome para o valor 5. Então, você acaba com um atributo de instância que oculta o atributo de classe. Tente o mesmo a.foo = 5na versão de Alex e você verá que b.foonão é alterado.
abarnert
30

Aqui está um post muito bom , e resuma-o como abaixo.

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

E na forma visual

insira a descrição da imagem aqui

Atribuição de atributo de classe

  • Se um atributo de classe for definido acessando a classe, ele substituirá o valor para todas as instâncias

    foo = Bar(2)
    foo.class_var
    ## 1
    Bar.class_var = 2
    foo.class_var
    ## 2
  • Se uma variável de classe for configurada acessando uma instância, ela substituirá o valor somente para essa instância . Substitui essencialmente a variável de classe e a transforma em uma variável de instância disponível, intuitivamente, apenas para essa instância .

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

Quando você usaria o atributo class?

  • Armazenando constantes . Como os atributos da classe podem ser acessados ​​como atributos da própria classe, geralmente é bom usá-los para armazenar constantes em toda a classe e específicas da classe

    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
  • Definindo valores padrão . Como um exemplo trivial, podemos criar uma lista limitada (ou seja, uma lista que pode conter apenas um determinado número de elementos ou menos) e optar por ter um limite padrão de 10 itens

    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
fonte
19

Como as pessoas nos comentários aqui e em duas outras perguntas marcadas como duplas parecem estar confusas sobre isso da mesma maneira, acho que vale a pena adicionar uma resposta adicional sobre a de Alex Coventry .

O fato de Alex estar atribuindo um valor de um tipo mutável, como uma lista, não tem nada a ver com o fato de as coisas serem compartilhadas ou não. Podemos ver isso com a idfunção ou o 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

(Se você está se perguntando por que eu usei, em object()vez de, digamos, 5isso, é para evitar dois outros problemas que eu não quero abordar aqui; por duas razões diferentes, s totalmente criados separadamente 5podem acabar sendo o mesma instância do número 5. Mas s totalmente criados separadamente object()não podem.)


Então, por que isso a.foo.append(5)afeta no exemplo de Alex b.foo, mas a.foo = 5no meu exemplo não? Bem, tente a.foo = 5no exemplo de Alex, e aviso de que ela não afeta b.foonão quer .

a.foo = 5está apenas criando a.fooum nome para 5. Isso não afeta b.foo, ou qualquer outro nome para o valor antigo que a.foocostumava se referir. * É um pouco complicado criar um atributo de instância que oculte um atributo de classe **, mas quando você obtém isso, nada é complicado. acontecendo aqui.


Espero que agora seja óbvio porque Alex usou uma lista: o fato de você poder alterar uma lista significa que é mais fácil mostrar que duas variáveis ​​denominam a mesma lista e também significa que é mais importante no código da vida real saber se você tem duas listas ou dois nomes para a mesma lista.


* A confusão para pessoas provenientes de uma linguagem como C ++ é que, no Python, os valores não são armazenados em variáveis. Os valores residem em áreas de valor, por si só, variáveis ​​são apenas nomes de valores e a atribuição apenas cria um novo nome para um valor. Se ajudar, pense em cada variável Python como uma em shared_ptr<T>vez de a T.

** Algumas pessoas aproveitam isso usando um atributo de classe como um "valor padrão" para um atributo de instância que as instâncias podem ou não definir. Isso pode ser útil em alguns casos, mas também pode ser confuso, portanto, tenha cuidado com isso.

abarnert
fonte
0

Há mais uma situação.

Os atributos de classe e instância são Descritor .

# -*- 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()

Acima irá produzir:

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

O mesmo tipo de acesso à instância por classe ou instância retorna resultados diferentes!

E eu encontrei na definição c.PyObject_GenericGetAttr, e um ótimo post .

Explicar

Se o atributo for encontrado no dicionário das classes que compõem. os objetos MRO e verifique se o atributo que está sendo pesquisado aponta para um Descritor de Dados (que nada mais é do que uma classe implementando __get__os __set__métodos e os ). Caso isso aconteça, resolva a pesquisa de atributo chamando o __get__método do Descritor de Dados (linhas 28 a 33).

SimonShyu
fonte