¿Cuál es el atributo __dict __.__ dict__ de una clase Python?

92
>>> class A(object): pass
... 
>>> A.__dict__
<dictproxy object at 0x173ef30>
>>> A.__dict__.__dict__
Traceback (most recent call last):
  File "<string>", line 1, in <fragment>
AttributeError: 'dictproxy' object has no attribute '__dict__'
>>> A.__dict__.copy()
{'__dict__': <attribute '__dict__' of 'A' objects> ... }
>>> A.__dict__['__dict__']
<attribute '__dict__' of 'A' objects> # What is this object?

Si lo hago A.something = 10, esto entra en A.__dict__. ¿Qué es esto <attribute '__dict__' of 'A' objects>que se encuentran en A.__dict__.__dict__, y cuándo se contiene algo?

porgarmingduod
fuente
11
Una variable de ejemplo más adecuada habría sido ive. Al menos habría hecho de esta una A.__dict__['ive']pregunta más ;) Me
despediré

Respuestas:

110

Primero que nada A.__dict__.__dict__es diferente A.__dict__['__dict__']y el primero no existe. Este último es el __dict__atributo que tendrían las instancias de la clase. Es un objeto descriptor que devuelve el diccionario interno de atributos para la instancia específica. En resumen, el __dict__atributo de un objeto no se puede almacenar en el objeto __dict__, por lo que se accede a través de un descriptor definido en la clase.

Para entender esto, tendría que leer la documentación del protocolo descriptor .

La versión corta:

  1. Para una instancia de clase A, el acceso a instance.__dict__es proporcionado por lo A.__dict__['__dict__']que es lo mismo que vars(A)['__dict__'].
  2. Para la clase A, el acceso a A.__dict__lo proporciona type.__dict__['__dict__'](en teoría), que es el mismo que vars(type)['__dict__'].

La versión larga:

Tanto las clases como los objetos proporcionan acceso a los atributos tanto a través del operador de atributo (implementado a través de la clase o metaclase __getattribute__) como del __dict__atributo / protocolo que utiliza vars(ob).

Para los objetos normales, el __dict__objeto crea un dictobjeto separado , que almacena los atributos, y __getattribute__primero intenta acceder a él y obtener los atributos desde allí (antes de intentar buscar el atributo en la clase utilizando el protocolo descriptor y antes de llamar __getattr__). El __dict__descriptor de la clase implementa el acceso a este diccionario.

  • x.namees equivalente a tratar aquellos en orden: x.__dict__['name'], type(x).name.__get__(x, type(x)),type(x).name
  • x.__dict__ hace lo mismo pero omite el primero por razones obvias

Como es imposible que el __dict__de instancese almacene en __dict__la instancia, se accede a él directamente a través del protocolo descriptor y se almacena en un campo especial en la instancia.

Un escenario similar es cierto para las clases, aunque __dict__hay un objeto proxy especial que pretende ser un diccionario (pero puede que no lo sea internamente) y no le permite cambiarlo o reemplazarlo por otro. Este proxy te permite, entre todo lo demás, acceder a los atributos de una clase que le son específicos y no definidos en una de sus bases.

De forma predeterminada, una vars(cls)de una clase vacía lleva tres descriptores: __dict__para almacenar los atributos de las instancias, __weakref__que se usa internamente weakref, y la cadena de documentos de la clase. Los dos primeros podrían desaparecer si lo define __slots__. Entonces no tendrías atributos __dict__y __weakref__, sino que tendrías un atributo de clase único para cada espacio. Los atributos de la instancia entonces no se almacenarían en un diccionario, y el acceso a ellos será proporcionado por los descriptores respectivos en la clase.


Y, por último, la inconsistencia que A.__dict__es diferente A.__dict__['__dict__']es porque el atributo __dict__, por excepción, nunca se busca vars(A), por lo que lo que es cierto no es cierto para prácticamente cualquier otro atributo que usaría. Por ejemplo, A.__weakref__es lo mismo que A.__dict__['__weakref__']. Si esta inconsistencia no existiera, el uso A.__dict__no funcionaría y tendría que usar siempre en su vars(A)lugar.

