Uso de __slots__?

Respuestas:

1020

En Python, ¿cuál es el propósito __slots__y cuáles son los casos en los que se debe evitar esto?

TLDR:

El atributo especial le __slots__permite establecer explícitamente qué atributos de instancia espera que tengan sus instancias de objeto, con los resultados esperados:

  1. Acceso más rápido a los atributos.
  2. Ahorro de espacio en memoria.

El ahorro de espacio es de

  1. Almacenar referencias de valor en ranuras en lugar de __dict__.
  2. Negación __dict__y __weakref__creación si las clases primarias las niegan y usted declara __slots__.

Advertencias rápidas

Pequeña advertencia, solo debe declarar un espacio en particular una vez en un árbol de herencia. Por ejemplo:

class Base:
    __slots__ = 'foo', 'bar'

class Right(Base):
    __slots__ = 'baz', 

class Wrong(Base):
    __slots__ = 'foo', 'bar', 'baz'        # redundant foo and bar

Python no se opone cuando te equivocas (probablemente debería), de lo contrario los problemas podrían no manifestarse, pero tus objetos ocuparán más espacio del que deberían. Python 3.8:

>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(56, 72)

Esto se debe a que el descriptor de ranura de la Base tiene una ranura separada de la de Wrong's. Esto generalmente no debería aparecer, pero podría:

>>> w = Wrong()
>>> w.foo = 'foo'
>>> Base.foo.__get__(w)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: foo
>>> Wrong.foo.__get__(w)
'foo'

La mayor advertencia es para la herencia múltiple: no se pueden combinar varias "clases principales con ranuras no vacías".

Para acomodar esta restricción, siga las mejores prácticas: Factorice la abstracción de todos menos uno o todos los padres de los cuales su clase concreta respectivamente y su nueva clase concreta heredarán colectivamente, dando a la abstracción espacios vacíos (al igual que las clases base abstractas en el biblioteca estándar).

Consulte la sección sobre herencia múltiple a continuación para ver un ejemplo.

Requisitos:

  • Para que los atributos nombrados __slots__se almacenen realmente en ranuras en lugar de a __dict__, una clase debe heredar de object.

  • Para evitar la creación de a __dict__, debe heredar de objecty todas las clases en la herencia deben declarar __slots__y ninguna de ellas puede tener una '__dict__'entrada.

Hay muchos detalles si desea seguir leyendo.

Por qué usar __slots__: Acceso más rápido a los atributos.

El creador de Python, Guido van Rossum, afirma que en realidad creó __slots__para un acceso más rápido a los atributos.

Es trivial demostrar un acceso más rápido apreciablemente significativo:

import timeit

class Foo(object): __slots__ = 'foo',

class Bar(object): pass

slotted = Foo()
not_slotted = Bar()

def get_set_delete_fn(obj):
    def get_set_delete():
        obj.foo = 'foo'
        obj.foo
        del obj.foo
    return get_set_delete

y

>>> min(timeit.repeat(get_set_delete_fn(slotted)))
0.2846834529991611
>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))
0.3664822799983085

El acceso ranurado es casi un 30% más rápido en Python 3.5 en Ubuntu.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

En Python 2 en Windows lo he medido aproximadamente un 15% más rápido.

Por que usar __slots__ : Ahorro de memoria

Otro propósito de __slots__ es reducir el espacio en memoria que ocupa cada instancia de objeto.

Mi propia contribución a la documentación establece claramente las razones detrás de esto :

El espacio ahorrado sobre el uso __dict__puede ser significativo.

SQLAlchemy atribuye muchos ahorros de memoria __slots__.

Para verificar esto, usando la distribución Anaconda de Python 2.7 en Ubuntu Linux, con guppy.hpy(también conocido como pesado) y sys.getsizeof, el tamaño de una instancia de clase sin __slots__declarar, y nada más, es de 64 bytes. Eso no incluye el __dict__. Gracias Python por la evaluación perezosa nuevamente, __dict__aparentemente no se activa hasta que se hace referencia a ella, pero las clases sin datos generalmente son inútiles. Cuando se llama a la existencia, el __dict__atributo tiene un mínimo de 280 bytes adicionalmente.

Por el contrario, una instancia de clase con __slots__declarada ser() (sin datos) tiene solo 16 bytes y 56 bytes totales con un elemento en las ranuras, 64 con dos.

