Hoy me topé con el siguiente comportamiento:
class myobject(object): """Should behave the same as object, right?""" obj = myobject() obj.a = 2 # <- works obj = object() obj.a = 2 # AttributeError: 'object' object has no attribute 'a'Quiero saber cuál es la lógica detrás del diseño del lenguaje para que se comporte de esta manera, porque me parece totalmente paradójico. Rompe mi intuición de que si creo una subclase, sin modificaciones, debería comportarse igual que la clase principal.
EDITAR: muchas de las respuestas sugieren que esto se debe a que queremos poder escribir clases que funcionen con __slots__ en lugar de __dict__ por razones de rendimiento. Sin embargo, podemos hacer:
class myobject_with_slots(myobject): __slots__ = ("x",) obj = myobject_with_slots() obj.x = 2 obj.a = 2 assert "a" in obj.__dict__ # ✔ assert "x" not in obj.__dict__ # ✔ Entonces, parece que podemos tener __slots__ y __dict__ al mismo tiempo, entonces, ¿por qué el object no permite ambos, pero las subclases uno a uno sí?
Porque las clases derivadas tampoco son necesariamente compatibles con setattr .
class myobject(object): """Should behave the same as object!""" __slots__ = () obj = myobject() obj.a = 2 # <- works the same as for object Dado que todos los tipos se derivan de object , la mayoría de los tipos integrados, como list , también son ejemplos.
La asignación de atributos arbitrarios es algo que las subclases de object pueden admitir, pero no todos lo hacen. Por lo tanto, la clase base común tampoco admite esto .
La compatibilidad con atributos arbitrarios suele estar respaldada por la llamada ranura __dict__ . Este es un atributo fijo que contiene un dict literal 1 para almacenar cualquier par de atributo-valor.
De hecho, uno puede definir manualmente la ranura __dict__ para obtener compatibilidad con atributos arbitrarios.
class myobject(object): """Should behave the same as object, right?""" __slots__ = ("__dict__",) obj = myobject() obj.a = 2 # <- works! print(obj.__dict__) # {'a': 2}La conclusión de esta demostración es que los atributos fijos son en realidad el "comportamiento base" de Python; el soporte de atributos arbitrarios se construye en la parte superior cuando es necesario .
Agregar atributos arbitrarios para subtipos de object de manera predeterminada brinda una experiencia de programación más simple. Sin embargo, seguir admitiendo atributos fijos para subtipos de object permite un mejor uso y rendimiento de la memoria.
Modelo de datos:
__slots__El espacio ahorrado [por
__slots__] sobre el uso de__dict__puede ser significativo. La velocidad de búsqueda de atributos también se puede mejorar significativamente.
Tenga en cuenta que es posible definir clases con atributos fijos y atributos arbitrarios. Los atributos fijos se beneficiarán del diseño y el rendimiento mejorados de la memoria; dado que no se almacenan en el __dict__ , su sobrecarga de memoria 2 es menor, pero aún cuesta.
1 Las implementaciones de Python pueden usar diferentes tipos optimizados para __dict__ siempre que se comporten como un dict .
2 Para que su búsqueda basada en hash funcione de manera eficiente con pocas colisiones, un dict debe ser mayor que la cantidad de elementos que almacena.
Considere este código:
class A: __slots__ = () class B(A): __slots__ = ("x", "y") b = B() bz = 1 # AttributeError __slots__ = ("x", "y") significa que las instancias B no tienen un __dict__ y solo pueden tener los atributos x e y . Esto es bueno para el rendimiento.
Si elimina los __slots__ de A , las instancias de A obtienen un __dict__ , lo que significa que también lo hacen sus subclases, particularmente B . Esto reduce la eficacia de __slots__ para mejorar el rendimiento.
Debido a que object es una superclase de todas las clases, puede pensar en él como A aquí, con __slots__ = () y sin __dict__ para que otras clases también puedan evitar tener un __dict__ y beneficiarse completamente de __slots__ personalizados.
object() por defecto no tiene un diccionario de atributos ( __dict__ ). Permite que la clase object() y cualquier cosa que herede de ella guarde unos pocos bytes.
Cada clase en Python hereda de object() . Las clases como str , dict , tuple e int se usan sin cesar tanto interna como externamente.
Tener un diccionario de instancias significa que cada objeto en Python será más grande (consumirá más memoria) y más lento (cada atributo provocará una búsqueda en el diccionario).
Para mejorar la flexibilidad, de forma predeterminada, las clases creadas por el usuario vienen con una instancia __dict__ . Nos permite parchear instancias, enganchar métodos, inyectar dependencias dinámicamente y ofrece una cantidad infinita de beneficios diferentes. Es lo que le da a Python su fuerza como lenguaje de programación dinámico y una de las principales razones de su éxito.
Para evitar la creación de uno, puede configurar __slots__ así:
class A: __slots__ = () A().abc = 123 # Will throw an error No tener un diccionario de instancias significa que el acceso a los atributos regulares puede omitir la búsqueda __dict__ . El acceso más rápido a los atributos conduce a una gran mejora general en el tiempo de ejecución de Python, pero reducirá la flexibilidad en el uso de su clase.
La forma en que funciona la búsqueda de atributos sin usar __dict__ está fuera del alcance de la pregunta. Puede leer más sobre __slots__ en la documentación .
Para tu segunda pregunta:
Cualquier clase creada por el usuario que no tenga __slots__ tiene un diccionario de instancias ( __dict__ ).
Si lo subclasifica, no puede agregar __slots__ y eliminar el diccionario de la clase principal, ya existe.
Tener __slots__ y un diccionario elimina la mayoría de las ventajas de usar __slots__ que ahorra espacio y evita la creación de un diccionario.
>>> import sys >>> class A: ... pass ... >>> class B: ... __slots__ = () ... >>> sys.getsizeof(A()) 48 >>> sys.getsizeof(B()) 32 >>> class C(A): ... __slots__ = () ... >>> sys.getsizeof(C()) 48 >>> C.__dict__ mappingproxy({'__module__': '__main__', '__slots__': (), '__doc__': None})