Uso de __slots__?

Respostas:

1019

No Python, qual é o objetivo __slots__e quais são os casos em que se deve evitar isso?

TLDR:

O atributo especial __slots__permite que você indique explicitamente quais atributos de instância você espera que suas instâncias de objeto tenham, com os resultados esperados:

  1. acesso mais rápido aos atributos.
  2. economia de espaço na memória.

A economia de espaço é de

  1. Armazenando referências de valor nos slots em vez de __dict__.
  2. Negação __dict__e __weakref__criação se as classes pai as negarem e você declara __slots__.

Advertências rápidas

Advertência pequena, você deve declarar apenas um slot específico uma vez em uma árvore de herança. Por exemplo:

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

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

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

Python não se opõe quando você errar (provavelmente deveria), os problemas podem não se manifestar de outra forma, mas seus objetos ocuparão mais espaço do que deveriam. Python 3.8:

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

Isso ocorre porque o descritor de slot da Base possui um slot separado do errado. Normalmente, isso não deveria aparecer, mas poderia:

>>> 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'

A maior ressalva é a herança múltipla - várias "classes pai com slots não vazios" não podem ser combinadas.

Para acomodar essa restrição, siga as práticas recomendadas: Fatore a abstração de todos, exceto um ou todos os pais, que sua classe concreta, respectivamente, e sua nova classe concreta herdarão coletivamente - fornecendo espaços vazios para a abstração (como as classes básicas abstratas no biblioteca padrão).

Veja a seção sobre herança múltipla abaixo para um exemplo.

Requisitos:

  • Para que os atributos nomeados __slots__sejam realmente armazenados nos slots em vez de a __dict__, uma classe deve herdar de object.

  • Para impedir a criação de a __dict__, você deve herdar de objecte todas as classes na herança devem declarar __slots__e nenhuma delas pode ter uma '__dict__'entrada.

Existem muitos detalhes se você deseja continuar lendo.

Por que usar __slots__: Acesso mais rápido aos atributos.

O criador do Python, Guido van Rossum, afirma que ele realmente criou __slots__para um acesso mais rápido aos atributos.

É trivial demonstrar um acesso mais rápido e mensurável:

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

e

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

O acesso com fenda é quase 30% mais rápido no Python 3.5 no Ubuntu.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

No Python 2 no Windows, eu o medi cerca de 15% mais rápido.

Por que usar __slots__: Economia de Memória

Outro objetivo __slots__é reduzir o espaço na memória que cada instância do objeto ocupa.

Minha própria contribuição para a documentação afirma claramente as razões por trás disso :

O espaço economizado durante o uso __dict__pode ser significativo.

SQLAlchemy atribui muita economia de memória a __slots__.

Para verificar isso, o uso da distribuição Anaconda do Python 2.7 no Ubuntu Linux, com guppy.hpy(também conhecido como heapy) e sys.getsizeof, o tamanho de uma instância de classe sem __slots__declarada e nada mais, é de 64 bytes. Isso não inclui o __dict__. Obrigado Python pela avaliação lenta novamente, o que __dict__aparentemente não é chamado de existência até que seja referenciado, mas as classes sem dados geralmente são inúteis. Quando chamado, o __dict__atributo tem no mínimo 280 bytes adicionalmente.

Por outro lado, uma instância de classe __slots__declarada como sendo ()(sem dados) tem apenas 16 bytes e 56 bytes totais com um item nos slots, 64 com dois.

