Qual é o objetivo do __slots__
Python - especialmente em relação a quando eu gostaria de usá-lo e quando não?
No Python, qual é o objetivo
__slots__
e quais são os casos em que se deve evitar isso?
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:
A economia de espaço é de
__dict__
.__dict__
e __weakref__
criação se as classes pai as negarem e você declara __slots__
.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.
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 object
e 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.
__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.
__slots__
: Economia de MemóriaOutro 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
__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!
'__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.
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 AttributeError
porque 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.
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 AbstractBase
slots 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'
__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ê.)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.
As principais respostas atuais citam informações desatualizadas e são bastante ondulantes e perdem a marca em alguns aspectos importantes.
__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 collections
mó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 decapagemAo 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 -1
argumento 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.
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.
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.
class Child(BaseA, BaseB): __slots__ = ('a', 'b')
exemplo com os pais de slots empy. Por que aqui édictproxy
criado em vez de criar umAttributeError
parac
?__slots__
. A sério! Obrigado!__slots__
cerca de um ano atrás: github.com/python/cpython/pull/1819/filesCitando Jacob Hallen :
fonte
__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!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.
fonte
Cada objeto python possui um
__dict__
atributo que é um dicionário que contém todos os outros atributos. por exemplo, quando você digitaself.attr
python está realmente fazendoself.__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.fonte
__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 queinstance.__hidden_attributes[instance.__class__[attrname]]
seja mais rápido queinstance.__dict__[attrname]
.Além das outras respostas, aqui está um exemplo de uso
__slots__
: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.fonte
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.
fonte
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
fonte
__slots__
?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.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
dict
atributo para cada objeto ( obj1 e obj2 ).Suponha que minha classe Teste tenha milhares desses objetos? Criar um atributo adicional
dict
para 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 atributox
) e o python não cria mais umdict
atributo. Isso elimina a sobrecarga de cada objeto, que pode se tornar significativa se você tiver muitos objetos.fonte
Outro uso um tanto obscuro
__slots__
é adicionar atributos a um proxy de objeto do pacote ProxyTypes, anteriormente parte do projeto PEAK. IssoObjectWrapper
permite 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-safeconcurrent.Future
objetos 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.fonte
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.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.
fonte
__slots__
?__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.__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).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):
Classes de teste (ident, apart from slots):
testcode, modo detalhado:
fonte