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


当前回答

引用雅各布·海伦的话:

The proper use of __slots__ 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 use of __slots__ eliminates the overhead of one dict for every object.] 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. Unfortunately there is a side effect to slots. They change the behavior of the objects that have slots in a way that can be abused by control freaks and static typing weenies. This is bad, because the control freaks should be abusing the metaclasses and the static typing weenies should be abusing decorators, since in Python, there should be only one obvious way of doing something. Making CPython smart enough to handle saving space without __slots__ is a major undertaking, which is probably why it is not on the list of changes for P3k (yet).

其他回答

在Python中,__slots__的目的是什么?在哪些情况下应该避免使用它?

TLDR:

特殊属性__slots__允许你显式地声明你希望你的对象实例具有哪些实例属性,以及预期的结果:

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

节省的空间来自

将值引用存储在槽中而不是__dict__。 如果父类拒绝创建__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__中的属性实际存储在slots中,而不是__dict__,类必须继承自object(在Python 3中自动,但在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

and

>>> 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__: Memory Savings

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

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

使用__dict__节省的空间是非常重要的。

SQLAlchemy将大量内存节省归因于__slots__。

To verify this, using the Anaconda distribution of Python 2.7 on Ubuntu Linux, with guppy.hpy (aka heapy) and sys.getsizeof, the size of a class instance without __slots__ declared, and nothing else, is 64 bytes. That does not include the __dict__. Thank you Python for lazy evaluation again, the __dict__ is apparently not called into existence until it is referenced, but classes without data are usually useless. When called into existence, the __dict__ attribute is a minimum of 280 bytes additionally.

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

对于64位Python,我在Python 2.7和3.6中以字节为单位说明了内存消耗,对于3.6中dict增长的每个点的__slots__和__dict__(没有定义插槽)(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。在python3中,所有子类都是对象,但在python2中必须显式:

class Base(object): 
    __slots__ = ()

now:

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

but:

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

要允许在继承slot对象子类时创建__dict__,只需在__slots__中添加'__dict__'(注意,slot是有序的,并且您不应该重复已经在父类中的slot):

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

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

and

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

或者你甚至不需要在子类中声明__slots__,你仍然会使用来自父类的slot,但不限制__dict__的创建:

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

And:

>>> 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子类时设置为empty 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'

你不需要有插槽-所以如果你添加它们,然后删除它们,它应该不会引起任何问题。

这里冒个风险:如果你正在组合mixin或使用抽象基类,它们不打算被实例化,在这些父类中使用空__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__。

Why?

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

在创建接口或mixins时,__slots__有助于重用性。

确实,许多Python用户并不是为了可重用性而编写的,但如果是这样,那么可以选择拒绝不必要的空间使用是很有价值的。

__slots__不会破坏酸洗

当pickle一个槽对象时,你可能会发现它会抱怨一个误导性的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解释器足够动态,只在对象实际添加时才需要字典,那么这就完全没有必要了。

Python实际上做了类似的事情,只在访问__dict__时创建__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__ =[]"。

除了其他答案,这里还有一个使用__slots__的例子:

>>> class Test(object):   #Must be new-style class!
...  __slots__ = ['x', 'y']
... 
>>> pt = Test()
>>> dir(pt)
['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__', 
 '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', 
 '__repr__', '__setattr__', '__slots__', '__str__', 'x', 'y']
>>> pt.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: x
>>> pt.x = 1
>>> pt.x
1
>>> pt.z = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute 'z'
>>> pt.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute '__dict__'
>>> pt.__slots__
['x', 'y']

因此,要实现__slots__,它只需要额外的一行(并使您的类成为一个新样式的类,如果它还不是的话)。通过这种方式,您可以将这些类的内存占用减少5倍,代价是必须编写自定义pickle代码(如果需要的话)。

如果你要实例化很多(成百上千)同一个类的对象,你会想要使用__slots__。__slots__仅作为内存优化工具存在。

强烈建议使用__slots__来约束属性创建。

使用__slots__ pickle对象将无法使用默认的(最古老的)pickle协议;有必要指定一个更高的版本。

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

最初的问题是关于一般用例,而不仅仅是关于内存。 因此,这里应该提到的是,当实例化大量对象时,您也会获得更好的性能——有趣的是,当将大型文档解析为对象或从数据库中解析时。

下面是使用插槽和不使用插槽创建具有一百万个条目的对象树的比较。作为对树使用普通字典时的性能参考(OSX上的Py2.7.10):

********** RUN 1 **********
1.96036410332 <class 'css_tree_select.element.Element'>
3.02922606468 <class 'css_tree_select.element.ElementNoSlots'>
2.90828204155 dict
********** RUN 2 **********
1.77050495148 <class 'css_tree_select.element.Element'>
3.10655999184 <class 'css_tree_select.element.ElementNoSlots'>
2.84120798111 dict
********** RUN 3 **********
1.84069895744 <class 'css_tree_select.element.Element'>
3.21540498734 <class 'css_tree_select.element.ElementNoSlots'>
2.59615707397 dict
********** RUN 4 **********
1.75041103363 <class 'css_tree_select.element.Element'>
3.17366290092 <class 'css_tree_select.element.ElementNoSlots'>
2.70941114426 dict

测试类(标识,除了槽):

class Element(object):
    __slots__ = ['_typ', 'id', 'parent', 'childs']
    def __init__(self, typ, id, parent=None):
        self._typ = typ
        self.id = id
        self.childs = []
        if parent:
            self.parent = parent
            parent.childs.append(self)

class ElementNoSlots(object): (same, w/o slots)

Testcode,详细模式:

na, nb, nc = 100, 100, 100
for i in (1, 2, 3, 4):
    print '*' * 10, 'RUN', i, '*' * 10
    # tree with slot and no slot:
    for cls in Element, ElementNoSlots:
        t1 = time.time()
        root = cls('root', 'root')
        for i in xrange(na):
            ela = cls(typ='a', id=i, parent=root)
            for j in xrange(nb):
                elb = cls(typ='b', id=(i, j), parent=ela)
                for k in xrange(nc):
                    elc = cls(typ='c', id=(i, j, k), parent=elb)
        to =  time.time() - t1
        print to, cls
        del root

    # ref: tree with dicts only:
    t1 = time.time()
    droot = {'childs': []}
    for i in xrange(na):
        ela =  {'typ': 'a', id: i, 'childs': []}
        droot['childs'].append(ela)
        for j in xrange(nb):
            elb =  {'typ': 'b', id: (i, j), 'childs': []}
            ela['childs'].append(elb)
            for k in xrange(nc):
                elc =  {'typ': 'c', id: (i, j, k), 'childs': []}
                elb['childs'].append(elc)
    td = time.time() - t1
    print td, 'dict'
    del droot

从Python 3.9开始,字典可用于通过__slots__向属性添加描述。没有描述的属性可以使用None,即使给出了描述,私有变量也不会出现。

class Person:

    __slots__ = {
        "birthday":
            "A datetime.date object representing the person's birthday.",
        "name":
            "The first and last name.",
        "public_variable":
            None,
        "_private_variable":
            "Description",
    }


help(Person)
"""
Help on class Person in module __main__:

class Person(builtins.object)
 |  Data descriptors defined here:
 |
 |  birthday
 |      A datetime.date object representing the person's birthday.
 |
 |  name
 |      The first and last name.
 |
 |  public_variable
"""