Para Python de 64 bits, ilustra el consumo de memoria en bytes en Python 2.7 y 3.6, para __slots__y __dict__(sin ranuras definidas) para cada punto donde el dict crece en 3.6 (excepto los atributos 0, 1 y 2):

       Python 2.7             Python 3.6
attrs  __slots__  __dict__*   __slots__  __dict__* | *(no slots defined)
none   16         56 + 272   16         56 + 112 | if __dict__ referenced
one    48         56 + 272    48         56 + 112
two    56         56 + 272    56         56 + 112
six    88         56 + 1040   88         56 + 152
11     128        56 + 1040   128        56 + 240
22     216        56 + 3344   216        56 + 408     
43     384        56 + 3344   384        56 + 752

Entonces, a pesar de los dictados más pequeños en Python 3, vemos cuán bien escalamos las __slots__instancias para ahorrarnos memoria, y esa es una razón importante por la que querría usar __slots__.

Solo para completar mis notas, tenga en cuenta que hay un costo único por espacio en el espacio de nombres de la clase de 64 bytes en Python 2 y 72 bytes en Python 3, porque los espacios usan descriptores de datos como propiedades, llamados "miembros".

>>> Foo.foo
<member 'foo' of 'Foo' objects>
>>> type(Foo.foo)
<class 'member_descriptor'>
>>> getsizeof(Foo.foo)
72

Demostración de __slots__:

Para negar la creación de a __dict__, debe subclasificar object:

class Base(object): 
    __slots__ = ()

ahora:

>>> b = Base()
>>> b.a = 'a'
Traceback (most recent call last):
  File "<pyshell#38>", line 1, in <module>
    b.a = 'a'
AttributeError: 'Base' object has no attribute 'a'

O subclase otra clase que define __slots__

class Child(Base):
    __slots__ = ('a',)

y ahora:

c = Child()
c.a = 'a'

pero:

>>> c.b = 'b'
Traceback (most recent call last):
  File "<pyshell#42>", line 1, in <module>
    c.b = 'b'
AttributeError: 'Child' object has no attribute 'b'

Para permitir la __dict__creación mientras se subclasifican los objetos ranurados, simplemente agregue '__dict__'a __slots__(tenga en cuenta que las ranuras están ordenadas y no debe repetir ranuras que ya están en las clases principales):

class SlottedWithDict(Child): 
    __slots__ = ('__dict__', 'b')

swd = SlottedWithDict()
swd.a = 'a'
swd.b = 'b'
swd.c = 'c'

y

>>> swd.__dict__
{'c': 'c'}

O ni siquiera necesita declarar __slots__en su subclase, y seguirá utilizando espacios de los padres, pero no restringirá la creación de un __dict__:

class NoSlots(Child): pass
ns = NoSlots()
ns.a = 'a'
ns.b = 'b'

Y:

>>> ns.__dict__
{'b': 'b'}

Sin embargo, __slots__puede causar problemas para la herencia múltiple:

class BaseA(object): 
    __slots__ = ('a',)

class BaseB(object): 
    __slots__ = ('b',)

Porque la creación de una clase secundaria a partir de padres con ambos espacios no vacíos falla:

>>> class Child(BaseA, BaseB): __slots__ = ()
Traceback (most recent call last):
  File "<pyshell#68>", line 1, in <module>
    class Child(BaseA, BaseB): __slots__ = ()
TypeError: Error when calling the metaclass bases
    multiple bases have instance lay-out conflict

Si se encuentra con este problema, puede eliminarlo __slots__de los padres, o si tiene el control de los padres, darles espacios vacíos o refactorizar las abstracciones:

from abc import ABC

class AbstractA(ABC):
    __slots__ = ()

class BaseA(AbstractA): 
    __slots__ = ('a',)

class AbstractB(ABC):
    __slots__ = ()

class BaseB(AbstractB): 
    __slots__ = ('b',)

class Child(AbstractA, AbstractB): 
    __slots__ = ('a', 'b')

c = Child() # no problem!

Agregar '__dict__'a __slots__para obtener una asignación dinámica:

class Foo(object):
    __slots__ = 'bar', 'baz', '__dict__'

y ahora:

>>> foo = Foo()
>>> foo.boink = 'boink'

Entonces, '__dict__'en las máquinas tragamonedas perdemos algunos de los beneficios de tamaño con la ventaja de tener una asignación dinámica y aún tener ranuras para los nombres que esperamos.

