Considere el siguiente código:
class Foo: def __init__(self): self.bar = 'hello!' foo = Foo() print(foo.bar)
Hoy analizaremos la respuesta a la pregunta: "¿Qué sucede exactamente cuando escribimos foo.bar ?"
Es posible que ya sepa que la mayoría de los objetos tienen un diccionario interno __dict__ que contiene todos sus atributos. Y lo que es especialmente agradable es lo fácil que es estudiar esos detalles de bajo nivel en Python:
>>> foo = Foo() >>> foo.__dict__ {'bar': 'hello!'}
Comencemos tratando de formular esta hipótesis (incompleta):
foo.bar es equivalente a foo .__ dict __ ['bar'] .
Si bien suena como la verdad:
>>> foo = Foo() >>> foo.__dict__['bar'] 'hello!'
Ahora suponga que ya sabe que los atributos dinámicos se pueden declarar en clases:
>>> 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 ... está bien. Se puede ver que __getattr__ puede emular el acceso a los atributos "falsos", pero no funcionará si ya hay una variable declarada (como foo.bar que devuelve '¡hola!' Y no '¡adiós!' ). Todo parece ser un poco más complicado de lo que parecía al principio.
Y de hecho: hay un método mágico que se llama cada vez que intentamos obtener un atributo, pero, como lo mostró el ejemplo anterior, este no es __getattr__ . El método llamado se llama __getattribute__ , e intentaremos entender cómo funciona exactamente observando varias situaciones.
Hasta ahora, modificamos nuestra hipótesis de la siguiente manera:
foo.bar es equivalente a foo .__ getattribute __ ('bar') , que más o menos funciona así:
def __getattribute__(self, item): if item in self.__dict__: return self.__dict__[item] return self.__getattr__(item)
Lo probaremos implementando este método (con un nombre diferente) y llamándolo directamente:
>>> 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!'
Se ve bien, ¿verdad?
Bueno, todo lo que queda es verificar que la asignación de variables sea compatible, después de lo cual puede irse a casa ...
>>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.my_getattribute('baz') = 'h4x0r' SyntaxError: can't assign to function call
El infierno
my_getattribute devuelve un objeto. Podemos cambiarlo si es mutable, pero no podemos reemplazarlo con otro usando el operador de asignación. Que hacer Después de todo, si foo.baz es el equivalente de llamar a una función, ¿cómo podemos asignar un nuevo valor a un atributo en principio?
Cuando miramos una expresión como foo.bar = 1 , hay más que solo llamar a una función para obtener el valor de foo.bar . Asignar un valor a un atributo parece ser fundamentalmente diferente de obtener un valor de atributo. Y la verdad: podemos implementar __setattr__ para ver esto:
>>> 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}}
Un par de cosas a tener en cuenta con respecto a este código:
- __setattr__ no tiene contraparte con __getattribute__ (es decir, el método mágico __setattribute__ no existe).
- __setattr__ se llama dentro de __init__ , por lo que nos vemos obligados a hacer uno mismo .__ dict __ ['my_dunder_dict'] = {} en lugar de self.my_dunder_dict = {} . De lo contrario, nos encontraríamos con una recursión infinita.
Pero también tenemos propiedades (y sus amigos). Un decorador que permite que los métodos actúen como atributos.
Tratemos de entender cómo sucede esto.
>>> 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
Solo por diversión, ¿qué tenemos en f .__ dict__ ?
>>> f.__dict__ __getattribute__ was called {}
No hay una tecla de barra en __dict__ , pero __getattr__ no se llama por alguna razón. Wat?
bar es un método que también toma self como parámetro, solo este método está en la clase y no en la instancia de la clase. Y esto es 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>})
La clave de la barra está en el diccionario de atributos de clase. Para entender cómo funciona __getattribute__ , debemos responder a la pregunta: ¿ A quién se llama __getattribute__ antes, una clase o una instancia?
>>> f.__dict__['bar'] = 'will we see this printed?' __getattribute__ was called >>> f.bar __getattribute__ was called bar property was called 100
Se puede ver que lo primero que hace la verificación es en la clase __dict__ , es decir tiene prioridad sobre la instancia.
Espera un minuto, ¿cuándo llamamos al método de la barra ? Quiero decir, nuestro pseudocódigo para __getattribute__ nunca llama a un objeto. Que esta pasando
Conoce el protocolo descriptor :
descr .__ get __ (self, obj, type = None) -> value
descr .__ set __ (self, obj, value) -> Ninguno
descr .__ delete __ (self, obj) -> Ninguno
Todo el punto está aquí. Implemente cualquiera de estos tres métodos para que el objeto se convierta en un descriptor y pueda cambiar el comportamiento predeterminado cuando se trata como un atributo.
Si un objeto declara __get __ () y __set __ () , se llama descriptor de datos. Los descriptores que implementan solo __get __ () se denominan descriptores que no son de datos.
Ambos tipos de descriptores difieren en cómo se sobrescriben los elementos del diccionario de atributos del objeto. Si el diccionario contiene una clave con el mismo nombre que el descriptor de datos, entonces el descriptor de datos tiene prioridad (es decir, se llama __set __ () ). Si el diccionario contiene una clave con el mismo nombre que el descriptor sin datos, entonces el diccionario tiene prioridad (es decir, se sobrescribe el elemento del diccionario).
Para crear un descriptor de datos de solo lectura, declare tanto __get __ () como __set __ () , donde __set __ () arroja un AttributeError cuando se llama. Implementar este __set __ () es suficiente para crear un descriptor de datos.
En resumen, si declaraste alguno de estos métodos __get__ , __set__ o __delete__ , implementaste soporte para el protocolo descriptor. Y esto es exactamente lo que hace el decorador de propiedades : declara un descriptor de solo lectura que se llamará en __getattribute__ .
Último cambio en nuestra implementación:
foo.bar es equivalente a foo .__ getattribute __ ('bar') , que más o menos funciona así:
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
Tratemos de demostrar en la práctica:
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!
Acabamos de arañar un poco la superficie de la implementación del atributo en Python. Aunque nuestro último intento de emular foo.bar es generalmente correcto, tenga en cuenta que siempre puede haber pequeños detalles implementados de manera diferente.
Espero que, además de saber cómo funcionan los atributos, también logré transmitir la belleza del lenguaje que lo alienta a experimentar. Pague parte de la deuda de conocimiento hoy.