Para Python de 64 bits, ilustro o consumo de memória em bytes no Python 2.7 e 3.6, para __slots__e __dict__(sem slots definidos) para cada ponto em que o ditado cresce em 3.6 (exceto os atributos 0, 1 e 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

Portanto, apesar de dictos menores no Python 3, vemos o quão bem __slots__as instâncias podem ser economizadas para economizar memória, e essa é uma das principais razões pelas quais você deseja usar __slots__.

Apenas para completar minhas anotações, observe que existe um custo único por slot no espaço de nome da classe de 64 bytes no Python 2 e 72 bytes no Python 3, porque os slots usam descritores de dados como propriedades, chamados "membros".

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

Demonstração de __slots__:

Para negar a criação de a __dict__, você deve subclassificar object:

class Base(object): 
    __slots__ = ()

agora:

>>> 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'

Ou subclasse outra classe que define __slots__

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

e agora:

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

mas:

>>> 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 a __dict__criação ao subclassificar objetos com fenda, basta adicionar '__dict__'ao __slots__(observe que os slots estão ordenados e você não deve repetir os slots que já estão nas classes pai):

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

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

e

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

Ou você nem precisa declarar __slots__em sua subclasse e ainda usará slots dos pais, mas não restringirá a criação de um __dict__:

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

E:

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

No entanto, __slots__pode causar problemas para herança múltipla:

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

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

Como a criação de uma classe filho de pais com os dois slots não vazios falha:

>>> 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

Se você se deparar com esse problema, poderá remover __slots__dos pais ou, se tiver o controle dos pais, dar a eles espaços vazios ou refatorar para abstrações:

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!

Adicione '__dict__'a __slots__para obter atribuição dinâmica:

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

e agora:

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

Portanto, '__dict__'nos slots, perdemos alguns dos benefícios de tamanho com a vantagem de ter atribuição dinâmica e ainda ter slots para os nomes que esperamos.

Quando você herda de um objeto que não é com fenda, você obtém o mesmo tipo de semântica quando usa __slots__- nomes que __slots__apontam para valores com fenda, enquanto outros valores são colocados na instância __dict__.

Evitar __slots__porque você deseja adicionar atributos em tempo real não é realmente um bom motivo - basta adicionar "__dict__"ao seu __slots__se for necessário.

Você pode semelhante adicionar __weakref__ao __slots__explicitamente se você precisar desse recurso.

Defina como tupla vazia ao subclassificar um duplo nomeado:

O componente nomeado duplo cria instâncias imutáveis ​​que são muito leves (essencialmente, o tamanho das tuplas), mas para obter os benefícios, você precisa fazer isso sozinho se as subclassificar:

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'

E tentar atribuir um atributo inesperado gera um AttributeErrorporque impedimos a criação de __dict__:

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

Você pode permitir a __dict__criação deixando de fora __slots__ = (), mas não pode usar não-vazio __slots__com subtipos de tupla.

Maior advertência: herança múltipla

Mesmo quando os slots não vazios são iguais para vários pais, eles não podem ser usados ​​juntos:

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

Usar um vazio __slots__no pai parece fornecer a maior flexibilidade, permitindo que a criança opte por impedir ou permitir (adicionando '__dict__'para obter atribuição dinâmica, consulte a seção acima) a criação de um__dict__ :

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

Você não precisa ter slots - portanto, se você adicioná-los e removê-los mais tarde, isso não causará problemas.

Explicando aqui : se você está compondo mixins ou usa classes básicas abstratas , que não devem ser instanciadas, um espaço vazio __slots__nesses pais parece ser o melhor caminho a seguir em termos de flexibilidade para subclassers.

Para demonstrar, primeiro, vamos criar uma classe com o código que gostaríamos de usar sob herança múltipla

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)})'

Poderíamos usar o exposto diretamente herdando e declarando os slots esperados:

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

Mas não nos importamos com isso, isso é herança simples trivial, precisamos de outra classe da qual também possamos herdar, talvez com um atributo barulhento:

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

Agora, se ambas as bases tivessem slots vazios, não poderíamos fazer o abaixo. (De fato, se quiséssemos, poderíamos ter fornecido os AbstractBaseslots não vazios aeb, e os deixado de fora da declaração abaixo - deixá-los dentro seria errado):

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

E agora temos funcionalidade de ambos por herança múltipla e ainda 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'

