__slots__ 的使用?

新手上路,请多包涵

在 Python 中 __slots__ 的目的是什么——特别是关于我什么时候想使用它,什么时候不想使用它?

原文由 Jeb 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 438
2 个回答

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

这是因为 Base 的插槽描述符有一个与 Wrong 的插槽分开的插槽。这通常不应该出现,但它可以:

 >>> 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 在 Python 2 中是显式的)。

  • 为了防止创建 __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 上的 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,我以字节为单位说明了 Python 2.7 和 3.6 中的内存消耗,对于 __slots____dict__ (没有定义插槽)对于字典在 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 中类的命名空间中每个槽有一次性成本,在 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 。所有子类 object 在 Python 3 中,但在 Python 2 中你必须明确:

 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__ 在对有槽对象进行子类化时创建,只需将 '__dict__' 添加到 __slots__ 在父类中):

 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__ 因为你希望能够动态添加属性实际上不是一个好理由 - 如果需要,只需将 "__dict__" 添加到你的 __slots__

如果您需要该功能,您可以类似地添加 __weakref____slots__ 显式。

子类化命名元组时设置为空元组:

内置的 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'

可以 允许 __dict__ __slots__ = () ,但您不能将非空 __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 开发文档是最新的) 的其余部分中梳理出更多注意事项,我最近对这些文档做出了重要贡献。

对其他答案的批评

当前的最佳答案引用了过时的信息,并且相当手摇,并且在某些重要方面没有达到目标。

不要“仅在实例化大量对象时使用 __slots__

我引用:

“如果你要实例化很多(成百上千)同一个类的对象,你会想使用 __slots__ 。”

抽象基类,例如,来自 collections 模块,没有被实例化,但是 __slots__ 被声明为它们。

为什么?

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

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

的确,许多 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 的历史是一致的,来自 Unifying types and classes in Python 2.2

如果您对内置类型进行子类化,则会自动向实例添加额外空间以容纳 __dict____weakrefs__ 。 ( __dict__ 在你使用它之前不会被初始化,所以你不应该担心你创建的每个实例的空字典占用的空间。)如果你不需要这个额外的空间,你可以将短语“ __slots__ = [] ”添加到你的班级。

原文由 Russia Must Remove Putin 发布,翻译遵循 CC BY-SA 4.0 许可协议

引用 雅各布·哈伦的话

正确使用 __slots__ 是为了节省对象空间。不是有一个允许随时向对象添加属性的动态字典,而是有一个不允许在创建后添加的静态结构。 [这种使用 __slots__ 消除了每个对象一个字典的开销。] 虽然这有时是一个有用的优化,但如果 Python 解释器足够动态,它就完全没有必要了,它只需要字典当实际上有添加到对象时。

不幸的是,插槽有副作用。它们以一种可以被控制狂和静态类型怪人滥用的方式改变具有插槽的对象的行为。这很糟糕,因为控制狂应该滥用元类,而静态类型专家应该滥用装饰器,因为在 Python 中,做某事应该只有一种明显的方法。

使 CPython 足够智能以处理节省空间而无需 __slots__ 是一项重大任务,这可能是它不在 P3k 更改列表中的原因(目前)。

原文由 Jeff Bauer 发布,翻译遵循 CC BY-SA 3.0 许可协议

推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