Rosh Oxymoron
fuente
6
Gracias por la respuesta detallada. Aunque tuve que leerlo varias veces, creo que ha pasado un tiempo desde que aprendí tantos detalles nuevos de Python.
porgarmingduod
¿Por qué exactamente no se __dict__puede almacenar el atributo de un objeto en el del objeto __dict__?
zumgruenenbaum
2
@zumgruenenbaum Dado que __dict__está destinado a almacenar todos los atributos de instancia, un acceso de atributo del formulario obj.xfinalmente se busca en el objeto __dict__, a saber obj.__dict__['x']. Ahora bien, si __dict__no se implementó como un descriptor, esto conduciría a una recursión infinita, ya que para acceder obj.__dict__necesitaría buscarlo como obj.__dict__['__dict__']. El descriptor evita este problema.
a_guest
11

Dado que A.__dict__es un diccionario que almacena Aatributos, A.__dict__['__dict__']es la referencia directa a ese mismo A.__dict__atributo.

A.__dict__contiene una (especie de) referencia a sí mismo. La parte "tipo de" es la razón por la que la expresión A.__dict__devuelve un en dictproxylugar de un normal dict.

>>> class B(object):
...     "Documentation of B class"
...     pass
...
>>> B.__doc__
'Documentation of B class'
>>> B.__dict__
<dictproxy object at 0x00B83590>
>>> B.__dict__['__doc__']
'Documentation of B class'
vz0
fuente
9
A.__dict__['__dict__']no es una referencia a A.__dict__. Implementa el __dict__atributo de las instancias. Para probar esto por sí mismo, A.__dict__['__dict__'].__get__(A(), A)devuelve los atributos de A(), mientras A.__dict__['__dict__'].__get__(A, type)falla.
Rosh Oxymoron
10

¡Exploremos un poco!

>>> A.__dict__['__dict__']
<attribute '__dict__' of 'A' objects>

Me pregunto que es eso

>>> type(A.__dict__['__dict__'])
<type 'getset_descriptor'>

¿Qué atributos tiene un getset_descriptorobjeto?

>>> type(A.__dict__["__dict__"]).__dict__
<dictproxy object at 0xb7efc4ac>

Al hacer una copia de eso dictproxypodemos encontrar algunos atributos interesantes, específicamente __objclass__y __name__.

>>> A.__dict__['__dict__'].__objclass__, A.__dict__['__dict__'].__name__
(<class '__main__.A'>, '__dict__')

Entonces, ¿ __objclass__es una referencia Ay __name__es solo la cadena '__dict__', quizás el nombre de un atributo?

>>> getattr(A.__dict__['__dict__'].__objclass__, A.__dict__['__dict__'].__name__) == A.__dict__
True

¡Ahí lo tenemos! A.__dict__['__dict__']es un objeto al que se puede hacer referencia A.__dict__.

Andrew Clark
fuente
PEP 252 dice que __objclass__es la clase que definió este atributo, no que sea un atributo de esa clase. Esto hace que su getattrejemplo sea incorrecto. Uno más correcto seríagetattr(A().__dict__['__dict__'].__objclass__, A.__dict__['__dict__'].__name__)
Rosh Oxymoron
1
@RoshOxymoron Su expresión aumenta KeyError: '__dict__', contrariamente a la de @ AndrewClark.
Maggyero
9

Puede probar el siguiente ejemplo simple para comprender más de esto:

>>> class A(object): pass
... 
>>> a = A()
>>> type(A)
<type 'type'>
>>> type(a)
<class '__main__.A'>
>>> type(a.__dict__)
<type 'dict'>
>>> type(A.__dict__)
<type 'dictproxy'>
>>> type(type.__dict__)
<type 'dictproxy'>
>>> type(A.__dict__['__dict__'])
<type 'getset_descriptor'>
>>> type(type.__dict__['__dict__'])
<type 'getset_descriptor'>
>>> a.__dict__ == A.__dict__['__dict__'].__get__(a)
True
>>> A.__dict__ == type.__dict__['__dict__'].__get__(A)
True
>>> a.__dict__ == type.__dict__['__dict__'].__get__(A)['__dict__'].__get__(a)
True

A partir del ejemplo anterior, parece que los atributos de los objetos de clase son almacenados por su clase, los atributos de la clase son almacenados por su clase, que son metaclases. Esto también está validado por:

>>> a.__dict__ == A.__getattribute__(a, '__dict__')
True
>>> A.__dict__ == type.__getattribute__(A, '__dict__')
True
damaZhang
fuente
1
Curiosamente, si isse sustituye ==en la segunda comparación, es decir A.__dict__ is type.__dict__['__dict__'].__get__(A), el resultado está Falseen python 2.7.15+ y 3.6.8.
Arne Vogel