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


当前回答

在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__ =[]"。

其他回答

__slot__属性的一个非常简单的例子。

问题:没有__slots__

如果我的类中没有__slot__属性,我可以向对象添加新属性。

class Test:
    pass

obj1=Test()
obj2=Test()

print(obj1.__dict__)  #--> {}
obj1.x=12
print(obj1.__dict__)  # --> {'x': 12}
obj1.y=20
print(obj1.__dict__)  # --> {'x': 12, 'y': 20}

obj2.x=99
print(obj2.__dict__)  # --> {'x': 99}

如果你看上面的例子,你可以看到obj1和obj2有它们自己的x和y属性,python还为每个对象(obj1和obj2)创建了一个dict属性。

假设我的类Test有数千个这样的对象?在我的代码中,为每个对象创建一个额外的属性字典将导致大量的开销(内存,计算能力等)。

解决方案:使用__slots__

现在在下面的例子中,我的类Test包含__slots__属性。现在我不能添加新的属性到我的对象(属性x除外)和python不再创建dict属性。这消除了每个对象的开销,如果您有许多对象,这将变得非常重要。

class Test:
    __slots__=("x")

obj1=Test()
obj2=Test()
obj1.x=12
print(obj1.x)  # --> 12
obj2.x=99
print(obj2.x)  # --> 99

obj1.y=28
print(obj1.y)  # --> AttributeError: 'Test' object has no attribute 'y'

除了其他答案,这里还有一个使用__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代码(如果需要的话)。

除了在这里的其他答案中描述的无数优点-内存意识的紧凑实例,比更易变的__dict__承载实例更不容易出错等等-我发现使用__slots__提供了更清晰的类声明,因为类的实例变量显式地公开。

为了解决__slots__声明的继承问题,我使用了这个元类:

import abc

class Slotted(abc.ABCMeta):
    
    """ A metaclass that ensures its classes, and all subclasses,
        will be slotted types.
    """
    
    def __new__(metacls, name, bases, attributes, **kwargs):
        """ Override for `abc.ABCMeta.__new__(…)` setting up a
            derived slotted class.
        """
        if '__slots__' not in attributes:
            attributes['__slots__'] = tuple()
        
        return super(Slotted, metacls).__new__(metacls, name, # type: ignore
                                                        bases,
                                                        attributes,
                                                      **kwargs)

…如果在继承塔中声明为基类的元类,则确保从该基类派生的所有内容都将正确继承__slots__属性,即使中间类没有声明任何属性。像这样:

# note no __slots__ declaration necessary with the metaclass:
class Base(metaclass=Slotted):
    pass

# class is properly slotted, no __dict__:
class Derived(Base):
    __slots__ = 'slot', 'another_slot'

# class is also properly slotted:
class FurtherDerived(Derived):
    pass

除了其他答案,__slots__还通过将属性限制在预定义的列表中增加了一点排版安全性。这一直是JavaScript的一个问题,它还允许您向现有对象添加新属性,无论您是否有意。

下面是一个普通的unslot对象,它什么都不做,但是允许你添加属性:

class Unslotted:
    pass
test = Unslotted()
test.name = 'Fred'
test.Name = 'Wilma'

由于Python是区分大小写的,所以拼写相同但大小写不同的两个属性是不同的。如果你怀疑其中一个是打字错误,那就太倒霉了。

使用插槽,你可以限制它:

class Slotted:
    __slots__ = ('name')
    pass
test = Slotted()
test.name = 'Fred'      #   OK
test.Name = 'Wilma'     #   Error

这一次,第二个属性(Name)是不允许的,因为它不在__slots__集合中。

我建议在可能的情况下使用__slots__更好,以保持对对象的更多控制。

本质上,你没有使用__slots__。

当你认为你可能需要__slots__时,你实际上想要使用轻量级或Flyweight设计模式。在这些情况下,您不再希望使用纯Python对象。相反,您需要一个Python类对象的包装器来包装数组、结构体或numpy数组。

class Flyweight(object):

    def get(self, theData, index):
        return theData[index]

    def set(self, theData, index, value):
        theData[index]= value

类包装器没有属性——它只提供作用于底层数据的方法。方法可以简化为类方法。实际上,它可以简化为仅对底层数据数组进行操作的函数。