Cuando hereda de un objeto que no está ranurado, obtiene el mismo tipo de semántica cuando lo usa __slots__: nombres que __slots__apuntan a valores ranurados, mientras que cualquier otro valor se coloca en la instancia __dict__.

Evitar __slots__porque desea poder agregar atributos sobre la marcha en realidad no es una buena razón, solo agregue "__dict__"a su __slots__si es necesario.

Del mismo modo se puede añadir __weakref__a __slots__explícitamente si necesita esa característica.

Establezca la tupla vacía al subclasificar una tupla nombrada:

El nombre de la tupla incorporada crea instancias inmutables que son muy livianas (esencialmente, el tamaño de las tuplas) pero para obtener los beneficios, debe hacerlo usted mismo si las subclasifica:

from collections import namedtuple
class MyNT(namedtuple('MyNT', 'bar baz')):
    """MyNT is an immutable and lightweight object"""
    __slots__ = ()

uso:

>>> nt = MyNT('bar', 'baz')
>>> nt.bar
'bar'
>>> nt.baz
'baz'

Y tratar de asignar un atributo inesperado genera un AttributeErrorporque hemos impedido la creación de __dict__:

>>> nt.quux = 'quux'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyNT' object has no attribute 'quux'

Usted puede permitir __dict__la creación dejando fuera __slots__ = (), pero no se puede utilizar no vacía__slots__ con subtipos de tupla.

La advertencia más grande: herencia múltiple

Incluso cuando las ranuras no vacías son iguales para varios padres, no se pueden usar juntas:

class Foo(object): 
    __slots__ = 'foo', 'bar'
class Bar(object):
    __slots__ = 'foo', 'bar' # alas, would work if empty, i.e. ()

>>> class Baz(Foo, Bar): pass
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
    multiple bases have instance lay-out conflict

El uso de un vacío __slots__en el padre parece proporcionar la mayor flexibilidad, permitiendo que el niño elija prevenir o permitir (agregando '__dict__'para obtener una asignación dinámica, consulte la sección anterior) la creación de__dict__ :

class Foo(object): __slots__ = ()
class Bar(object): __slots__ = ()
class Baz(Foo, Bar): __slots__ = ('foo', 'bar')
b = Baz()
b.foo, b.bar = 'foo', 'bar'

No tiene que tener ranuras, por lo que si las agrega y las elimina más tarde, no debería causar ningún problema.

Salir de apuros aquí : si estás componiendo mixins o usando clases base abstractas , que no están destinadas a ser instanciadas, un vacío __slots__en esos padres parece ser la mejor manera de ir en términos de flexibilidad para los subclases.

Para demostrar, primero, creemos una clase con el código que nos gustaría usar bajo herencia múltiple

class AbstractBase:
    __slots__ = ()
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __repr__(self):
        return f'{type(self).__name__}({repr(self.a)}, {repr(self.b)})'

Podríamos usar lo anterior directamente heredando y declarando las ranuras esperadas:

class Foo(AbstractBase):
    __slots__ = 'a', 'b'

Pero eso no nos importa, es una herencia simple trivial, necesitamos otra clase de la que también podamos heredar, quizás con un atributo ruidoso:

class AbstractBaseC:
    __slots__ = ()
    @property
    def c(self):
        print('getting c!')
        return self._c
    @c.setter
    def c(self, arg):
        print('setting c!')
        self._c = arg

Ahora, si ambas bases tenían ranuras no vacías, no podríamos hacer lo siguiente. (De hecho, si quisiéramos, podríamos haber asignado AbstractBaseranuras no vacías a y b, y haberlas dejado fuera de la siguiente declaración; dejarlas dentro estaría mal):

class Concretion(AbstractBase, AbstractBaseC):
    __slots__ = 'a b _c'.split()

Y ahora tenemos funcionalidad de ambos a través de herencia múltiple, y aún podemos negar __dict__e __weakref__instanciar:

>>> c = Concretion('a', 'b')
>>> c.c = c
setting c!
>>> c.c
getting c!
Concretion('a', 'b')
>>> c.d = 'd'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Concretion' object has no attribute 'd'

Otros casos para evitar tragamonedas:

  • Evítalos cuando quieras realizar __class__ tarea con otra clase que no los tenga (y no puede agregarlos) a menos que los diseños de las ranuras sean idénticos. (Estoy muy interesado en saber quién está haciendo esto y por qué).
  • Evítelos si desea subclasificar construcciones de longitud variable como long, tuple o str, y desea agregarles atributos.
  • Evítelos si insiste en proporcionar valores predeterminados a través de atributos de clase para variables de instancia.

