协慌网

登录 贡献 社区

__slots__的用法?

Python 中__slots__的目的是什么 - 尤其是关于何时以及何时不使用它的目的?

答案

在 Python 中, __slots__的目的是什么?在什么情况下应避免这种情况?

TLDR:

特殊属性__slots__允许您显式说明您希望对象实例具有哪些实例属性,并具有预期的结果:

  1. 更快的属性访问。
  2. 节省内存空间

节省的空间来自

  1. 将值引用存储在插槽中而不是__dict__
  2. 如果父类拒绝__dict____weakref__而您声明__slots__拒绝创建。

快速警告

请注意,您只应在继承树中一次声明一个特定的插槽。例如:

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

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

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

遇到错误时,Python 不会反对(它应该会),否则问题可能不会显现出来,但您的对象将比原先占用更多的空间。 Python 3.8:

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

这是因为基准站的插槽描述符的插槽与错误的插槽分开。通常不应该这样,但是可以:

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

最大的警告是多重继承 - 无法将多个 “具有非空插槽的父类” 组合在一起。

为适应此限制,请遵循最佳实践:排除所有父母的抽象,而其具体类和您的新具体类将共同继承该父类 - 给该抽象空的位置(就像父类中的抽象基类一样)标准库)。

有关示例,请参见下面有关多重继承的部分。

要求:

  • 为了使以__slots__命名的属性实际存储在插槽中而不是__dict__ ,类必须从object继承。

  • 为了防止创建__dict__ ,您必须从object继承,并且继承中的所有类都必须声明__slots__并且它们都不能具有'__dict__'条目。

如果您想继续阅读,有很多细节。

为什么使用__slots__ :更快的属性访问。

Python 的创建者 Guido van Rossum ,他实际上创建了__slots__以便更快地访问属性。

证明可观的显着更快访问是微不足道的:

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

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

在 Ubuntu 3.5 上的 Python 3.5 中,插槽式访问的速度几乎快了 30%。

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

在 Windows 上的 Python 2 中,我测得的速度要快 15%。

为什么使用__slots__ :节省内存

__slots__另一个目的是减少每个对象实例占用的内存空间。

我自己对文档的贡献清楚地说明了其背后的原因

使用__dict__节省的空间可能很大。

SQLAlchemy 将大量内存节省归因__slots__

为了验证这一点,请在 Ubuntu Linux 上使用 Python 2.7 的 Anaconda 发行版,并带有guppy.hpy (又称堆)和sys.getsizeof ,不声明__slots__且没有其他内容的类实例的大小为 64 字节。这包括__dict__ 。再次感谢 Python 的懒惰求值,显然在引用__dict__之前,它并不存在,但是没有数据的类通常是无用的。当被调用时, __dict__属性至少要最少 280 个字节。

相反,声明为() (无数据)的__slots__的类实例只有 16 个字节,而插槽中有一项的总字节数为 56 个,插槽中有两项的 64 个字节。

