Python中__slots__
的目的是什么-尤其是关于何时以及何时不使用它的目的?
插槽对于库调用非常有用,以消除进行函数调用时的“命名方法分派”。 SWIG 文档中对此进行了提及。 对于想要减少使用插槽的通常称为函数的函数开销的高性能库,速度要快得多。
现在,这可能与OP问题没有直接关系。 它与构建扩展有关,而不是与在对象上使用slot语法有关。 但这确实有助于完整了解插槽的使用情况以及它们背后的一些原因。
类实例的属性具有3个属性:实例,属性名称和属性值。
在常规属性访问中 ,实例充当字典,而属性名称充当该字典中查找值的键。
instance(attribute)->值
在__slots__ access中 ,属性名称充当字典,实例充当字典中查找值的键。
属性(实例)->值
在flyweight模式中 ,属性的名称充当字典,而值充当该字典中查找实例的键。
attribute(value)->实例
在Python中,
__slots__
的目的是什么?在什么情况下应避免这种情况?
特殊属性__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不会反对(它应该会),否则问题可能不会显现出来,但是您的对象将比原先占用更多的空间。
>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(64, 80)
最大的警告是多重继承-无法合并多个“具有非空插槽的父类”。
为适应此限制,请遵循最佳实践:排除其父母和您的新具体类将分别继承的一个或所有父类的所有抽象-给这些抽象空的位置(就像在父类中的抽象基类一样)标准库)。
有关示例,请参见下面有关多重继承的部分。
要使在__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 3.5上的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,我将说明dict在3.6中增长的每个点的__slots__
和__dict__
(未定义插槽)的内存消耗(以字节为单位)以python 2.7和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中64字节,而在Python 3中为72字节,因为插槽使用数据描述符(如属性)称为“成员”。
>>> Foo.foo
>>> type(Foo.foo)
>>> getsizeof(Foo.foo)
72
__slots__
演示: 要拒绝创建__dict__
,必须子类化object
:
class Base(object):
__slots__ = ()
现在:
>>> b = Base()
>>> b.a = 'a'
Traceback (most recent call last):
File "", line 1, in
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 "", line 1, in
c.b = 'b'
AttributeError: 'Child' object has no attribute 'b'
要在创建插槽对象的子类时创建__dict__
,只需在__slots__
添加'__dict__'
(请注意,插槽是有序的,并且您不应重复父类中已经存在的插槽):
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 "", line 1, in
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使不可变的实例非常轻巧(本质上是元组的大小),但是要获得好处,如果您将它们子类化,则需要自己做:
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 "", line 1, in
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 "", line 1, in
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__
在这些父级中留空__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 "", line 1, in
AttributeError: 'Concretion' object has no attribute 'd'
__class__
分配时,请避免使用它们。 (我对了解谁在做什么以及为什么这样做很感兴趣。) 您也许可以从__slots__
文档的其余部分(最新的3.7 dev文档)中找出更多的警告,我最近做出了很大的贡献。
当前的最佳答案引用了过时的信息,而且非常容易波动,并且在某些重要方面未达到要求。
__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
这实际上是不正确的。 此消息来自最早的协议,这是默认协议。 您可以使用-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>
所以我会牢记这一点,因为这是一个已解决的问题。
第一段是一半简短的解释,一半是预测的。 这是真正回答问题的唯一部分
__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历史一致,来自Python 2.2中的Unifying类型和类。
如果您将内置类型作为子类,则多余的空间会自动添加到实例中,以容纳
__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 "", line 1, in
AttributeError: x
>>> pt.x = 1
>>> pt.x
1
>>> pt.z = 2
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Test' object has no attribute 'z'
>>> pt.__dict__
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Test' object has no attribute '__dict__'
>>> pt.__slots__
['x', 'y']
因此,要实现__slots__
,只需__slots__
一行(并且如果还没有,请把您的类变成一个新样式的类)。 这样,您可以将这些类的内存占用空间减少5倍 ,如果需要的话,则必须编写自定义的pickle代码。
最初的问题是关于一般用例,而不仅仅是内存。 因此,在这里应该提到的是,在实例化大量对象时,您也会获得更好的性能 -有趣的是,例如,将大型文档解析为对象或从数据库中解析时。
这是使用插槽和不使用插槽创建具有一百万个条目的对象树的比较。 作为参考,在对树使用普通字典时的性能(在OSX上为Py2.7.10):
********** RUN 1 **********
1.96036410332
3.02922606468
2.90828204155 dict
********** RUN 2 **********
1.77050495148
3.10655999184
2.84120798111 dict
********** RUN 3 **********
1.84069895744
3.21540498734
2.59615707397 dict
********** RUN 4 **********
1.75041103363
3.17366290092
2.70941114426 dict
测试类(ident,来自插槽的appart):
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)
测试代码,详细模式:
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
__slot__
属性的一个非常简单的示例。
__slots__
如果我的__slot__
中没有__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是否有成千上万个此类对象? 为每个对象创建一个额外的属性dict
将导致我的代码中大量的开销(内存,计算能力等)。
__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__
另一种晦涩用法是从ProxyTypes包(以前是PEAK项目的一部分)中向对象代理添加属性。 它的ObjectWrapper
允许您代理另一个对象,但是拦截与代理对象的所有交互。 它不是很常用(并且没有Python 3支持),但是我们已经使用它来实现基于龙卷风的异步实现周围的线程安全的阻塞包装,该龙卷风使用线程安全来反弹通过ioloop对代理对象的所有访问concurrent.Future
对象用于同步并返回结果。
默认情况下,对代理对象的任何属性访问都将为您提供代理对象的结果。 如果需要在代理对象上添加属性,则可以使用__slots__
。
from peak.util.proxies import ObjectWrapper
class Original(object):
def __init__(self):
self.name = 'The Original'
class ProxyOriginal(ObjectWrapper):
__slots__ = ['proxy_name']
def __init__(self, subject, proxy_name):
# proxy_info attributed added directly to the
# Original instance, not the ProxyOriginal instance
self.proxy_info = 'You are proxied by {}'.format(proxy_name)
# proxy_name added to ProxyOriginal instance, since it is
# defined in __slots__
self.proxy_name = proxy_name
super(ProxyOriginal, self).__init__(subject)
if __name__ == "__main__":
original = Original()
proxy = ProxyOriginal(original, 'Proxy Overlord')
# Both statements print "The Original"
print "original.name: ", original.name
print "proxy.name: ", proxy.name
# Both statements below print
# "You are proxied by Proxy Overlord", since the ProxyOriginal
# __init__ sets it to the original object
print "original.proxy_info: ", original.proxy_info
print "proxy.proxy_info: ", proxy.proxy_info
# prints "Proxy Overlord"
print "proxy.proxy_name: ", proxy.proxy_name
# Raises AttributeError since proxy_name is only set on
# the proxy object
print "original.proxy_name: ", proxy.proxy_name
如果要实例化(数百个,数千个)同一类的对象,则需要使用__slots__
。 __slots__
仅作为内存优化工具存在。
强烈建议不要使用__slots__
约束属性的创建。
使用__slots__
腌制对象不能使用默认的(最早的)腌制协议。 有必要指定一个更高的版本。
python的其他一些自省功能也可能受到不利影响。
引用雅各布·哈伦 ( Jacob Hallen)的话 :
__slots__
的正确用法是节省对象空间。 静态结构不允许在创建后添加对象,而不是具有允许随时向对象添加属性的动态字典。 [__slots__
使用消除了每个对象的dict的开销。]尽管这有时是一个有用的优化,但是如果Python解释器足够动态,因此仅在实际添加了dict时才需要dict,则完全没有必要。物体。不幸的是,插槽有副作用。 它们以一种可被控制怪胎和静态类型临时表滥用的方式更改具有插槽的对象的行为。 这是不好的,因为控制怪胎应该滥用元类,而静态类型之间应该滥用装饰器,因为在Python中,应该只有一种明显的方法。
使CPython足够聪明以在不使用
__slots__
情况下处理节省的空间是一项重大任务,这可能就是为什么它不在P3k更改列表中的原因(至今)。
基本上__slots__
没有用。
因为当你认为你可能需要的时间__slots__
,你实际上要使用轻量或次最轻量级设计模式。 在这些情况下,您不再希望使用纯Python对象。 相反,您希望在数组,结构或numpy数组周围使用类似Python对象的包装器。
class Flyweight(object):
def get(self, theData, index):
return theData[index]
def set(self, theData, index, value):
theData[index]= value
类似于类的包装器没有属性-它仅提供对基础数据起作用的方法。 这些方法可以简化为类方法。 实际上,可以将其简化为仅对底层数据数组进行操作的函数。
每个python对象都有一个__dict__
属性,这是一个包含所有其他属性的字典。 例如,当您键入self.attr
python实际上正在执行self.__dict__['attr']
。 您可以想象使用字典存储属性会花费一些额外的空间和时间来访问它。
但是,当您使用__slots__
,为该类创建的任何对象都不会具有__dict__
属性。 相反,所有属性访问都直接通过指针进行。
因此,如果要使用C样式结构而不是完整的类,可以使用__slots__
压缩对象的大小并减少属性访问时间。 一个很好的例子是一个包含属性x&y的Point类。 如果您有很多要点,可以尝试使用__slots__
来节省一些内存。