No se pueden establecer atributos en la instancia de la clase "objeto"

87

Entonces, estaba jugando con Python mientras respondía esta pregunta , y descubrí que esto no es válido:

o = object()
o.attr = 'hello'

debido a un AttributeError: 'object' object has no attribute 'attr'. Sin embargo, con cualquier clase heredada del objeto, es válida:

class Sub(object):
    pass

s = Sub()
s.attr = 'hello'

La impresión s.attrmuestra "hola" como se esperaba. ¿Por qué es este el caso? ¿Qué especifica la especificación del lenguaje Python que no se pueden asignar atributos a los objetos vanilla?

Smashery
fuente
Puras conjeturas: ¿El objecttipo es inmutable y no se pueden agregar nuevos atributos? Esto parece que tendría más sentido.
Chris Lutz
2
@ S.Lott: Vea la primera línea de esta pregunta. Pura curiosidad.
Smashery
3
Su título es engañoso, está tratando de establecer atributos en objectinstancias de clase, no en objectclase.
dhill

Respuestas:

129

Para soportar la asignación de atributos arbitrarios, un objeto necesita un __dict__: un dictado asociado con el objeto, donde se pueden almacenar atributos arbitrarios. De lo contrario, no hay ningún lugar donde colocar nuevos atributos.

Una instancia de objectqué no llevar un __dict__- si lo hiciera, antes de que el problema de la dependencia circular horribles (ya que dict, como casi todo lo demás, hereda de object;-), esto sería cargar a cada objeto en Python con un diccionario, lo que significaría una sobrecarga de muchos bytes por objeto que actualmente no tiene o necesita un dict (esencialmente, todos los objetos que no tienen atributos asignables arbitrariamente no tienen o necesitan un dict).

Por ejemplo, usando el excelente pymplerproyecto (puede obtenerlo a través de svn desde aquí ), podemos hacer algunas medidas ...:

>>> from pympler import asizeof
>>> asizeof.asizeof({})
144
>>> asizeof.asizeof(23)
16

No querría que cada uno intocupe 144 bytes en lugar de solo 16, ¿verdad? -)

Ahora, cuando haces una clase (heredando de lo que sea), las cosas cambian ...:

>>> class dint(int): pass
... 
>>> asizeof.asizeof(dint(23))
184

... el __dict__ se añade ahora (además, un poco más sobrecarga) - por lo que una dintinstancia puede tener atributos arbitrarios, pero que pagar un costo de un espacio para que la flexibilidad.

Entonces, ¿qué pasaría si quisieras ints con solo un atributo adicional foobar...? Es una necesidad poco común, pero Python ofrece un mecanismo especial para ese propósito ...

>>> class fint(int):
...   __slots__ = 'foobar',
...   def __init__(self, x): self.foobar=x+100
... 
>>> asizeof.asizeof(fint(23))
80

... no es bastante tan pequeño como una int, que importa! (o incluso los dos ints, uno el selfy otro el self.foobar- el segundo se puede reasignar), pero seguramente mucho mejor que a dint.

Cuando la clase tiene el __slots__atributo especial (una secuencia de cadenas), entonces la classdeclaración (más precisamente, la metaclase predeterminada type) no equipa cada instancia de esa clase con un __dict__(y por lo tanto la capacidad de tener atributos arbitrarios), solo un finito , conjunto rígido de "ranuras" (básicamente lugares que pueden contener una referencia a algún objeto) con los nombres de pila.

A cambio de la flexibilidad perdida, se obtiene una gran cantidad de bytes por ejemplo (probablemente significativas sólo si tiene millones y millones de casos gallivanting alrededor, pero, no son casos de uso para eso).

Alex Martelli
fuente
5
Esto explica cómo se implementa el mecanismo, pero no explica por qué se implementa de esta manera. Puedo pensar en al menos dos o tres formas de implementar la adición de dictado sobre la marcha que no tendrá la desventaja general, pero agregará algo de simplicidad.
Георги Кременлиев
Tenga en cuenta que no vacío __slots__no funcionan con los tipos de longitud variable tales como str, tupley en Python 3 también int.
arekolek
Es una gran explicación, pero aún no responde por qué (o cómo) Subtiene el __dict__atributo y el objeto no, siendo que Subhereda object, ¿cómo se __module__agrega ese atributo (y otros similares ) en la herencia? Puede ser que esta sea una nueva pregunta
Rodrigo E. Principe
2
Un objeto __dict__solo se crea la primera vez que se necesita, por lo que la situación del costo de la memoria no es tan simple como asizeofparece la salida. ( asizeofno sabe cómo evitar la __dict__materialización). Puede ver que el dict no se materializa hasta que sea necesario en este ejemplo , y puede ver una de las rutas de código responsable de la __dict__materialización aquí .
user2357112 apoya a Monica el
17

Como han dicho otras personas que respondieron, an objectno tiene un __dict__. objectes la clase base de todos los tipos, incluidos into str. Por lo tanto, todo lo que les proporcione objectserá una carga para ellos también. Incluso algo tan simple como opcional __dict__ necesitaría un puntero adicional para cada valor; esto desperdiciaría 4-8 bytes adicionales de memoria para cada objeto en el sistema, por una utilidad muy limitada.


En lugar de hacer una instancia de una clase ficticia, en Python 3.3+, puede (y debe) usar types.SimpleNamespacepara esto.

Antti Haapala
fuente
4

Simplemente se debe a la optimización.

Los dictados son relativamente grandes.

>>> import sys
>>> sys.getsizeof((lambda:1).__dict__)
140

La mayoría (quizás todas) de las clases que se definen en C no tienen un dictado para la optimización.

Si observa el código fuente , verá que hay muchas comprobaciones para ver si el objeto tiene un dict o no.

Desconocido
fuente
1

Entonces, investigando mi propia pregunta, descubrí esto sobre el lenguaje Python: puedes heredar de cosas como int, y ves el mismo comportamiento:

>>> class MyInt(int):
       pass

>>> x = MyInt()
>>> print x
0
>>> x.hello = 4
>>> print x.hello
4
>>> x = x + 1
>>> print x
1
>>> print x.hello
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
AttributeError: 'int' object has no attribute 'hello'

Supongo que el error al final se debe a que la función de adición devuelve un int, por lo que tendría que anular funciones como __add__esta para conservar mis atributos personalizados. Pero todo esto ahora tiene sentido para mí (creo), cuando pienso en "objeto" como "int".

Smashery
fuente
0

Es porque el objeto es un "tipo", no una clase. En general, todas las clases que se definen en extensiones C (como todos los tipos de datos incorporados y cosas como matrices numpy) no permiten la adición de atributos arbitrarios.

Ryan
fuente
Pero object () es un objeto, al igual que Sub () es un objeto. Tengo entendido que tanto s como o son objetos. Entonces, ¿cuál es la diferencia fundamental entre s y o? ¿Es que uno es un tipo instanciado y el otro es una clase instanciada?
Smashery
Bingo. Ese es exactamente el problema.
Ryan
1
En Python 3, la diferencia entre tipos y clases no existe realmente. Así que "tipo" y "clase" ahora son bastante sinónimos. Pero aún no puede agregar atributos a aquellas clases que no tienen __dict__, por las razones dadas por Alex Martelli.
PM 2 Ring
-2

Esta es (IMO) una de las limitaciones fundamentales de Python: no puede volver a abrir las clases. Sin embargo, creo que el problema real es causado por el hecho de que las clases implementadas en C no se pueden modificar en tiempo de ejecución ... las subclases sí, pero no las clases base.

Pedro
fuente