Atributos e protocolo de manipulação em Python

Considere o seguinte código:


class Foo: def __init__(self): self.bar = 'hello!' foo = Foo() print(foo.bar) 

Hoje vamos analisar a resposta à pergunta: "O que exatamente acontece quando escrevemos foo.bar ?"




Você já deve saber que a maioria dos objetos tem um dicionário interno __dict__ contendo todos os seus atributos. E o que é especialmente agradável é como é fácil estudar esses detalhes de baixo nível no Python:


 >>> foo = Foo() >>> foo.__dict__ {'bar': 'hello!'} 

Vamos começar tentando formular esta hipótese (incompleta):


foo.bar é equivalente a foo .__ dict __ ['bar'] .


Embora pareça a verdade:


 >>> foo = Foo() >>> foo.__dict__['bar'] 'hello!' 


Agora, suponha que você já saiba que os atributos dinâmicos podem ser declarados nas classes:


 >>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.__dict__ {'bar': 'hello!'} 

Hmm ... tudo bem. Pode-se observar que __getattr__ pode emular o acesso a atributos "falsos", mas não funcionará se já houver uma variável declarada (como foo.bar que retorna 'olá!' E não 'adeus!' ). Tudo parece ser um pouco mais complicado do que parecia no começo.


E, de fato: existe um método mágico chamado sempre que tentamos obter um atributo, mas, como o exemplo acima mostrou, esse não é __getattr__ . O método chamado é chamado __getattribute__ , e tentaremos entender exatamente como ele funciona, observando várias situações.


Até agora, modificamos nossa hipótese da seguinte maneira:


foo.bar é equivalente a foo .__ getattribute __ ('bar') , que funciona aproximadamente assim:



 def __getattribute__(self, item): if item in self.__dict__: return self.__dict__[item] return self.__getattr__(item) 

Vamos testá-lo implementando este método (com um nome diferente) e chamando-o diretamente:


 >>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... def my_getattribute(self, item): ... if item in self.__dict__: ... return self.__dict__[item] ... return self.__getattr__(item) >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.my_getattribute('bar') 'hello!' >>> foo.my_getattribute('baz') 'goodbye!' 

Parece certo, certo?



Bem, tudo o que resta é verificar se a atribuição de variáveis ​​é suportada, após o qual você pode ir para casa ... -


 >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.my_getattribute('baz') = 'h4x0r' SyntaxError: can't assign to function call 

Inferno


my_getattribute retorna um objeto. Podemos alterá-lo se for mutável, mas não podemos substituí-lo por outro usando o operador de atribuição. O que fazer? Afinal, se foo.baz é o equivalente a chamar uma função, como podemos atribuir um novo valor a um atributo em princípio?


Quando olhamos para uma expressão como foo.bar = 1 , há mais do que apenas chamar uma função para obter o valor de foo.bar . Atribuir um valor a um atributo parece ser fundamentalmente diferente de obter um valor de atributo. E a verdade: podemos implementar __setattr__ para ver isso:


 >>> class Foo: ... def __init__(self): ... self.__dict__['my_dunder_dict'] = {} ... self.bar = 'hello!' ... ... def __setattr__(self, item, value): ... self.my_dunder_dict[item] = value ... ... def __getattr__(self, item): ... return self.my_dunder_dict[item] >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.bar = 'goodbye!' >>> foo.bar 'goodbye!' >>> foo.baz Traceback (most recent call last): File "<pyshell#75>", line 1, in <module> foo.baz File "<pyshell#70>", line 10, in __getattr__ return self.my_dunder_dict[item] KeyError: 'baz' >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.__dict__ {'my_dunder_dict': {'bar': 'goodbye!', 'baz': 1337}} 

Algumas coisas a serem observadas em relação a este código:


  1. __setattr__ não tem contrapartida de __getattribute__ (ou seja, o método mágico __setattribute__ não existe).
  2. __setattr__ é chamado dentro de __init__ , e é por isso que somos forçados a fazer o self .__ dict __ ['my_dunder_dict'] = {} em vez de self.my_dunder_dict = {} . Caso contrário, encontraríamos uma recursão infinita.


Mas também temos propriedades (e seus amigos). Um decorador que permite que os métodos atuem como atributos.


Vamos tentar entender como isso acontece.


 >>> class Foo(object): ... def __getattribute__(self, item): ... print('__getattribute__ was called') ... return super().__getattribute__(item) ... ... def __getattr__(self, item): ... print('__getattr__ was called') ... return super().__getattr__(item) ... ... @property ... def bar(self): ... print('bar property was called') ... return 100 >>> f = Foo() >>> f.bar __getattribute__ was called bar property was called 

Apenas por diversão, o que temos em f .__ dict__ ?


 >>> f.__dict__ __getattribute__ was called {} 

Não há chave de barra em __dict__ , mas __getattr__ não é chamado por algum motivo. Wat?


