__slots__?在Python中,目的是什么,应该避免这种情况是什么?__slots__内存使用证据

2022-09-05 01:06:39

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


答案 1

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

最大的警告是多重继承 - 多个“具有非空插槽的父类”不能组合。

为了适应此限制,请遵循最佳实践:除一个或所有父级的抽象外,其他所有抽象都将分别继承其具体类和新的具体类 - 为抽象提供空槽(就像标准库中的抽象基类一样)。

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

要求:

  • 要使 name in 中的属性实际存储在插槽中而不是 ,类必须继承自(在 Python 3 中是自动的,但在 Python 2 中必须是显式的)。__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上的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 发行版,带有(又名堆)和 ,未声明的类实例的大小为 64 字节。这不包括 .再次感谢Python的惰性评估,显然在引用之前不会调用它,但是没有数据的类通常是无用的。当调用时,该属性至少为 280 个字节。guppy.hpysys.getsizeof__slots____dict____dict____dict__

相比之下,声明为(无数据)的类实例只有 16 个字节,总字节数为 56 个字节,其中一个项目位于槽中,64 个字节位于两个。__slots__()

对于 64 位 Python,我说明了 Python 2.7 和 3.6 中以字节为单位的内存消耗,对于和(未定义插槽)在 3.6 中 dict 增长的每个点(0、1 和 2 属性除外):__slots____dict__

       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__

要拒绝创建 ,您必须子类 。在Python 3中,所有子类都是,但在Python 2中,你必须是显式的:__dict__objectobject

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

在父级中使用空似乎提供了最大的灵活性,允许孩子选择阻止或允许(通过添加以获取动态分配,请参阅上面的部分)创建__dict____slots__'__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

现在,如果两个基地都有非空插槽,我们就无法做到以下情况。(事实上,如果我们愿意,我们可以给出非空的插槽a和b,并将它们从下面的声明中省略 - 将它们留在里面是错误的):AbstractBase

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__
  • 如果要对长、元组或 str 等可变长度内置元素进行子类化,并且要向它们添加属性,请避免使用它们。
  • 如果您坚持通过实例变量的类属性提供默认值,请避免使用它们。

您可以从文档的其余部分(3.7 dev文档是最新的)中梳理出进一步的警告,我最近对这些文档做出了重大贡献。__slots__

对其他答案的批评

目前排名靠前的答案引用了过时的信息,并且非常容易挥舞,并且在一些重要方面错过了标记。

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

我引用:

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

抽象基类(例如,来自模块的基类)不会实例化,但会为它们声明。collections__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

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

>>> 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解释器足够动态,那么它完全没有必要,以至于它只需要在对象实际添加时才需要字典。

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的历史一致,从Python 2.2中的统一类型和类

如果对内置类型进行子类化,则会自动向实例添加额外的空间以容纳 和 。(在使用它之前不会初始化 ,因此您不必担心您创建的每个实例的空字典所占用的空间。如果您不需要此额外空间,则可以在类中添加短语 “”。__dict____weakrefs____dict____slots__ = []


答案 2

引用Jacob Hallen的话

正确的用法是节省对象中的空间。没有一个允许随时向对象添加属性的动态命令,而是有一个静态结构,不允许在创建后添加。[这种使用消除了每个对象一个字典的开销。虽然这有时是一个有用的优化,但如果Python解释器足够动态,那么它完全没有必要,以至于它只需要在对象实际添加时才需要字典。__slots____slots__

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

使CPython足够智能以处理节省空间是一项重大任务,这可能就是为什么它不在P3k的更改列表中的原因。__slots__