<blockquote>
<h1>In Python, what is the purpose of <code>__slots__</code> and what are the cases one should avoid this?</h1>
</blockquote>
<h2>TLDR编号:</h2>
<p>特殊属性<code>__slots__</code>允许您显式地声明希望对象实例具有哪些实例属性,并显示预期结果:</p>
<ol>
<li><strong>更快的属性访问。</li>
<li>在内存中节省空间。</li>
</ol>
<p>节省的空间来自</p>
<ol>
<li>将值引用存储在插槽中,而不是<code>__dict__</code>。</li>
<li>拒绝<code>__dict__</code>和<code>__weakref__</code>创建,如果父类拒绝它们并且您声明<code>__slots__</code>。</li>
</ol>
<h3>快速警告</h3>
<p>小小的警告,您应该在继承树中只声明一次特定的时隙。例如:</p>
<pre><code>class Base:
__slots__ = 'foo', 'bar'
class Right(Base):
__slots__ = 'baz',
class Wrong(Base):
__slots__ = 'foo', 'bar', 'baz' # redundant foo and bar
</code></pre>
<p>Python不反对当你弄错了(它可能应该),问题可能不会以其他方式显现,但是你的对象将占用比他们应该占用更多的空间。</p>
<pre><code>>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(64, 80)
</code></pre>
<p>最大的警告是多重继承-不能组合多个“带非空槽的父类”。</p>
<p>为了适应这种限制,请遵循最佳实践:去掉所有父类的抽象(除了一个或所有父类之外),这些父类的具体类和新的具体类将共同继承自这些抽象(就像标准库中的抽象基类一样)。</p>
<p>有关示例,请参见下面关于多重继承的部分。</p>
<h3>要求:</h3>
<ul>
<li><p>若要使在<code>__slots__</code>中命名的属性实际存储在插槽中而不是<code>__dict__</code>,则类必须从<code>object</code>继承。</p></li>
<li><p>为了防止创建<code>__dict__</code>,必须从<code>object</code>继承,继承中的所有类都必须声明<code>__slots__</code>,并且任何类都不能有<code>'__dict__'</code>项。</p></li>
</ul>
<p>如果你想继续阅读的话,有很多细节。</p>
<h2>为什么使用<code>__slots__</code>:更快的属性访问</h2>
<p>Python的创建者Guido van Rossum,<a href="http://python-history.blogspot.com/2010/06/inside-story-on-new-style-classes.html" rel="noreferrer">states</a>实际上是他为了更快的属性访问而创建的<code>__slots__</code>。</p>
<p>很容易证明访问速度显著加快:</p>
<pre><code>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
</code></pre>
<p>以及</p>
<pre><code>>>> min(timeit.repeat(get_set_delete_fn(slotted)))
0.2846834529991611
>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))
0.3664822799983085
</code></pre>
<p>在Ubuntu上的Python 3.5中,时隙访问快了近30%。</p>
<pre><code>>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342
</code></pre>
<p>在Windows上的Python2中,我已经测量到它大约快了15%。</p>
<h2>为什么使用<code>__slots__</code>:节省内存</h2>
<p><code>__slots__</code>的另一个目的是减少每个对象实例占用的内存空间。</p>
<p><a href="https://docs.python.org/3/reference/datamodel.html#slots" rel="noreferrer">My own contribution to the documentation clearly states the reasons behind this</a>:</p>
<blockquote>
<p>The space saved over using <code>__dict__</code> can be significant.</p>
</blockquote>
<p><a href="http://docs.sqlalchemy.org/en/rel_1_0/changelog/migration_10.html#significant-improvements-in-structural-memory-use" rel="noreferrer">SQLAlchemy attributes</a>为<code>__slots__</code>节省了大量内存。</p>
<p>为了验证这一点,在Ubuntu Linux上使用Python 2.7的Anaconda发行版,在声明了<code>guppy.hpy</code>(又名heapy)和<code>sys.getsizeof</code>的情况下,没有声明<code>__slots__</code>的类实例的大小是64字节。这并不包括<code>__dict__</code>。再次感谢Python的懒惰计算,<code>__dict__</code>在被引用之前显然不会被调用,但是没有数据的类通常是无用的。当被调用存在时,<code>__dict__</code>属性至少额外280字节。</p>
<p>相比之下,声明为<code>__slots__</code>的类实例(没有数据)只有16个字节,插槽中有一个项的类实例总共有56个字节,有两个项的类实例有64个字节。</p>
<p>对于64位Python,我以字节为单位说明了Python 2.7和3.6中的内存消耗,对于dict在3.6中增长的每个点(除了0、1和2个属性),分别是<code>__slots__</code>和<code>__dict__</code>(未定义槽):</p>
<pre><code> 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
</code></pre>
<p>因此,尽管Python 3中有更小的dict,但是我们可以看到实例的<code>__slots__</code>伸缩性如何很好地节省我们的内存,这也是您希望使用<code>__slots__</code>的主要原因。</p>
<p>为了完整起见,请注意,在Python 2中,类的名称空间中的每个时隙都有一个一次性的开销,即64个字节,在Python 3中为72个字节,因为时隙使用诸如属性之类的数据描述符,称为“成员”。</p>
<pre><code>>>> Foo.foo
<member 'foo' of 'Foo' objects>
>>> type(Foo.foo)
<class 'member_descriptor'>
>>> getsizeof(Foo.foo)
72
</code></pre>
<h2>演示<code>__slots__</code>:</h2>
<p>若要拒绝创建<code>__dict__</code>,必须子类<code>object</code>:</p>
<pre><code>class Base(object):
__slots__ = ()
</code></pre>
<p>现在:</p>
<pre><code>>>> 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'
</code></pre>
<p>或子类定义<code>__slots__</code>的其他类</p>
<pre><code>class Child(Base):
__slots__ = ('a',)
</code></pre>
<p>现在:</p>
<pre><code>c = Child()
c.a = 'a'
</code></pre>
<p>但是:</p>
<pre><code>>>> 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'
</code></pre>
<p>若要允许在子类化时隙对象时创建<code>__dict__</code>,只需将<code>'__dict__'</code>添加到<code>__slots__</code>(请注意,插槽是有序的,不应重复父类中已存在的插槽):</p>
<pre><code>class SlottedWithDict(Child):
__slots__ = ('__dict__', 'b')
swd = SlottedWithDict()
swd.a = 'a'
swd.b = 'b'
swd.c = 'c'
</code></pre>
<p>以及</p>
<pre><code>>>> swd.__dict__
{'c': 'c'}
</code></pre>
<p>或者您甚至不需要在子类中声明<code>__slots__</code>,并且您仍将使用来自父类的时隙,但不限制<code>__dict__</code>的创建:</p>
<pre><code>class NoSlots(Child): pass
ns = NoSlots()
ns.a = 'a'
ns.b = 'b'
</code></pre>
<p>以及:</p>
<pre><code>>>> ns.__dict__
{'b': 'b'}
</code></pre>
<p>但是,<code>__slots__</code>可能会导致多重继承问题:</p>
<pre><code>class BaseA(object):
__slots__ = ('a',)
class BaseB(object):
__slots__ = ('b',)
</code></pre>
<p>因为从同时具有两个非空插槽的父类创建子类失败:</p>
<pre><code>>>> 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
</code></pre>
<p>如果遇到此问题,您可以从父级中删除<code>__slots__</code>,或者如果您控制了父级,则为它们提供空槽,或者重构为抽象:</p>
<pre><code>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!
</code></pre>
<h3>将<code>'__dict__'</code>添加到<code>__slots__</code>以获取动态分配:</h3>
<pre><code>class Foo(object):
__slots__ = 'bar', 'baz', '__dict__'
</code></pre>
<p>现在:</p>
<pre><code>>>> foo = Foo()
>>> foo.boink = 'boink'
</code></pre>
<p>因此,使用插槽中的<code>'__dict__'</code>时,我们会失去一些大小优势,因为这样做的好处是具有动态分配,并且仍然为我们所期望的名称提供插槽。</p>
<p>当您从一个未开槽的对象继承时,当您使用<code>__slots__</code>时会得到相同的语义-位于<code>__slots__</code>中的名称指向开槽值,而任何其他值都放在实例的<code>__dict__</code>中。</p>
<p>避免<code>__slots__</code>因为您希望能够动态添加属性实际上不是一个好的理由-如果需要,只需将<code>"__dict__"</code>添加到您的<code>__slots__</code>中。</p>
<p>如果您需要这个特性,也可以类似地将<code>__weakref__</code>显式地添加到<code>__slots__</code>。</p>
<h3>子类化namedtuple时设置为空元组:</h3>
<p>namedtuple内置的不可变实例非常轻量级(本质上是元组的大小),但要获得这些好处,如果对它们进行子类化,则需要自己执行:</p>
<pre><code>from collections import namedtuple
class MyNT(namedtuple('MyNT', 'bar baz')):
"""MyNT is an immutable and lightweight object"""
__slots__ = ()
</code></pre>
<p>用法:</p>
<pre><code>>>> nt = MyNT('bar', 'baz')
>>> nt.bar
'bar'
>>> nt.baz
'baz'
</code></pre>
<p>尝试分配意外属性会引发一个<code>AttributeError</code>,因为我们已经阻止了<code>__dict__</code>的创建:</p>
<pre><code>>>> nt.quux = 'quux'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyNT' object has no attribute 'quux'
</code></pre>
<p>您可以通过关闭<code>__slots__ = ()</code>来允许<code>__dict__</code>创建,但不能对tuple的子类型使用非空<code>__slots__</code>。</p>
<h2>最大的警告:多重继承</h2>
<p>即使对于多个父级,非空插槽是相同的,它们也不能一起使用:</p>
<pre><code>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
</code></pre>
<p>在父级中使用空的<code>__slots__</code>似乎提供了最大的灵活性,<strong>允许子级选择阻止或允许</strong>(通过添加<code>'__dict__'</code>来获取动态赋值,请参阅上面的部分)<strong>创建<code>__dict__</code></strong>:</p>
<pre><code>class Foo(object): __slots__ = ()
class Bar(object): __slots__ = ()
class Baz(Foo, Bar): __slots__ = ('foo', 'bar')
b = Baz()
b.foo, b.bar = 'foo', 'bar'
</code></pre>
<p>你没有<em>的</em>插槽-所以如果你添加它们,并在以后删除它们,应该不会造成任何问题。</p>
<p><strong>在这里走投无路:如果您正在编写<a href="https://stackoverflow.com/questions/860245/mixin-vs-inheritance/27907511#27907511">mixins</a>或使用<a href="https://stackoverflow.com/questions/372042/difference-between-abstract-class-and-interface-in-python/31439126#31439126">abstract base classes</a>(不打算被实例化),在这些父类中使用空的<code>__slots__</code>似乎是子类灵活性方面的最佳方法。</p>
<p>为了演示,首先,让我们创建一个类,其中包含要在多重继承下使用的代码</p>
<pre><code>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)})'
</code></pre>
<p>我们可以通过继承和声明预期的插槽来直接使用上述内容:</p>
<pre><code>class Foo(AbstractBase):
__slots__ = 'a', 'b'
</code></pre>
<p>但我们不在乎这个,这是一个微不足道的单一继承,我们需要另一个我们也可以继承的类,可能有一个嘈杂的属性:</p>
<pre><code>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
</code></pre>
<p>如果两个基地都有空位,我们就不能做下面的事。(事实上,如果我们愿意的话,我们可以给<code>AbstractBase</code>非空的插槽a和b,并将它们排除在下面的声明之外—将它们留在中是错误的):</p>
<pre><code>class Concretion(AbstractBase, AbstractBaseC):
__slots__ = 'a b _c'.split()
</code></pre>
<p>现在我们可以通过多重继承来实现这两种功能,并且仍然可以拒绝<code>__dict__</code>和<code>__weakref__</code>实例化:</p>
<pre><code>>>> 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'
</code></pre>
<h2>避免开槽的其他情况:</h2>
<ul>
<li>当您想用另一个没有它们的类执行<code>__class__</code>赋值时,请避免它们(并且您可以不要添加它们),除非插槽布局相同。(我对了解谁在做这件事以及为什么这么做很感兴趣。)</li>
<li>如果要对可变长度内置项(如long、tuple或str)进行子类划分,并且要向其添加属性,请避免使用它们。</li>
<li>如果坚持通过实例变量的类属性提供默认值,请避免使用它们。</li>
</ul>
<p>您也许可以从<code>__slots__</code><a href="https://docs.python.org/3.7/reference/datamodel.html#slots" rel="noreferrer">documentation (the 3.7 dev docs are the most current)</a>的其余部分中梳理出进一步的注意事项,我最近对这些内容做出了重要贡献。</p>
<h2>对其他答案的评论</h2>
<p>目前最热门的答案引用了过时的信息,在一些重要的方面是相当不得体的。</p>
<h3>不要“在实例化大量对象时只使用<code>__slots__</code>”</h3>
<p>我引用:</p>
<blockquote>
<p>"You would want to use <code>__slots__</code> if you are going to instantiate a lot (hundreds, thousands) of objects of the same class." </p>
</blockquote>
<p>例如,来自<code>collections</code>模块的抽象基类没有被实例化,但是为它们声明了<code>__slots__</code>。</p>
<p>为什么?</p>
<p>如果用户希望拒绝<code>__dict__</code>或<code>__weakref__</code>创建,则这些内容在父类中不能使用。</p>
<p><code>__slots__</code>有助于在创建接口或混合时重用。</p>
<p>确实,许多Python用户不是为了可重用性而编写的,但是如果是这样,可以选择拒绝不必要的空间使用是很有价值的。</p>
<h3><code>__slots__</code>不会破坏酸洗</h3>
<p>当挑选一个时隙对象时,您可能会发现它抱怨一个误导性的<code>TypeError</code>:</p>
<pre><code>>>> pickle.loads(pickle.dumps(f))
TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled
</code></pre>
<p>这实际上是不正确的。此消息来自最早的协议,这是默认协议。您可以使用<code>-1</code>参数选择最新的协议。在Python 2.7中,这是<code>2</code>(在2.3中引入),在3.6中是<code>4</code>。</p>
<pre><code>>>> pickle.loads(pickle.dumps(f, -1))
<__main__.Foo object at 0x1129C770>
</code></pre>
<p>在Python2.7中:</p>
<pre><code>>>> pickle.loads(pickle.dumps(f, 2))
<__main__.Foo object at 0x1129C770>
</code></pre>
<p>在Python3.6中</p>
<pre><code>>>> pickle.loads(pickle.dumps(f, 4))
<__main__.Foo object at 0x1129C770>
</code></pre>
<p>所以我会记住这一点,因为这是一个已经解决的问题。</p>
<h2>对(截至2016年10月2日)接受答案的评论</h2>
<p>第一段是半简短的解释,半预测。这是唯一能回答这个问题的部分</p>
<blockquote>
<p>The proper use of <code>__slots__</code> is to save space in objects. Instead of having a dynamic dict that allows adding attributes to objects at anytime, there is a static structure which does not allow additions after creation. This saves the overhead of one dict for every object that uses slots</p>
</blockquote>
<p>下半场是一厢情愿的想法,而且离题了:</p>
<blockquote>
<p>While this is sometimes a useful optimization, it would be completely unnecessary if the Python interpreter was dynamic enough so that it would only require the dict when there actually were additions to the object.</p>
</blockquote>
<p>Python实际上做了类似的事情,只在访问时创建<code>__dict__</code>,但是创建许多没有数据的对象是相当荒谬的。</p>
<p>第二段过分简化并忽略了避免<code>__slots__</code>的实际原因。下面是<em>不</em>避免槽的真正原因(对于<em>实际</em>原因,请参阅上面我的其余答案):</p>
<blockquote>
<p>They change the behavior of the objects that have slots in a way that can be abused by control freaks and static typing weenies.</p>
</blockquote>
<p>然后讨论用Python实现这个错误目标的其他方法,而不是讨论与<code>__slots__</code>有关的任何事情。</p>
<p>第三段更是一厢情愿。加在一起,大部分都是离谱的内容,回答者甚至没有作者和贡献的弹药批评者的网站。</p>
<h2>内存使用证据</h2>
<p>创建一些普通对象和开槽对象:</p>
<pre><code>>>> class Foo(object): pass
>>> class Bar(object): __slots__ = ()
</code></pre>
<p>例举一百万个:</p>
<pre><code>>>> foos = [Foo() for f in xrange(1000000)]
>>> bars = [Bar() for b in xrange(1000000)]
</code></pre>
<p>用<code>guppy.hpy().heap()</code>检查:</p>
<pre><code>>>> 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
...
</code></pre>
<p>访问常规对象及其<code>__dict__</code>,然后再次检查:</p>
<pre><code>>>> 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
...
</code></pre>
<p>这与Python的历史是一致的,从<a href="https://www.python.org/download/releases/2.2.2/descrintro/" rel="noreferrer">Unifying types and classes in Python 2.2</a></p>
<blockquote>
<p>If you subclass a built-in type, extra space is automatically added to the instances to accomodate <code>__dict__</code> and <code>__weakrefs__</code>. (The <code>__dict__</code> is not initialized until you use it though, so you shouldn't worry about the space occupied by an empty dictionary for each instance you create.) If you don't need this extra space, you can add the phrase "<code>__slots__ = []</code>" to your class.</p>
</blockquote>