Outros casos para evitar slots:

  • Evite-os quando desejar realizar __class__tarefas com outra classe que não os possua (e não poderá adicioná-los), a menos que os layouts de slot sejam idênticos. (Estou muito interessado em saber quem está fazendo isso e por quê.)
  • Evite-os se desejar subclassificar os comprimentos de variável internos como long, tuple ou str, e desejar adicionar atributos a eles.
  • Evite-os se você insistir em fornecer valores padrão por meio de atributos de classe para variáveis ​​de instância.

Você pode apresentar outras advertências do restante da __slots__ documentação ( os documentos de desenvolvimento 3.7 são os mais atuais) , para os quais fiz contribuições recentes significativas.

Críticas de outras respostas

As principais respostas atuais citam informações desatualizadas e são bastante ondulantes e perdem a marca em alguns aspectos importantes.

Não "use apenas __slots__ao instanciar muitos objetos"

Eu cito:

"Você gostaria de usar __slots__se for instanciar muitos (centenas, milhares) de objetos da mesma classe."

As classes base abstratas, por exemplo, do collectionsmódulo, não são instanciadas, mas __slots__são declaradas para elas.

Por quê?

Se um usuário desejar negar __dict__ou __weakref__criar, essas coisas não deverão estar disponíveis nas classes pai.

__slots__ contribui para a reutilização ao criar interfaces ou mixins.

É verdade que muitos usuários de Python não estão escrevendo para reutilização, mas quando você está, ter a opção de negar o uso desnecessário de espaço é valioso.

__slots__ não quebra decapagem

Ao escolher um objeto com fenda, você pode achar que ele se engana TypeError:

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

Isso está realmente incorreto. Esta mensagem vem do protocolo mais antigo, que é o padrão. Você pode selecionar o protocolo mais recente com o -1argumento No Python 2.7 isso seria 2(que foi introduzido na 2.3) e na 3.6 é 4.

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

no Python 2.7:

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

no Python 3.6

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

Então, eu manteria isso em mente, pois é um problema resolvido.

Crítica da (até 2 de outubro de 2016) resposta aceita

O primeiro parágrafo é meio breve explicação, meio preditivo. Aqui está a única parte que realmente responde à pergunta

O uso adequado de __slots__é economizar espaço nos objetos. Em vez de ter um ditado dinâmico que permite adicionar atributos aos objetos a qualquer momento, existe uma estrutura estática que não permite adições após a criação. Isso economiza a sobrecarga de um ditado para cada objeto que usa slots

A segunda metade é uma ilusão, e fora da marca:

Embora isso às vezes seja uma otimização útil, seria completamente desnecessário se o intérprete Python fosse dinâmico o suficiente para exigir apenas o ditado quando houvesse realmente acréscimos ao objeto.

O Python realmente faz algo semelhante a isso, apenas criando o __dict__quando é acessado, mas a criação de muitos objetos sem dados é bastante ridícula.

O segundo parágrafo simplifica demais e perde razões reais para evitar __slots__. O abaixo não é um motivo real para evitar slots (por motivos reais , veja o restante da minha resposta acima.):

Eles alteram o comportamento dos objetos que possuem slots de uma maneira que pode ser abusada por malucos de controle e weenies de digitação estática.

Em seguida, ele discute outras maneiras de atingir esse objetivo perverso com o Python, sem discutir nada a respeito __slots__.

O terceiro parágrafo é mais ilusório. Juntos, é na maior parte do conteúdo errado que o respondente nem sequer é autor e contribui com munição para os críticos do site.

Evidência de uso de memória

Crie alguns objetos normais e objetos com fenda:

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

Instancie um milhão deles:

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

Inspecione com 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
...

Acesse os objetos regulares e seus __dict__e inspecione novamente:

>>> 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
...

Isso é consistente com o histórico do Python, dos tipos e classes da Unificação no Python 2.2