Es posible que pueda descifrar más advertencias del resto de la __slots__ documentación ( los documentos de desarrollo 3.7 son los más actuales) , a los que he realizado importantes contribuciones recientes.

Críticas de otras respuestas

Las principales respuestas actuales citan información obsoleta y son bastante onduladas y pierden la marca de algunas maneras importantes.

No "solo use __slots__ al crear instancias de muchos objetos"

Yo cito:

"Te gustaría usar __slots__si vas a crear una instancia de muchos (cientos, miles) de objetos de la misma clase".

Las clases base abstractas, por ejemplo, del collectionsmódulo, no se instancian, pero __slots__se declaran para ellas.

¿Por qué?

Si un usuario desea negar __dict__o __weakref__crear, esas cosas no deben estar disponibles en las clases principales.

__slots__ contribuye a la reutilización al crear interfaces o mixins.

Es cierto que muchos usuarios de Python no escriben para la reutilización, pero cuando lo haces, tener la opción de negar el uso innecesario de espacio es valioso.

__slots__ no rompe el decapado

Al encurtir un objeto ranurado, es posible que se queje con un error TypeError:

>>> pickle.loads(pickle.dumps(f))
TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled

Esto es realmente incorrecto. Este mensaje proviene del protocolo más antiguo, que es el predeterminado. Puede seleccionar el último protocolo con el -1argumento. En Python 2.7 esto sería 2(que se introdujo en 2.3), y en 3.6 lo es 4.

>>> pickle.loads(pickle.dumps(f, -1))
<__main__.Foo object at 0x1129C770>

en Python 2.7:

>>> pickle.loads(pickle.dumps(f, 2))
<__main__.Foo object at 0x1129C770>

en Python 3.6

>>> pickle.loads(pickle.dumps(f, 4))
<__main__.Foo object at 0x1129C770>

Así que tendría esto en mente, ya que es un problema resuelto.

Crítica de la respuesta aceptada (hasta el 2 de octubre de 2016)

El primer párrafo es mitad breve explicación, mitad predictiva. Aquí está la única parte que realmente responde la pregunta

El uso adecuado de __slots__es para ahorrar espacio en los objetos. En lugar de tener un dict dinámico que permite agregar atributos a los objetos en cualquier momento, existe una estructura estática que no permite adiciones después de la creación. Esto ahorra la sobrecarga de un dict por cada objeto que usa ranuras

La segunda mitad es una ilusión, y fuera de lugar:

Si bien esto a veces es una optimización útil, sería completamente innecesario si el intérprete de Python fuera lo suficientemente dinámico como para que solo requiera el dict cuando realmente haya adiciones al objeto.

Python en realidad hace algo similar a esto, solo crea __dict__cuando se accede a él, pero crear muchos objetos sin datos es bastante ridículo.

El segundo párrafo simplifica demasiado y pierde razones reales para evitar __slots__. Lo siguiente no es una razón real para evitar espacios (por razones reales , vea el resto de mi respuesta anterior):

Cambian el comportamiento de los objetos que tienen ranuras de una manera que puede ser abusada por los fanáticos del control y los weenies de escritura estática.

Luego continúa discutiendo otras formas de lograr ese objetivo perverso con Python, sin discutir nada que ver con __slots__ .

El tercer párrafo es más ilusiones. Juntos, es en su mayoría contenido fuera de la marca que el respondedor ni siquiera escribió y contribuye a las municiones para los críticos del sitio.

Evidencia de uso de memoria

Cree algunos objetos normales y objetos ranurados:

>>> class Foo(object): pass
>>> class Bar(object): __slots__ = ()

Instanciar un millón de ellos:

>>> foos = [Foo() for f in xrange(1000000)]
>>> bars = [Bar() for b in xrange(1000000)]

Inspeccionar con guppy.hpy().heap():

>>> guppy.hpy().heap()
Partition of a set of 2028259 objects. Total size = 99763360 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0 1000000  49 64000000  64  64000000  64 __main__.Foo
     1     169   0 16281480  16  80281480  80 list
     2 1000000  49 16000000  16  96281480  97 __main__.Bar
     3   12284   1   987472   1  97268952  97 str
...

Acceda a los objetos regulares y sus __dict__e inspeccione nuevamente:

>>> for f in foos:
...     f.__dict__
>>> guppy.hpy().heap()
Partition of a set of 3028258 objects. Total size = 379763480 bytes.
 Index  Count   %      Size    % Cumulative  % Kind (class / dict of class)
     0 1000000  33 280000000  74 280000000  74 dict of __main__.Foo
     1 1000000  33  64000000  17 344000000  91 __main__.Foo
     2     169   0  16281480   4 360281480  95 list
     3 1000000  33  16000000   4 376281480  99 __main__.Bar
     4   12284   0    987472   0 377268952  99 str
...

Esto es consistente con la historia de Python, desde Unificar tipos y clases en Python 2.2

Si subclasifica un tipo incorporado, se agrega automáticamente espacio adicional a las instancias para acomodar __dict__y __weakrefs__. (Sin __dict__embargo, no se inicializa hasta que lo use, por lo que no debe preocuparse por el espacio ocupado por un diccionario vacío para cada instancia que cree). Si no necesita este espacio adicional, puede agregar la frase " __slots__ = []" a Tu clase.

Aaron Hall
fuente
14
wow, una respuesta infernal - ¡gracias! Sin embargo, no entendí el class Child(BaseA, BaseB): __slots__ = ('a', 'b')ejemplo con los empy-slot-parents. ¿Por qué aquí se dictproxycrea un en lugar de crear un AttributeErrorpara c?
Skandix
@Skandix, gracias por llamar mi atención sobre el error tipográfico, resultó que c no era una instanciación, probablemente olvidé que estaba editando esa parte cuando lo guardé en el historial de publicaciones. Probablemente se habría detectado antes si hubiera hecho lo correcto y hubiera hecho que el código fuera más fácil de copiar ... ¡Gracias de nuevo!
Aaron Hall
38
Esta respuesta debería ser parte de la documentación oficial de Python sobre __slots__. ¡Seriamente! ¡Gracias!
NightElfik
13
@NightElfik lo creas o no, contribuí a los documentos de Python hace __slots__aproximadamente un año: github.com/python/cpython/pull/1819/files
Aaron Hall
Respuesta fantásticamente detallada. Tengo una pregunta: ¿debería uno usar las ranuras por defecto a menos que el uso llegue a una de las advertencias, o las ranuras son algo a considerar si sabe que va a luchar por la velocidad / memoria? Para decirlo de otra manera, ¿debería alentar a un novato a aprender sobre ellos y usarlos desde el principio?
freethebees
265

Citando a Jacob Hallen :

El uso adecuado de __slots__es para ahorrar espacio en los objetos. En lugar de tener un dict dinámico que permite agregar atributos a los objetos en cualquier momento, hay una estructura estática que no permite adiciones después de la creación. [Este uso de __slots__elimina la sobrecarga de un dict por cada objeto.] Si bien esto a veces es una optimización útil, sería completamente innecesario si el intérprete de Python fuera lo suficientemente dinámico como para que solo requiriera el dict cuando realmente hubiera adiciones al objeto.

Desafortunadamente hay un efecto secundario en las tragamonedas. Cambian el comportamiento de los objetos que tienen ranuras de una manera que puede ser abusada por los fanáticos del control y los weenies de escritura estática. Esto es malo, porque los fanáticos del control deberían estar abusando de las metaclases y las weenies de tipeo estático deberían estar abusando de los decoradores, ya que en Python, solo debería haber una forma obvia de hacer algo.

Hacer que CPython sea lo suficientemente inteligente como para manejar el ahorro de espacio sin __slots__es una tarea importante, por lo que probablemente no esté en la lista de cambios para P3k (todavía).

Jeff Bauer
fuente
86
Me gustaría ver alguna explicación sobre el "tipeo estático" / punto decorador, sin peyorativas. No es útil citar a terceros ausentes. __slots__no aborda los mismos problemas que la escritura estática. Por ejemplo, en C ++, no es que la declaración de una variable miembro esté restringida, es la asignación de un tipo no intencionado (y el compilador forzado) a esa variable. No estoy tolerando el uso de __slots__, solo estoy interesado en la conversación. ¡Gracias!
hiwaylon
126

Debería usarlo __slots__si va a crear una instancia de muchos (cientos, miles) de objetos de la misma clase. __slots__solo existe como una herramienta de optimización de memoria.

Se desaconseja su uso __slots__para restringir la creación de atributos.

Decapado de objetos con __slots__no funcionará con el protocolo de decapado predeterminado (más antiguo); Es necesario especificar una versión posterior.

