在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)
这是因为基本的插槽描述符的插槽与错误的插槽分开。通常不应该这样,但是可以:
>>> 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'
最大的警告是多重继承-无法将多个“具有非空插槽的父类”组合在一起。
为适应此限制,请遵循最佳实践:排除所有父母的抽象,而它们的具体类和你的新具体类将共同继承该父类-给这些抽象空的位置(就像父类中的抽象基类一样)标准库)。
有关示例,请参见下面有关多重继承的部分。
要求:
要使名为in的属性
__slots__实际上存储在插槽中而不是存储在插槽中
__dict__,则类必须从继承
object。
为防止创建
__dict__,你必须继承,object并且继承中的所有类都必须声明,
__slots__并且它们都不能具有
'__dict__'条目。
如果你想继续阅读,有很多细节。
为什么使用__slots__:更快的属性访问。
Python的创建者
Guido van Rossum指出,他实际上是
__slots__为了更快地访问属性而创建的。
证明可观的显着更快访问是微不足道的:
import timeitclass Foo(object): __slots__ = 'foo',class Bar(object): passslotted = 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.28468345299916111.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中增长的每个点(对于0和1和2属性除外),对于Python 2.7和3.6,对于
__slots__和
__dict__(未定义插槽),以字节为单位说明了内存消耗:
Python 2.7 Python 3.6attrs __slots__ __dict__* __slots__ __dict__* | *(no slots defined)none 16 56 + 272† 16 56 + 112† | †if __dict__ referencedone 48 56 + 272 48 56 + 112two 56 56 + 272 56 56 + 112six 88 56 + 1040 88 56 + 15211 128 56 + 1040 128 56 + 24022 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__创建插槽对象的子类的子类时允许创建,只需添加
'__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): passns = 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 ABCclass 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时,设置为空
tuple:
内置
namedtuple使不可变的实例非常轻巧(本质上是元组的大小),但是要获得好处,如果你将它们子类化,则需要自己做:
from collections import namedtupleclass 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__通过设置off 来允许创建__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): passTraceback (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'你不具备有槽-因此,如果你添加它们,后来删除它们,它不应引起任何问题。
走出放在这里肢体:如果你撰写的混入或使用抽象基类,它不打算被实例化,空
__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 = csetting c!>>> c.cgetting 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模块的抽象基类未实例化,但
__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 2.2中的统一类型和类中的Python历史一致
如果你将内置类型作为子类,则多余的空间会自动添加到实例中以容纳__dict__和__weakrefs__。(dict__尽管直到使用完,它才会被初始化,因此你不必担心空字典为你创建的每个实例所占用的空间。)如果不需要此多余的空间,可以在短语中添加“ __slots = []”你的班。