Se você subclassificar um tipo interno, um espaço extra será adicionado automaticamente às instâncias para acomodar __dict__e __weakrefs__. (Ele __dict__não é inicializado até você usá-lo, portanto, não se preocupe com o espaço ocupado por um dicionário vazio para cada instância criada.) Se você não precisar desse espaço extra, poderá adicionar a frase " __slots__ = []" a sua classe.

Aaron Hall
fonte
14
uau, um inferno de resposta - obrigado! No entanto, eu não peguei o class Child(BaseA, BaseB): __slots__ = ('a', 'b')exemplo com os pais de slots empy. Por que aqui é dictproxycriado em vez de criar um AttributeErrorpara c?
Skandix
@ Skandix, obrigado por chamar minha atenção para o erro de digitação, pois c não foi uma instanciação; provavelmente esqueci que estava editando essa parte quando a salvei no histórico de postagens. Provavelmente teria sido pego mais cedo se eu tivesse feito a coisa certa e tornado o código mais passível de copiar ... Mais uma vez obrigado!
Aaron Hall
38
Esta resposta deve fazer parte da documentação oficial do Python sobre __slots__. A sério! Obrigado!
NightElfik
13
@NightElfik acredite ou não, eu contribuiu para os docs Python em __slots__cerca de um ano atrás: github.com/python/cpython/pull/1819/files
Aaron Hall
Resposta fantasticamente detalhada. Eu tenho uma pergunta: alguém deveria estar usando slots como padrão , a menos que o uso atinja uma das advertências ou os slots são algo a considerar se você sabe que vai lutar por velocidade / memória? Em outras palavras, você deve incentivar um novato a aprender sobre eles e usá-los desde o início?
freethebees
265

Citando Jacob Hallen :

O uso adequado de __slots__é economizar espaço nos objetos. Em vez de ter um ditado dinâmico que permita adicionar atributos aos objetos a qualquer momento, existe uma estrutura estática que não permite adições após a criação. [Esse uso __slots__elimina a sobrecarga de um ditado para cada objeto.] Embora isso às vezes seja uma otimização útil, seria completamente desnecessário se o intérprete Python fosse dinâmico o suficiente para que só exigisse o ditado quando houvesse realmente acréscimos ao objeto.

Infelizmente, há um efeito colateral nos slots. Eles alteram o comportamento dos objetos que possuem slots de uma maneira que pode ser abusada por malucos de controle e weenies de digitação estática. Isso é ruim, porque os malucos de controle devem estar abusando das metaclasses e os weenies de digitação estáticos devem estar abusando dos decoradores, já que no Python, deve haver apenas uma maneira óbvia de fazer algo.

Tornar o CPython inteligente o suficiente para lidar com a economia de espaço sem __slots__é uma tarefa importante, e é provavelmente por isso que ainda não está na lista de alterações do P3k.

Jeff Bauer
fonte
86
Eu gostaria de ver alguma elaboração sobre o ponto "digitação estática" / decorador, sem pejorativos. Citar terceiros ausentes é inútil. __slots__não aborda os mesmos problemas da digitação estática. Por exemplo, em C ++, não é a declaração de uma variável de membro que está sendo restrita, é a atribuição de um tipo não intencional (e compilador imposto) a essa variável. Não estou tolerando o uso __slots__, apenas interessado na conversa. Obrigado!
hiwaylon
126

Você gostaria de usar __slots__se for instanciar muitos (centenas, milhares) de objetos da mesma classe. __slots__só existe como uma ferramenta de otimização de memória.

É altamente desencorajado usar __slots__para restringir a criação de atributos.

Objetos de decapagem com __slots__não funcionam com o protocolo de decapagem padrão (mais antigo); é necessário especificar uma versão posterior.

Alguns outros recursos de introspecção do python também podem ser afetados adversamente.

