Python:¿Cómo funciona la herencia de __ranuras__ en las subclases en realidad?
-
08-07-2019 - |
Pregunta
En el Python modelo de datos de la sección de referencia en las ranuras hay una lista de notas sobre el uso de __slots__
.Estoy completamente confundido por el 1 y 6 de los artículos, porque parece ser que contradicen el uno al otro.
Primer elemento:
- Cuando se hereda de una clase sin
__slots__
, el__dict__
atributo de que clase siempre se accesible, por lo que un__slots__
definición en la subclase es carece de sentido.
Sexto punto:
- La acción de un
__slots__
la declaración se limita a la clase donde está definido.Como resultado, subclases tendrá un__dict__
a menos que también definir__slots__
(que sólo debe contener los nombres de cualquier ranuras adicionales).
A mí me parece que estos elementos podría ser mejor redactado o muestre a través de código, pero he estado tratando de envolver mi cabeza alrededor de esto y todavía estoy llegando a confundirse.Yo no entiendo cómo __slots__
son supone que se utiliza, y estoy tratando de obtener una mejor comprensión de cómo funcionan.
La Pregunta:
Puede por favor alguien que me explique en lenguaje claro cuáles son las condiciones para la herencia de las ranuras cuando subclases?
(Simples ejemplos de código sería útil pero no necesario.)
Solución
Como otros han mencionado, la única razón para definir __slots__
es ahorrar algo de memoria, cuando tiene objetos simples con un conjunto predefinido de atributos y no desea que cada uno lleve consigo un diccionario. Esto es significativo solo para las clases de las cuales planea tener muchas instancias, por supuesto.
Los ahorros pueden no ser inmediatamente obvios; considere ...:
>>> class NoSlots(object): pass
...
>>> n = NoSlots()
>>> class WithSlots(object): __slots__ = 'a', 'b', 'c'
...
>>> w = WithSlots()
>>> n.a = n.b = n.c = 23
>>> w.a = w.b = w.c = 23
>>> sys.getsizeof(n)
32
>>> sys.getsizeof(w)
36
¡De esto, parecería que el tamaño con ranuras es más grande que el tamaño sin ranuras! Pero eso es un error, porque sys.getsizeof
no considera & Quot; contenido del objeto & Quot; como el diccionario:
>>> sys.getsizeof(n.__dict__)
140
Dado que el dict solo toma 140 bytes, claramente el " 32 bytes " Se supone que el objeto n
toma no considera todo lo que está involucrado en cada instancia. Puede hacer un mejor trabajo con extensiones de terceros como pympler :
>>> import pympler.asizeof
>>> pympler.asizeof.asizeof(w)
96
>>> pympler.asizeof.asizeof(n)
288
Esto muestra mucho más claramente la huella de memoria guardada por a
: para un objeto simple como este caso, es un poco menos de 200 bytes, casi 2/3 de la huella total del objeto. Ahora, dado que en estos días un megabyte más o menos realmente no importa tanto para la mayoría de las aplicaciones, esto también le dice que AB
no vale la pena si va a tener solo unos pocos miles de instancias por vez tiempo, sin embargo, para millones de instancias, seguro que hace una diferencia muy importante. También puede obtener una aceleración microscópica (en parte debido al mejor uso de caché para objetos pequeños con b
):
$ python -mtimeit -s'class S(object): __slots__="x","y"' -s's=S(); s.x=s.y=23' 's.x'
10000000 loops, best of 3: 0.37 usec per loop
$ python -mtimeit -s'class S(object): pass' -s's=S(); s.x=s.y=23' 's.x'
1000000 loops, best of 3: 0.604 usec per loop
$ python -mtimeit -s'class S(object): __slots__="x","y"' -s's=S(); s.x=s.y=23' 's.x=45'
1000000 loops, best of 3: 0.28 usec per loop
$ python -mtimeit -s'class S(object): pass' -s's=S(); s.x=s.y=23' 's.x=45'
1000000 loops, best of 3: 0.332 usec per loop
pero esto es algo dependiente de la versión de Python (estos son los números que mido repetidamente con 2.5; con 2.6, veo una ventaja relativa mayor a A
para configurar un atributo, pero ninguno en todo, de hecho una pequeña ventaja de dis , para obtener eso).
Ahora, con respecto a la herencia: para que una instancia no tenga dict, todas clases en su cadena de herencia también deben tener instancias sin dict. Las clases con instancias sin dict son aquellas que definen <=>, más la mayoría de los tipos incorporados (los tipos incorporados cuyas instancias tienen dictados son aquellos en cuyas instancias puede establecer atributos arbitrarios, como funciones). Las superposiciones en los nombres de las ranuras no están prohibidas, pero son inútiles y desperdician algo de memoria, ya que las ranuras se heredan:
>>> class A(object): __slots__='a'
...
>>> class AB(A): __slots__='b'
...
>>> ab=AB()
>>> ab.a = ab.b = 23
>>>
como puede ver, puede establecer el atributo <=> en una instancia de <=> - <=> solo define el espacio <=>, pero hereda el espacio <=> de <=>. No está prohibido repetir el espacio heredado:
>>> class ABRed(A): __slots__='a','b'
...
>>> abr=ABRed()
>>> abr.a = abr.b = 23
pero desperdicia un poco de memoria:
>>> pympler.asizeof.asizeof(ab)
88
>>> pympler.asizeof.asizeof(abr)
96
así que realmente no hay razón para hacerlo.
Otros consejos
class WithSlots(object):
__slots__ = "a_slot"
class NoSlots(object): # This class has __dict__
pass
Primer elemento
class A(NoSlots): # even though A has __slots__, it inherits __dict__
__slots__ = "a_slot" # from NoSlots, therefore __slots__ has no effect
Sexto elemento
class B(WithSlots): # This class has no __dict__
__slots__ = "some_slot"
class C(WithSlots): # This class has __dict__, because it doesn't
pass # specify __slots__ even though the superclass does.
Probablemente no necesite usar __slots__
en el futuro cercano. Solo pretende ahorrar memoria a costa de cierta flexibilidad. A menos que tenga decenas de miles de objetos, no importará.
Python: ¿Cómo funciona realmente la herencia de
__slots__
en las subclases?Estoy completamente confundido por los ítems primero y sexto, porque parecen estar contradiciéndose entre sí.
Esos elementos no se contradicen entre sí. El primero se refiere a las subclases de clases que no implementan __dict__
, el segundo se refiere a las subclases de clases que sí implementan Bar
.
Subclases de clases que no implementan __weakref__
Cada vez soy más consciente de que, aunque los documentos de Python tienen (correctamente) fama de ser, no son perfectos, especialmente con respecto a las características menos utilizadas del lenguaje. Alteraría los docs de la siguiente manera:
Cuando se hereda de una clase sin <=>, el atributo <=> de esa clase siempre estará accesible
, por lo que una definición <=> en la subclase no tiene sentido.
<=> sigue siendo significativo para dicha clase. Documenta los nombres esperados de los atributos de la clase. También crea espacios para esos atributos: obtendrán búsquedas más rápidas y usarán menos espacio. Simplemente permite otros atributos, que se asignarán a <=>.
Este cambio ha sido aceptado y ahora está en última documentación .
Aquí hay un ejemplo:
class Foo:
"""instances have __dict__"""
class Bar(Foo):
__slots__ = 'foo', 'bar'
<=> no solo tiene las ranuras que declara, también tiene las ranuras de Foo, que incluyen <=>:
>>> b = Bar()
>>> b.foo = 'foo'
>>> b.quux = 'quux'
>>> vars(b)
{'quux': 'quux'}
>>> b.foo
'foo'
Subclases de clases que hacen implementan <=>
La acción de una declaración <=> se limita a la clase donde se define. Como resultado, las subclases tendrán un <=> a menos que también defina <=> (que solo debe contener nombres de cualquier adicional ranuras).
Bueno, eso tampoco está del todo bien. La acción de una declaración <=> es no completamente limitada a la clase donde se define. Pueden tener implicaciones para la herencia múltiple, por ejemplo.
Cambiaría eso a:
Para las clases en un árbol de herencia que define <=>, las subclases tendrán un <=> a menos que también defina <=> (que solo debe contener nombres de cualquier adicional ranuras).
Realmente lo actualicé para leer:
La acción de una declaración <=> no se limita a la clase donde se define <=> declarado en padres están disponibles en Clases para niños. Sin embargo, las subclases secundarias obtendrán un <=> y <=> a menos que también definan <=> (que solo debe contener nombres de espacios adicionales).
Aquí hay un ejemplo:
class Foo:
__slots__ = 'foo'
class Bar(Foo):
"""instances get __dict__ and __weakref__"""
Y vemos que una subclase de una clase con ranuras puede usar las ranuras:
>>> b = Bar()
>>> b.foo = 'foo'
>>> b.bar = 'bar'
>>> vars(b)
{'bar': 'bar'}
>>> b.foo
'foo'
(Para más información sobre <=>, vea mi respuesta aquí .)
De la respuesta que vinculó:
El uso apropiado de
__slots__
es ahorrar espacio en los objetos. En lugar de tener un dict dinámico ...
" Al heredar de una clase sin __dict__
, el atributo <=> de esa clase siempre estará accesible " ;, por lo que agregar su propio <=> no puede evitar que los objetos tengan un < =>, y no puede ahorrar espacio.
El bit sobre <=> no se hereda es un poco obtuso. Recuerde que es un atributo mágico y no se comporta como otros atributos, luego vuelva a leer eso como diciendo que este comportamiento de las tragamonedas mágicas no se hereda. (Eso es todo lo que hay que hacer).
Mi entendimiento es como sigue:
clase
X
no tiene__dict__
<------->
claseX
y sus superclases todos tienen__slots__
especificadoen este caso, el real ranuras de la clase se compone de la unión de
__slots__
las declaraciones deX
y sus superclases;el comportamiento no está definido (y se convertirá en un error) si esta unión no es distinto