bar é um método que também se assume como parâmetro, apenas esse método está na classe e não na instância da classe. E isso é fácil de ver:


 >>> Foo.__dict__ mappingproxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__doc__': None, '__getattr__': <function Foo.__getattr__ at 0x038308A0>, '__getattribute__': <function Foo.__getattribute__ at 0x038308E8>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, 'bar': <property object at 0x0381EC30>}) 

A chave da barra está realmente no dicionário de atributos da classe. Para entender como o __getattribute__ funciona , precisamos responder à pergunta: De quem __getattribute__ é chamado antes - uma classe ou uma instância?


 >>> f.__dict__['bar'] = 'will we see this printed?' __getattribute__ was called >>> f.bar __getattribute__ was called bar property was called 100 

Pode-se observar que a primeira coisa que a verificação está na classe __dict__ , ou seja, tem prioridade sobre a instância.



Espere um minuto, quando chamamos o método bar ? Quero dizer, nosso pseudo-código para __getattribute__ nunca chama um objeto. O que está havendo?


Conheça o protocolo do descritor :


descr .__ get __ (self, obj, type = None) -> valor


descr .__ set __ (self, obj, value) -> Nenhum


descr .__ delete __ (self, obj) -> nenhum



A questão toda está aqui. Implemente qualquer um desses três métodos para que o objeto se torne um descritor e possa alterar o comportamento padrão quando for tratado como um atributo.



Se um objeto declarar __get __ () e __set __ () , será chamado de descritor de dados. Descritores que implementam apenas __get __ () são chamados de descritores que não são de dados.



Os dois tipos de descritores diferem na maneira como os elementos do dicionário de atributos do objeto são substituídos. Se o dicionário contiver uma chave com o mesmo nome que o descritor de dados, o descritor de dados terá precedência (ou seja, __set __ () é chamado). Se o dicionário contiver uma chave com o mesmo nome que o descritor sem dados, o dicionário terá prioridade (ou seja, o elemento do dicionário será substituído).



Para criar um descritor de dados somente leitura, declare __get __ () e __set __ () , onde __set __ () lança um AttributeError quando chamado. A implementação desse __set __ () é suficiente para criar um descritor de dados.


Em resumo, se você declarou algum desses métodos __get__ , __set__ ou __delete__ , implementou o suporte ao protocolo do descritor. E é exatamente isso que o decorador de propriedades faz: declara um descritor somente leitura que será chamado em __getattribute__ .


Última mudança em nossa implementação:


foo.bar é equivalente a foo .__ getattribute __ ('bar') , que funciona aproximadamente assim:



 def __getattribute__(self, item): if item in self.__class__.__dict__: v = self.__class__.__dict__[item] elif item in self.__dict__: v = self.__dict__[item] else: v = self.__getattr__(item) if hasattr(v, '__get__'): v = v.__get__(self, type(self)) return v 

Vamos tentar demonstrar na prática:


 class Foo: class_attr = "I'm a class attribute!" def __init__(self): self.dict_attr = "I'm in a dict!" @property def property_attr(self): return "I'm a read-only property!" def __getattr__(self, item): return "I'm dynamically returned!" def my_getattribute(self, item): if item in self.__class__.__dict__: print('Retrieving from self.__class__.__dict__') v = self.__class__.__dict__[item] elif item in self.__dict__: print('Retrieving from self.__dict__') v = self.__dict__[item] else: print('Retrieving from self.__getattr__') v = self.__getattr__(item) if hasattr(v, '__get__'): print("Invoking descriptor's __get__") v = v.__get__(self, type(self)) return v 


 >>> foo = Foo() ... ... print(foo.class_attr) ... print(foo.dict_attr) ... print(foo.property_attr) ... print(foo.dynamic_attr) ... ... print() ... ... print(foo.my_getattribute('class_attr')) ... print(foo.my_getattribute('dict_attr')) ... print(foo.my_getattribute('property_attr')) ... print(foo.my_getattribute('dynamic_attr')) I'm a class attribute! I'm in a dict! I'm a read-only property! I'm dynamically returned! Retrieving from self.__class__.__dict__ I'm a class attribute! Retrieving from self.__dict__ I'm in a dict! Retrieving from self.__class__.__dict__ Invoking descriptor's __get__ I'm a read-only property! Retrieving from self.__getattr__ I'm dynamically returned! 


Nós apenas arranhamos a superfície da implementação do atributo em Python um pouco. Embora nossa última tentativa de emular o foo.bar seja geralmente correta, lembre-se de que sempre pode haver pequenos detalhes implementados de maneira diferente.


Espero que, além de saber como os atributos funcionam, eu também tenha conseguido transmitir a beleza da linguagem que o incentiva a experimentar. Saldar parte da dívida do conhecimento hoje.

Source: https://habr.com/ru/post/pt479824/


All Articles