Ryan
fonte
10
Eu demonstro decapitar um objeto com fenda na minha resposta e também abordo a primeira parte da sua resposta.
Aaron Hall
2
Entendo o seu ponto, mas os slots também oferecem acesso mais rápido aos atributos (como outros já declararam). Nesse caso, você não precisa "instanciar muitos (centenas, milhares) de objetos da mesma classe" para obter desempenho. Em vez disso, você precisa de muitos acessos ao mesmo atributo (com fenda) da mesma instância. (Por favor, me corrija se eu estiver errado.)
Rotareti
61

Cada objeto python possui um __dict__atributo que é um dicionário que contém todos os outros atributos. por exemplo, quando você digita self.attrpython está realmente fazendo self.__dict__['attr']. Como você pode imaginar, usar um dicionário para armazenar atributos requer algum espaço e tempo extra para acessá-lo.

No entanto, quando você usa __slots__, qualquer objeto criado para essa classe não terá um __dict__atributo. Em vez disso, todo o acesso ao atributo é feito diretamente via ponteiros.

Portanto, se você quiser uma estrutura de estilo C em vez de uma classe completa, poderá usar __slots__para compactar o tamanho dos objetos e reduzir o tempo de acesso aos atributos. Um bom exemplo é uma classe Point que contém os atributos x e y. Se você tiver muitos pontos, tente usar __slots__para economizar memória.

Suraj
fonte
10
Não, uma instância de uma classe com __slots__definida não é como uma estrutura de estilo C. Há um dicionário de nível de classe que mapeia nomes de atributos para índices, caso contrário, o seguinte não seria possível: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Eu realmente acho que essa resposta deve ser esclarecida (posso fazer isso se você quiser). Além disso, não tenho certeza de que instance.__hidden_attributes[instance.__class__[attrname]]seja mais rápido que instance.__dict__[attrname].
tzot
22

Além das outras respostas, aqui está um exemplo 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']

Portanto, para implementar __slots__, é preciso apenas uma linha extra (e tornar sua classe uma nova classe, se ainda não estiver). Dessa forma, você pode reduzir o espaço de memória dessas classes em cinco vezes , às custas de ter que escrever um código de pickle personalizado, se e quando isso for necessário.

Evgeni Sergeev
fonte
11

Os slots são muito úteis para chamadas de biblioteca para eliminar o "envio de método nomeado" ao fazer chamadas de função. Isso é mencionado na documentação do SWIG . Para bibliotecas de alto desempenho que desejam reduzir a sobrecarga de funções para funções comumente chamadas usando slots, é muito mais rápido.

Agora, isso pode não estar diretamente relacionado à questão dos POs. Está mais relacionado à criação de extensões do que ao uso da sintaxe de slots em um objeto. Mas ajuda a completar a imagem para o uso de slots e alguns dos motivos por trás deles.

Demolishun
fonte
7

Um atributo de uma instância de classe possui 3 propriedades: a instância, o nome do atributo e o valor do atributo.

No acesso regular ao atributo , a instância atua como um dicionário e o nome do atributo atua como a chave no dicionário que procura valor.

instância (atributo) -> valor

No acesso __slots__ , o nome do atributo atua como o dicionário e a instância atua como a chave no valor de pesquisa do dicionário.

atributo (instância) -> valor

No padrão flyweight , o nome do atributo atua como o dicionário e o valor atua como a chave nesse dicionário que procura a instância.

atributo (valor) -> instância

Dmitry Rubanovich
fonte
Essa é uma boa parte, e não se encaixará bem em um comentário de uma das respostas que também sugerem pesos de mosca, mas não é uma resposta completa à pergunta em si. Em particular (apenas no contexto da questão): por que Flyweight e "quais são os casos que se deve evitar ..." __slots__?
precisa
@Merlyn Morgan-Graham, serve como uma dica para escolher: acesso regular, __slots__ ou peso da mosca.
Dmitry Rubanovich
3

Um exemplo muito simples de __slot__atributo.

Problema: Sem __slots__

Se não tenho __slot__atributo na minha classe, posso adicionar novos atributos aos meus 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}