Algunas otras características de introspección de python también pueden verse afectadas negativamente.

Ryan
fuente
10
Demuestro encurtir un objeto ranurado en mi respuesta y también abordo la primera parte de su respuesta.
Aaron Hall
2
Veo su punto, pero las tragamonedas también ofrecen un acceso más rápido a los atributos (como han dicho otros). En ese caso, no necesita "crear una instancia de muchos (cientos, miles) de objetos de la misma clase" para obtener rendimiento. En cambio, lo que necesita son muchos accesos al mismo atributo (ranurado) de la misma instancia. (Por favor
corrígeme
61

Cada objeto de Python tiene un __dict__atributo que es un diccionario que contiene todos los demás atributos. por ejemplo, cuando escribe self.attrpython realmente está haciendo self.__dict__['attr']. Como puede imaginar, el uso de un diccionario para almacenar atributos requiere espacio y tiempo extra para acceder a él.

Sin embargo, cuando lo use __slots__, cualquier objeto creado para esa clase no tendrá un __dict__atributo. En cambio, todo el acceso a los atributos se realiza directamente a través de punteros.

Entonces, si desea una estructura de estilo C en lugar de una clase completa, puede usarla __slots__para compactar el tamaño de los objetos y reducir el tiempo de acceso a los atributos. Un buen ejemplo es una clase Point que contiene los atributos x & y. Si va a tener muchos puntos, puede intentar usarlos __slots__para conservar algo de memoria.

Suraj
fuente
10
No, una instancia de una clase con __slots__definido no es como una estructura de estilo C. Existe un nombre de atributo de asignación de diccionario a nivel de clase para índices, de lo contrario, no sería posible lo siguiente: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Realmente creo que esta respuesta debería aclararse (puedo hacerlo si lo desea). Además, no estoy seguro de que instance.__hidden_attributes[instance.__class__[attrname]]sea ​​más rápido que instance.__dict__[attrname].
tzot
22

Además de las otras respuestas, aquí hay un ejemplo de uso __slots__:

>>> class Test(object):   #Must be new-style class!
...  __slots__ = ['x', 'y']
... 
>>> pt = Test()
>>> dir(pt)
['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__', 
 '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', 
 '__repr__', '__setattr__', '__slots__', '__str__', 'x', 'y']
>>> pt.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: x
>>> pt.x = 1
>>> pt.x
1
>>> pt.z = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute 'z'
>>> pt.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute '__dict__'
>>> pt.__slots__
['x', 'y']

Entonces, para implementar __slots__, solo se necesita una línea adicional (y hacer que su clase sea una clase de nuevo estilo si aún no lo está). De esta forma, puede reducir la huella de memoria de esas clases 5 veces , a expensas de tener que escribir un código personalizado de pickle, cuando sea necesario.

Evgeni Sergeev
fuente
11

Las ranuras son muy útiles para las llamadas a la biblioteca para eliminar el "envío de método con nombre" al realizar llamadas a funciones. Esto se menciona en la documentación SWIG . Para las bibliotecas de alto rendimiento que desean reducir la sobrecarga de funciones para las funciones comúnmente llamadas que usan ranuras es mucho más rápido.

Ahora esto puede no estar directamente relacionado con la pregunta de los OP. Está más relacionado con la construcción de extensiones que con el uso de la sintaxis de ranuras en un objeto. Pero ayuda a completar la imagen del uso de las máquinas tragamonedas y algunos de los razonamientos detrás de ellas.

Demolishun
fuente
7

Un atributo de una instancia de clase tiene 3 propiedades: la instancia, el nombre del atributo y el valor del atributo.

En el acceso regular a los atributos , la instancia actúa como un diccionario y el nombre del atributo actúa como la clave en ese diccionario buscando valor.

instancia (atributo) -> valor

En el acceso __slots__ , el nombre del atributo actúa como el diccionario y la instancia actúa como la clave en el valor de búsqueda del diccionario.

atributo (instancia) -> valor

En el patrón de peso mosca , el nombre del atributo actúa como el diccionario y el valor actúa como la clave en ese diccionario buscando la instancia.

atributo (valor) -> instancia

Dmitry Rubanovich
fuente
Esta es una buena parte, y no encajará bien en un comentario sobre una de las respuestas que también sugieren pesos mosca, pero no es una respuesta completa a la pregunta en sí. En particular (solo en el contexto de la pregunta): ¿por qué Flyweight y "cuáles son los casos que uno debería evitar ..." __slots__?
Merlyn Morgan-Graham
@Merlyn Morgan-Graham, sirve como una pista para elegir: acceso regular, __slots__ o flyweight.
Dmitry Rubanovich
3

Un ejemplo muy simple de __slot__atributo.

Problema: sin __slots__

Si no tengo __slot__atributo en mi clase, puedo agregar nuevos atributos a mis objetos.

class Test:
    pass

obj1=Test()
obj2=Test()

print(obj1.__dict__)  #--> {}
obj1.x=12
print(obj1.__dict__)  # --> {'x': 12}
obj1.y=20
print(obj1.__dict__)  # --> {'x': 12, 'y': 20}

obj2.x=99
print(obj2.__dict__)  # --> {'x': 99}

Si observa el ejemplo anterior, puede ver que obj1 y obj2 tienen sus propios atributos x e y y python también ha creado un dictatributo para cada objeto ( obj1 y obj2 ).

¿Y si mi clase Test tiene miles de tales objetos? Crear un atributo adicional dictpara cada objeto causará mucha sobrecarga (memoria, potencia informática, etc.) en mi código.

Solución: con __slots__

Ahora, en el siguiente ejemplo, mi clase Test contiene un __slots__atributo. Ahora no puedo agregar nuevos atributos a mis objetos (excepto el atributo x) y Python ya no crea un dictatributo. Esto elimina la sobrecarga de cada objeto, que puede ser importante si tiene muchos objetos.

class Test:
    __slots__=("x")

obj1=Test()
obj2=Test()
obj1.x=12
print(obj1.x)  # --> 12
obj2.x=99
print(obj2.x)  # --> 99

obj1.y=28
print(obj1.y)  # --> AttributeError: 'Test' object has no attribute 'y'
N Randhawa
fuente
2

Otro uso un tanto oscuro __slots__es agregar atributos a un proxy de objeto desde el paquete ProxyTypes, anteriormente parte del proyecto PEAK. Su ObjectWrapperle permite al proxy de otro objeto, pero interceptan todas las interacciones con los objetos proxy. No se usa con mucha frecuencia (y no es compatible con Python 3), pero lo hemos usado para implementar un contenedor de bloqueo seguro para subprocesos alrededor de una implementación asincrónica basada en tornado que rebota todo el acceso al objeto proxy a través del ioloop, usando subprocesos seguros concurrent.Futureobjetos para sincronizar y devolver resultados.

Por defecto, cualquier acceso de atributo al objeto proxy le dará el resultado del objeto proxy. Si necesita agregar un atributo en el objeto proxy, __slots__puede usarse.

from peak.util.proxies import ObjectWrapper

class Original(object):
    def __init__(self):
        self.name = 'The Original'

class ProxyOriginal(ObjectWrapper):

    __slots__ = ['proxy_name']

    def __init__(self, subject, proxy_name):
        # proxy_info attributed added directly to the
        # Original instance, not the ProxyOriginal instance
        self.proxy_info = 'You are proxied by {}'.format(proxy_name)

        # proxy_name added to ProxyOriginal instance, since it is
        # defined in __slots__
        self.proxy_name = proxy_name

        super(ProxyOriginal, self).__init__(subject)

if __name__ == "__main__":
    original = Original()
    proxy = ProxyOriginal(original, 'Proxy Overlord')

    # Both statements print "The Original"
    print "original.name: ", original.name
    print "proxy.name: ", proxy.name

    # Both statements below print 
    # "You are proxied by Proxy Overlord", since the ProxyOriginal
    # __init__ sets it to the original object 
    print "original.proxy_info: ", original.proxy_info
    print "proxy.proxy_info: ", proxy.proxy_info

    # prints "Proxy Overlord"
    print "proxy.proxy_name: ", proxy.proxy_name
    # Raises AttributeError since proxy_name is only set on 
    # the proxy object
    print "original.proxy_name: ", proxy.proxy_name
NeilenMarais
fuente
1

Usted no tiene - esencialmente - uso para __slots__.

Para el momento en que cree que podría necesitar __slots__, en realidad desea usar patrones de diseño Ligero o Flyweight . Estos son casos en los que ya no desea utilizar únicamente objetos de Python. En cambio, desea un contenedor similar a un objeto Python alrededor de una matriz, estructura o matriz numpy.

class Flyweight(object):

    def get(self, theData, index):
        return theData[index]

    def set(self, theData, index, value):
        theData[index]= value

El contenedor de clase no tiene atributos, solo proporciona métodos que actúan sobre los datos subyacentes. Los métodos se pueden reducir a métodos de clase. De hecho, podría reducirse a solo funciones que operan en la matriz subyacente de datos.

S.Lott
fuente
17
¿Qué tiene que ver Flyweight __slots__?
oefe
3
@oefe: Ciertamente no entiendo tu pregunta. Puedo citar mi respuesta, si ayuda "cuando creas que podrías necesitar máquinas tragamonedas , realmente quieres usar ... Patrón de diseño Flyweight". Eso es lo que Flyweight tiene que ver con las tragamonedas . ¿Tienes una pregunta más específica?
S.Lott
21
@oefe: Flyweight y __slots__son técnicas de optimización para ahorrar memoria. __slots__muestra beneficios cuando tiene muchos objetos, así como el patrón de diseño Flyweight. Ambos resuelven el mismo problema.
jfs el
77
¿Existe una comparación disponible entre usar slots y usar Flyweight con respecto al consumo de memoria y la velocidad?
kontulai
8
Aunque Flyweight es ciertamente útil en algunos contextos, lo creas o no, la respuesta a "cómo puedo reducir el uso de memoria en Python cuando creo un billón de objetos" no siempre es "no uses Python para tus billones de objetos". A veces, __slots__realmente es la respuesta, y como Evgeni señala, se puede agregar como una simple idea de último momento (por ejemplo, puede enfocarse primero en la corrección y luego agregar rendimiento).
Patrick Maupin
0

La pregunta original era sobre casos de uso general, no solo sobre memoria. Por lo tanto, debe mencionarse aquí que también obtiene un mejor rendimiento al crear instancias de grandes cantidades de objetos, lo que es interesante, por ejemplo, al analizar documentos grandes en objetos o desde una base de datos.

Aquí hay una comparación de la creación de árboles de objetos con un millón de entradas, usando ranuras y sin ranuras. Como referencia también el rendimiento al usar dictados simples para los árboles (Py2.7.10 en OSX):

********** RUN 1 **********
1.96036410332 <class 'css_tree_select.element.Element'>
3.02922606468 <class 'css_tree_select.element.ElementNoSlots'>
2.90828204155 dict
********** RUN 2 **********
1.77050495148 <class 'css_tree_select.element.Element'>
3.10655999184 <class 'css_tree_select.element.ElementNoSlots'>
2.84120798111 dict
********** RUN 3 **********
1.84069895744 <class 'css_tree_select.element.Element'>
3.21540498734 <class 'css_tree_select.element.ElementNoSlots'>
2.59615707397 dict
********** RUN 4 **********
1.75041103363 <class 'css_tree_select.element.Element'>
3.17366290092 <class 'css_tree_select.element.ElementNoSlots'>
2.70941114426 dict

Clases de prueba (ident, aparte de las ranuras):

class Element(object):
    __slots__ = ['_typ', 'id', 'parent', 'childs']
    def __init__(self, typ, id, parent=None):
        self._typ = typ
        self.id = id
        self.childs = []
        if parent:
            self.parent = parent
            parent.childs.append(self)

class ElementNoSlots(object): (same, w/o slots)

código de prueba, modo detallado:

na, nb, nc = 100, 100, 100
for i in (1, 2, 3, 4):
    print '*' * 10, 'RUN', i, '*' * 10
    # tree with slot and no slot:
    for cls in Element, ElementNoSlots:
        t1 = time.time()
        root = cls('root', 'root')
        for i in xrange(na):
            ela = cls(typ='a', id=i, parent=root)
            for j in xrange(nb):
                elb = cls(typ='b', id=(i, j), parent=ela)
                for k in xrange(nc):
                    elc = cls(typ='c', id=(i, j, k), parent=elb)
        to =  time.time() - t1
        print to, cls
        del root

    # ref: tree with dicts only:
    t1 = time.time()
    droot = {'childs': []}
    for i in xrange(na):
        ela =  {'typ': 'a', id: i, 'childs': []}
        droot['childs'].append(ela)
        for j in xrange(nb):
            elb =  {'typ': 'b', id: (i, j), 'childs': []}
            ela['childs'].append(elb)
            for k in xrange(nc):
                elc =  {'typ': 'c', id: (i, j, k), 'childs': []}
                elb['childs'].append(elc)
    td = time.time() - t1
    print td, 'dict'
    del droot
Píldora roja
fuente