对于 64 位 Python,我将说明 dict 在 3.6 中增长的每个点的__slots____dict__ (未定义插槽)的内存消耗(以字节为单位)以 python 2.7 和 3.6 为单位(0、1 和 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

因此,尽管 Python 3 中的指令较小,但是我们看到__slots__可以很好地扩展实例以节省内存,这是您想使用__slots__主要原因。

仅出于我的注意事项的完整性,请注意,在类的名称空间中,每个插槽的一次性成本为 Python 2 中 64 字节,而在 Python 3 中为 72 字节,因为插槽使用数据描述符(如属性)称为 “成员”。

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

__slots__演示:

要拒绝创建__dict__ ,必须子类化object

class Base(object): 
    __slots__ = ()

现在:

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

或子类化另一个定义__slots__

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

现在:

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

但:

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

若要在创建插槽对象的子类时创建__dict__ ,只需在__slots__添加'__dict__' (请注意,插槽是有序的,您不应重复父类中已经存在的插槽):

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

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

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

或者甚至不需要在子类中声明__slots__ ,并且仍将使用父级的插槽,但不限制__dict__的创建:

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

和:

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

但是, __slots__可能会导致多重继承问题:

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

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

由于从具有两个非空插槽的父母创建子类失败:

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

如果遇到此问题,则可以从父级中删除__slots__ ,或者如果您可以控制父级,则为他们提供空位,或重构为抽象:

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__'添加到__slots__以获得动态分配:

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

现在:

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

因此,在插槽中使用'__dict__' ,由于具有动态分配并且仍然具有我们期望的名称的插槽,因此我们失去了一些尺寸上的好处。

当你从没有开槽对象继承,你得到同样的排序语义当您使用__slots__ - 名字是在__slots__点开槽值,而任何其他值都放在实例的__dict__

避免__slots__是因为您希望能够随时添加属性实际上不是一个好理由 - 如果需要,只需在__slots__添加"__dict__"

如果需要该功能,可以类似地将__weakref__添加到__slots__

子类化 namedtuple 时,设置为空 tuple:

内置 namedtuple 使不可变的实例非常轻巧(本质上是元组的大小),但是要获得好处,如果将它们子类化,则需要自己做:

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

用法:

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

尝试分配意外的属性会引发AttributeError因为我们阻止了__dict__的创建:

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

可以通过省略__slots__ = () 允许__dict__创建,但不能将非空__slots__与元组的子类型一起使用。

最大的警告:多重继承

即使多个父级的非空插槽相同,也不能一起使用:

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

在父级中使用空的__slots__似乎提供了最大的灵活性, 允许孩子选择阻止或允许 (通过添加'__dict__'来获得动态分配,请参见上文) 创建__dict__

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

不必有插槽 - 因此,如果您添加它们,后来删除它们,它不应引起任何问题。

在这里 忙个不停 :如果您要编写mixins或使用不打算实例化的抽象基类 ,那么对于子类来说,在这些父级中留空__slots__似乎是最好的方法。

为了演示,首先,让我们用要在多重继承下使用的代码创建一个类

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

我们可以通过继承并声明预期的位置来直接使用以上内容:

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

但是我们不在乎,这是微不足道的单一继承,我们需要另一个我们也可能继承的类,也许带有嘈杂的属性:

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

现在,如果两个基地都有非空插槽,我们将无法进行以下操作。 (实际上,如果我们愿意,我们可以给AbstractBase非空插槽 a 和 b,并将它们排除在下面的声明之外 - 将它们留在里面是错误的):

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

现在,我们通过多重继承获得了两种功能,并且仍然可以拒绝__dict____weakref__实例化:

>>> 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__分配(并且不能添加它们)时,请避免使用它们。 (我对了解谁在做什么以及为什么这样做很感兴趣。)
  • 如果您想将诸如 long,tuple 或 str 之类的可变长度内建子类化,并想为其添加属性,请避免使用它们。
  • 如果您坚持通过实例变量的类属性提供默认值,请避免使用它们。

您也许可以从__slots__ 文档的其余部分(最新的 3.7 dev 文档)中找出更多的警告,我最近做出了很大的贡献。

对其他答案的批评

当前的最佳答案引用了过时的信息,而且非常容易波动,并且在某些重要方面未达到要求。

不要 “在实例化许多对象时仅使用__slots__

我引用:

“如果要实例化大量(数百,数千个)同一类的对象,则需要使用__slots__ 。”

例如,没有实例化来自collections模块的 Abstract Base Classes,但为它们声明了__slots__

为什么?

如果用户希望拒绝__dict____weakref__创建,则这些内容在父类中必须不可用。

__slots__有助于创建接口或 mixin 时的可重用性。

的确,许多 Python 用户并不是为可重用性而编写的,但是当您这样做时,可以选择拒绝不必要的空间使用是很有价值的。

__slots__不会破坏酸洗

腌制开槽的对象时,您可能会发现它抱怨带有误导的TypeError

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

这实际上是不正确的。此消息来自最早的协议,这是默认协议。您可以使用-1参数选择最新的协议。在 Python 2.7 中,该值为2 (在 2.3 中引入),在 3.6 中为4

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

在 Python 2.7 中:

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

在 Python 3.6 中

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

因此,我会牢记这一点,因为这是一个已解决的问题。

评论(至 2016 年 10 月 2 日)被接受

第一段是一半简短的解释,一半是预测的。这是真正回答问题的唯一部分

__slots__的正确用法是节省对象空间。静态结构不允许在创建后添加对象,而不是具有允许随时向对象添加属性的动态字典。这样可以为使用插槽的每个对象节省一个指令的开销

后半部分是一厢情愿的想法,并且超出了预期:

尽管有时这是一个有用的优化,但如果 Python 解释器具有足够的动态性,则仅在实际添加对象时才需要 dict,就完全没有必要了。

Python 实际上做了类似的事情,只在被访问时创建__dict__ ,但是创建许多没有数据的对象是相当荒谬的。

第二段过分简化并错过了避免__slots__实际原因。以下不是避免使用插槽的真正原因(出于实际原因,请参阅上面我的回答的其余部分。):

它们以一种可被控制怪胎和静态类型临时表滥用的方式更改具有插槽的对象的行为。

然后,它继续讨论了使用 Python 实现此有害目标的其他方法,而不是讨论与__slots__有关的任何内容。

第三段是更多的如意算盘。答案者甚至根本没有写过这些杂乱的内容,而是为网站的批评者弹药。

内存使用证据

创建一些普通对象和带槽对象:

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

实例化其中的一百万:

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

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

访问常规对象及其__dict__并再次检查:

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

这与Python 2.2 中的统一类型和类中的 Python历史一致

如果您将内置类型作为子类,则多余的空间会自动添加到实例中,以容纳__dict____weakrefs__ 。 (虽然__dict__被初始化,除非您使用它,所以您不必担心空字典为您创建的每个实例所占用的空间。)如果不需要此多余的空间,则可以添加短语 “ __slots__ = [] “转到您的班级。

引用雅各布 · 哈伦Jacob Hallen)的话

__slots__的正确用法是节省对象空间。静态结构不允许在创建后添加对象,而不是具有允许随时向对象添加属性的动态字典。 [ __slots__使用消除了每个对象的一个字典的开销。] 虽然这有时是一个有用的优化,但是如果 Python 解释器足够动态,以便仅在实际添加了字典时才需要该字典,则完全没有必要。物体。

不幸的是,插槽有副作用。它们以一种可被控制怪胎和静态类型临时表滥用的方式更改具有插槽的对象的行为。这是不好的,因为控件怪胎应该滥用元类,而静态类型之间应该滥用装饰器,因为在 Python 中,应该只有一种明显的方法。

使 CPython 足够智能以在不使用__slots__情况下节省空间是一项重要的工作,这可能就是为什么它不在 P3k 更改列表中的原因(至今)。

如果要实例化(数百个,数千个)同一类的对象,则需要使用__slots____slots__仅作为内存优化工具存在。

不建议使用__slots__约束属性创建。

使用__slots__腌制对象不能使用默认的(最早的)腌制协议。有必要指定一个更高的版本。

python 的其他一些自省功能也可能受到不利影响。