Se você olhar para exemplo acima, você pode ver que obj1 e obj2 têm o seu próprio x e y atributos e python também criou um dictatributo para cada objeto ( obj1 e obj2 ).

Suponha que minha classe Teste tenha milhares desses objetos? Criar um atributo adicional dictpara cada objeto causará muita sobrecarga (memória, poder de computação etc.) no meu código.

Solução: Com __slots__

Agora, no exemplo a seguir, minha classe Test contém __slots__atributo. Agora não posso adicionar novos atributos aos meus objetos (exceto o atributo x) e o python não cria mais um dictatributo. Isso elimina a sobrecarga de cada objeto, que pode se tornar significativa se você tiver muitos 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
fonte
2

Outro uso um tanto obscuro __slots__é adicionar atributos a um proxy de objeto do pacote ProxyTypes, anteriormente parte do projeto PEAK. Isso ObjectWrapperpermite proxy de outro objeto, mas intercepta todas as interações com o objeto proxy. Não é muito usado (e não há suporte para Python 3), mas nós o implementamos para implementar um invólucro de bloqueio seguro de thread em torno de uma implementação assíncrona baseada em tornado que libera todo o acesso ao objeto em proxy através do ioloop, usando thread-safe concurrent.Futureobjetos para sincronizar e retornar resultados.

Por padrão, qualquer acesso de atributo ao objeto proxy fornecerá o resultado do objeto proxy. Se você precisar adicionar um atributo no objeto proxy, __slots__poderá ser usado.

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
fonte
1

Você não tem - essencialmente - nenhuma utilidade __slots__.

No momento em que você acha que pode precisar __slots__, você realmente deseja usar padrões de design Leve ou Flyweight . Esses são casos em que você não deseja mais usar objetos puramente Python. Em vez disso, você deseja um wrapper semelhante ao objeto Python em torno de uma matriz, estrutura ou matriz numpy.

class Flyweight(object):

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

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

O wrapper de classe não possui atributos - apenas fornece métodos que atuam nos dados subjacentes. Os métodos podem ser reduzidos para métodos de classe. De fato, poderia ser reduzido a apenas funções operando na matriz subjacente de dados.

S.Lott
fonte
17
O que o Flyweight tem a ver __slots__?
oefe
3
@oefe: Eu certamente não entendi sua pergunta. Posso citar minha resposta, se ajudar "quando você acha que precisa de slots , você realmente deseja usar ... Padrão de design Flyweight". Isso é o que a Flyweight tem a ver com slots . Você tem uma pergunta mais específica?
S.Lott
21
@oefe: Flyweight e __slots__são duas técnicas de otimização para economizar memória. __slots__mostra benefícios quando você tem muitos objetos, além do padrão de design Flyweight. Os dois resolvem o mesmo problema.
JFS
7
Existe uma comparação disponível entre o uso de slots e o Flyweight em relação ao consumo e velocidade de memória?
kontulai
8
Embora o Flyweight seja certamente útil em alguns contextos, acredite ou não, a resposta para "como reduzir o uso de memória no Python quando crio um zilhão de objetos" nem sempre é "não use o Python para seus zilhões de objetos". Às vezes, __slots__realmente é a resposta e, como Evgeni aponta, pode ser adicionado como uma simples reflexão tardia (por exemplo, você pode se concentrar primeiro na correção e depois adicionar o desempenho).
Patrick Maupin
0

A pergunta original era sobre casos de uso geral, não apenas sobre memória. Portanto, deve-se mencionar aqui que você também obtém melhor desempenho ao instanciar grandes quantidades de objetos - interessante, por exemplo, ao analisar grandes documentos em objetos ou em um banco de dados.

Aqui está uma comparação da criação de árvores de objetos com um milhão de entradas, usando slots e sem slots. Como referência, também o desempenho ao usar dictos simples para as árvores (Py2.7.10 no 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

Classes de teste (ident, apart from slots):

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)

testcode, modo detalhado:

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
Comprimido vermelho
fonte