Atributos de classe do Python: um guia muito completo
Publicados: 2022-03-11Recentemente, tive uma entrevista de programação, uma tela de telefone em que usamos um editor de texto colaborativo.
Pediram-me para implementar uma determinada API e optei por fazê-lo em Python. Abstraindo a declaração do problema, digamos que eu precise de uma classe cujas instâncias armazenam alguns data
e alguns other_data
.
Respirei fundo e comecei a digitar. Depois de algumas linhas, eu tinha algo assim:
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Meu entrevistador me interrompeu:
- Entrevistador: “Aquela linha:
data = []
. Eu não acho que isso seja válido em Python?” - Eu: “Tenho certeza que sim. Está apenas definindo um valor padrão para o atributo de instância.”
- Entrevistador: “Quando esse código é executado?”
- Eu: “Não tenho certeza. Vou consertar isso para evitar confusão.”
Para referência, e para dar uma ideia do que eu estava procurando, aqui está como alterei o código:
class Service(object): def __init__(self, other_data): self.data = [] self.other_data = other_data ...
Como se vê, nós dois estávamos errados. A resposta real está em entender a distinção entre os atributos de classe do Python e os atributos de instância do Python.
Observação: se você tiver um conhecimento especializado em atributos de classe, poderá pular para os casos de uso.
Atributos de classe do Python
Meu entrevistador estava errado porque o código acima é sintaticamente válido.
Eu também estava errado porque não está definindo um “valor padrão” para o atributo de instância. Em vez disso, está definindo data
como um atributo de classe com valor []
.
Na minha experiência, os atributos de classe do Python são um tópico sobre o qual muitas pessoas sabem algo , mas poucas entendem completamente.
Variável de classe Python versus variável de instância: qual é a diferença?
Um atributo de classe Python é um atributo da classe (circular, eu sei), em vez de um atributo de uma instância de uma classe.
Vamos usar um exemplo de classe Python para ilustrar a diferença. Aqui, class_var
é um atributo de classe e i_var
é um atributo de instância:
class MyClass(object): class_var = 1 def __init__(self, i_var): self.i_var = i_var
Observe que todas as instâncias da classe têm acesso a class_var
, e que também pode ser acessada como propriedade da própria classe :
foo = MyClass(2) bar = MyClass(3) foo.class_var, foo.i_var ## 1, 2 bar.class_var, bar.i_var ## 1, 3 MyClass.class_var ## <— This is key ## 1
Para programadores Java ou C++, o atributo class é semelhante — mas não idêntico — ao membro estático. Veremos como eles diferem mais tarde.
Namespaces de classe x instância
Para entender o que está acontecendo aqui, vamos falar brevemente sobre namespaces Python .
Um namespace é um mapeamento de nomes para objetos, com a propriedade de que há relação zero entre nomes em namespaces diferentes. Eles geralmente são implementados como dicionários Python, embora isso seja abstraído.
Dependendo do contexto, você pode precisar acessar um namespace usando a sintaxe de ponto (por exemplo, object.name_from_objects_namespace
) ou como uma variável local (por exemplo, object_from_namespace
). Como exemplo concreto:
class MyClass(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 MyClass.class_var ## 1
Classes Python e instâncias de classes têm seus próprios namespaces distintos representados por atributos predefinidos MyClass.__dict__
e instance_of_MyClass.__dict__
, respectivamente.
Quando você tenta acessar um atributo de uma instância de uma classe, ele primeiro examina seu namespace de instância . Se encontrar o atributo, ele retornará o valor associado. Caso contrário, ele procura no namespace da classe e retorna o atributo (se estiver presente, lançando um erro caso contrário). Por exemplo:
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 (MyClass.__dict__) foo.class_var ## 1
O namespace da instância tem supremacia sobre o namespace da classe: se houver um atributo com o mesmo nome em ambos, o namespace da instância será verificado primeiro e seu valor retornado. Aqui está uma versão simplificada do código (fonte) para pesquisa de atributos:
def instlookup(inst, name): ## simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]
E, em forma visual:
Como os atributos de classe lidam com a atribuição
Com isso em mente, podemos entender como os atributos de classe do Python lidam com a atribuição:
Se um atributo de classe for definido ao acessar a classe, ele substituirá o valor para todas as instâncias. Por exemplo:
foo = MyClass(2) foo.class_var ## 1 MyClass.class_var = 2 foo.class_var ## 2
No nível do namespace… estamos configurando
MyClass.__dict__['class_var'] = 2
. (Nota: este não é o código exato (que seriasetattr(MyClass, 'class_var', 2)
) já que__dict__
retorna um dictproxy, um wrapper imutável que impede a atribuição direta, mas ajuda para fins de demonstração). Então, quandofoo.class_var
,class_var
tem um novo valor no namespace da classe e assim 2 é retornado.Se uma variável de classe Paython for definida acessando uma instância, ela substituirá o valor apenas para essa instância . Isso essencialmente substitui a variável de classe e a transforma em uma variável de instância disponível, intuitivamente, apenas para essa instância . Por exemplo:
foo = MyClass(2) foo.class_var ## 1 foo.class_var = 2 foo.class_var ## 2 MyClass.class_var ## 1
No nível do namespace… estamos adicionando o atributo
class_var
afoo.__dict__
, então quando pesquisamosfoo.class_var
, retornamos 2. Enquanto isso, outras instâncias deMyClass
não terãoclass_var
em seus namespaces de instância, então elas continuam a encontrarclass_var
emMyClass.__dict__
e assim retornar 1.
Mutabilidade
Pergunta do quiz: E se seu atributo de classe tiver um tipo mutável ? Você pode manipular (mutilar?) o atributo class acessando-o através de uma determinada instância e, por sua vez, acabar manipulando o objeto referenciado que todas as instâncias estão acessando (como apontado por Timothy Wiseman).
Isso é melhor demonstrado pelo exemplo. Vamos voltar ao Service
que defini anteriormente e ver como meu uso de uma variável de classe pode ter levado a problemas no futuro.
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Meu objetivo era ter a lista vazia ( []
) como o valor padrão para data
, e para cada instância de Service
ter seus próprios dados que seriam alterados ao longo do tempo instância por instância. Mas neste caso, obtemos o seguinte comportamento (lembre-se de que Service
recebe algum argumento other_data
, que é arbitrário neste exemplo):
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data.append(1) s1.data ## [1] s2.data ## [1] s2.data.append(2) s1.data ## [1, 2] s2.data ## [1, 2]
Isso não é bom - alterar a variável de classe por meio de uma instância a altera para todas as outras!
No nível de namespace… todas as instâncias de Service
estão acessando e modificando a mesma lista em Service.__dict__
sem criar seus próprios atributos de data
em seus namespaces de instância.
Poderíamos contornar isso usando atribuição; ou seja, ao invés de explorar a mutabilidade da lista, poderíamos atribuir nossos objetos Service
a terem suas próprias listas, como segue:
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data = [1] s2.data = [2] s1.data ## [1] s2.data ## [2]
Nesse caso, estamos adicionando s1.__dict__['data'] = [1]
, portanto, o Service.__dict__['data']
original permanece inalterado.
Infelizmente, isso requer que os usuários Service
tenham um conhecimento profundo de suas variáveis e certamente está sujeito a erros. De certa forma, estaríamos abordando os sintomas e não a causa. Preferimos algo que fosse correto por construção.
Minha solução pessoal: se você estiver usando apenas uma variável de classe para atribuir um valor padrão a uma possível variável de instância do Python, não use valores mutáveis . Nesse caso, cada instância de Service
iria substituir Service.data
com seu próprio atributo de instância eventualmente, então usar uma lista vazia como padrão levou a um pequeno bug que foi facilmente ignorado. Em vez do acima, poderíamos ter:
- Preso inteiramente aos atributos de instância, conforme demonstrado na introdução.
Evitamos usar a lista vazia (um valor mutável) como nosso “padrão”:
class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...
É claro que teríamos que lidar com o caso
None
adequadamente, mas esse é um preço pequeno a pagar.
Então, quando você deve usar os atributos de classe do Python?
Atributos de classe são complicados, mas vamos ver alguns casos em que eles seriam úteis:
Armazenando constantes . Como os atributos de classe podem ser acessados como atributos da própria classe, geralmente é bom usá-los para armazenar constantes de classe e específicas de classe. Por exemplo:
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 certo 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
Também poderíamos criar instâncias com seus próprios limites específicos, atribuindo ao atributo
limit
da instância.foo = MyClass() foo.limit = 50 ## foo can now hold 50 elements—other instances can hold 10
Isso só faz sentido se você quiser que sua instância típica de
MyClass
contenha apenas 10 elementos ou menos - se você estiver dando a todas as suas instâncias limites diferentes,limit
deve ser uma variável de instância. (Lembre-se, porém: tome cuidado ao usar valores mutáveis como seus padrões.)Rastreamento de todos os dados em todas as instâncias de uma determinada classe . Isso é meio específico, mas eu pude ver um cenário no qual você pode querer acessar um dado relacionado a cada instância existente de uma determinada classe.
Para tornar o cenário mais concreto, digamos que temos uma classe
Person
, e cada pessoa tem umname
. Queremos manter o controle de todos os nomes que foram usados. Uma abordagem pode ser iterar sobre a lista de objetos do coletor de lixo, mas é mais simples usar variáveis de classe.Observe que, neste caso, os
names
serão acessados apenas como uma variável de classe, portanto, o padrão mutável é aceitável.class Person(object): all_names = [] def __init__(self, name): self.name = name Person.all_names.append(name) joe = Person('Joe') bob = Person('Bob') print Person.all_names ## ['Joe', 'Bob']
Poderíamos até usar esse padrão de design para rastrear todas as instâncias existentes de uma determinada classe, em vez de apenas alguns dados associados.
class Person(object): all_people = [] def __init__(self, name): self.name = name Person.all_people.append(self) joe = Person('Joe') bob = Person('Bob') print Person.all_people ## [<__main__.Person object at 0x10e428c50>, <__main__.Person object at 0x10e428c90>]
Desempenho (mais ou menos... veja abaixo).
Sob o capô
Nota: Se você está se preocupando com o desempenho neste nível, talvez não queira usar o Python em primeiro lugar, pois as diferenças serão da ordem de décimos de milissegundo - mas ainda é divertido bisbilhotar um pouco, e ajuda por causa da ilustração.
Lembre-se de que o namespace de uma classe é criado e preenchido no momento da definição da classe. Isso significa que fazemos apenas uma atribuição — sempre — para uma determinada variável de classe, enquanto as variáveis de instância devem ser atribuídas toda vez que uma nova instância é criada. Vamos dar um exemplo.
def called_class(): print "Class assignment" return 2 class Bar(object): y = called_class() def __init__(self, x): self.x = x ## "Class assignment" def called_instance(): print "Instance assignment" return 2 class Foo(object): def __init__(self, x): self.y = called_instance() self.x = x Bar(1) Bar(2) Foo(1) ## "Instance assignment" Foo(2) ## "Instance assignment"
Atribuímos a Bar.y
apenas uma vez, mas instance_of_Foo.y
em cada chamada a __init__
.
Como evidência adicional, vamos usar o desmontador Python:
import dis class Bar(object): y = 2 def __init__(self, x): self.x = x class Foo(object): def __init__(self, x): self.y = 2 self.x = x dis.dis(Bar) ## Disassembly of __init__: ## 7 0 LOAD_FAST 1 (x) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (x) ## 9 LOAD_CONST 0 (None) ## 12 RETURN_VALUE dis.dis(Foo) ## Disassembly of __init__: ## 11 0 LOAD_CONST 1 (2) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (y) ## 12 9 LOAD_FAST 1 (x) ## 12 LOAD_FAST 0 (self) ## 15 STORE_ATTR 1 (x) ## 18 LOAD_CONST 0 (None) ## 21 RETURN_VALUE
Quando olhamos para o código de byte, é novamente óbvio que Foo.__init__
tem que fazer duas atribuições, enquanto Bar.__init__
faz apenas uma.
Na prática, como é realmente esse ganho? Eu serei o primeiro a admitir que os testes de tempo são altamente dependentes de fatores muitas vezes incontroláveis e as diferenças entre eles são muitas vezes difíceis de explicar com precisão.
No entanto, acho que esses pequenos trechos (executados com o módulo Python timeit) ajudam a ilustrar as diferenças entre as variáveis de classe e de instância, então eu os incluí de qualquer maneira.
Observação: estou em um MacBook Pro com OS X 10.8.5 e Python 2.7.2.
Inicialização
10000000 calls to `Bar(2)`: 4.940s 10000000 calls to `Foo(2)`: 6.043s
As inicializações de Bar
são mais rápidas em mais de um segundo, então a diferença aqui parece ser estatisticamente significativa.
Então por que é esse o caso? Uma explicação especulativa : fazemos duas atribuições em Foo.__init__
, mas apenas uma em Bar.__init__
.
Tarefa
10000000 calls to `Bar(2).y = 15`: 6.232s 10000000 calls to `Foo(2).y = 15`: 6.855s 10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s 10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s
Observação: não há como executar novamente seu código de configuração em cada teste com timeit, então temos que reinicializar nossa variável em nosso teste. A segunda linha de tempos representa os tempos acima com os tempos de inicialização calculados anteriormente deduzidos.
Do exposto, parece que Foo
leva apenas cerca de 60% do tempo que Bar
para lidar com as atribuições.
Por que este é o caso? Uma explicação especulativa : quando atribuímos a Bar(2).y
, primeiro procuramos no namespace da instância ( Bar(2).__dict__[y]
), não encontramos y
e depois procuramos no namespace da classe ( Bar.__dict__[y]
), em seguida, fazendo a atribuição adequada. Quando atribuímos a Foo(2).y
, fazemos metade das pesquisas, já que atribuímos imediatamente ao namespace da instância ( Foo(2).__dict__[y]
).
Em resumo, embora esses ganhos de desempenho não importem na realidade, esses testes são interessantes no nível conceitual. Na verdade, espero que essas diferenças ajudem a ilustrar as distinções mecânicas entre variáveis de classe e de instância.
Para concluir
Atributos de classe parecem ser subutilizados em Python; muitos programadores têm impressões diferentes de como funcionam e por que podem ser úteis.
Minha opinião: as variáveis de classe do Python têm seu lugar na escola do bom código. Quando usados com cuidado, eles podem simplificar as coisas e melhorar a legibilidade. Mas quando jogados descuidadamente em uma determinada aula, eles certamente farão você tropeçar.
Apêndice : Variáveis de instância privada
Uma coisa que eu queria incluir, mas não tinha um ponto de entrada natural…
Python não tem variáveis privadas , por assim dizer, mas outra relação interessante entre a nomenclatura de classes e instâncias vem com o desmembramento de nomes.
No guia de estilo do Python, diz-se que as variáveis pseudo-privadas devem ser prefixadas com um sublinhado duplo: '__'. Isso não é apenas um sinal para os outros de que sua variável deve ser tratada de forma privada, mas também uma maneira de impedir o acesso a ela. Aqui está o que quero dizer:
class Bar(object): def __init__(self): self.__zap = 1 a = Bar() a.__zap ## Traceback (most recent call last): ## File "<stdin>", line 1, in <module> ## AttributeError: 'Bar' object has no attribute '__baz' ## Hmm. So what's in the namespace? a.__dict__ {'_Bar__zap': 1} a._Bar__zap ## 1
Veja isso: o atributo de instância __zap
é prefixado automaticamente com o nome da classe para gerar _Bar__zap
.
Embora ainda seja configurável e obtido usando a._Bar__zap
, esse nome mangling é um meio de criar uma variável 'privada', pois impede que você e outros acessem por acidente ou por ignorância.
Edit: como Pedro Werneck gentilmente apontou, esse comportamento é em grande parte destinado a ajudar com subclasses. No guia de estilo PEP 8, eles vêem isso como servindo a dois propósitos: (1) impedir que as subclasses acessem determinados atributos e (2) evitar conflitos de namespace nessas subclasses. Embora útil, o desmembramento de variáveis não deve ser visto como um convite para escrever código com uma distinção público-privada assumida, como está presente em Java.