在Python中,目的是什么
__slots__
应该避免哪些情况?
太长,读不下去了
特殊属性
__slots__
允许您明确说明您希望对象实例具有哪些实例属性,并给出预期结果:
-
快
属性访问。
-
节省空间
在记忆中。
-
__dict__
.
-
否认
__dict__
__weakref__
如果父类拒绝它们并且您声明
__插槽__
.
快速警告
小警告,您应该只在继承树中声明一次特定的槽位。例如:
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'
最大的警告是多重继承——多个“具有非空槽的父类”不能组合在一起。
为了适应这一限制,请遵循最佳实践:排除除一个或所有父类之外的所有抽象,它们的具体类和新的具体类将共同继承这些抽象——给抽象空槽(就像标准库中的抽象基类一样)。
请参阅下面关于多重继承的部分以获取示例。
要求:
-
__插槽__
实际上存储在插槽中,而不是
字典
,类必须继承自
object
(在Python 3中是自动的,但在Python 2中必须是显式的)。
-
为了防止创建
字典
,你必须继承自
对象
继承中的所有类都必须声明
__插槽__
他们都不能有
'__dict__'
进入。
如果你想继续阅读,有很多细节。
为什么使用
__插槽__
:更快的属性访问。
Python的创建者Guido van Rossum,
states
__插槽__
以实现更快的属性访问。
证明明显更快的访问是微不足道的:
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%。
为什么使用
__插槽__
:节省内存
另一个目的
__插槽__
是为了减少每个对象实例占用的内存空间。
My own contribution to the documentation clearly states the reasons behind this
:
使用后节省的空间
字典
可能很重要。
SQLAlchemy attributes
节省大量内存
__插槽__
.
为了验证这一点,在Ubuntu Linux上使用Python 2.7的Anaconda发行版
guppy.hpy
(又名heapy)和
sys.getsizeof
,类实例的大小没有
__插槽__
声明的,而不是其他,是64个字节。确实如此
不
包括
字典
再次感谢Python的懒惰求值
字典
在被引用之前,它显然不会被调用为存在,但没有数据的类通常是无用的。当被召唤时
字典
属性至少为280字节。
__插槽__
宣布为
()
(无数据)只有16个字节,插槽中有一个项目,总共56个字节,有两个项目,64个字节。
对于64位Python,我以字节为单位说明了Python 2.7和3.6中的内存消耗
__插槽__
和
字典
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中的字典较小,但我们看到它有多好
__插槽__
缩放实例以节省内存,这是您想要使用的主要原因
__插槽__
.
为了使我的笔记更完整,请注意,在Python 2中,类名称空间中的每个插槽的一次性成本为64个字节,在Python 3中为72个字节,因为插槽使用像属性这样的数据描述符,称为“成员”。
>>> Foo.foo
<member 'foo' of 'Foo' objects>
>>> type(Foo.foo)
<class 'member_descriptor'>
>>> getsizeof(Foo.foo)
72
__插槽__
:
否认创造
字典
,您必须子类化
对象
所有子类
对象
在Python 3中,但在Python 2中,你必须明确:
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'
或者子类化另一个定义
__插槽__
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'
允许
字典
在创建子类化开槽对象的同时,只需添加
字典
到
__插槽__
(请注意,插槽是有序的,您不应该重复已经在父类中的插槽):
class SlottedWithDict(Child):
__slots__ = ('__dict__', 'b')
swd = SlottedWithDict()
swd.a = 'a'
swd.b = 'b'
swd.c = 'c'
和
>>> swd.__dict__
{'c': 'c'}
或者你甚至不需要申报
__插槽__
在你的子类中,你仍然会使用父类中的插槽,但不会限制创建
字典
:
class NoSlots(Child): pass
ns = NoSlots()
ns.a = 'a'
ns.b = 'b'
以及:
>>> ns.__dict__
{'b': 'b'}
然而,
__插槽__
可能会导致多重继承问题:
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
如果你遇到这个问题,你
可能会
只需删除
__插槽__
从父节点开始,或者如果你可以控制父节点,给它们空的插槽,或者重构为抽象:
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!
添加
字典
__插槽__
要获得动态分配:
class Foo(object):
__slots__ = 'bar', 'baz', '__dict__'
现在:
>>> foo = Foo()
>>> foo.boink = 'boink'
所以与
字典
在插槽中,我们失去了一些大小优势,但有动态分配的好处,而且仍然有我们期望的名称插槽。
当你从一个没有开槽的对象继承时,当你使用
__插槽__
-中的名称
__插槽__
指向开槽值,而任何其他值都放在实例的
字典
.
__插槽__
因为你想能够动态添加属性实际上并不是一个好理由——只需添加
"__dict__"
到你的
__插槽__
如果需要的话。
您可以类似地添加
__weakref__
__插槽__
如果你需要这个功能。
在对namedtuple进行子类化时设置为空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
因为我们阻止了
字典
:
>>> nt.quux = 'quux'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyNT' object has no attribute 'quux'
你
能
字典
__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
使用空
__插槽__
父母似乎提供了最大的灵活性,
(通过添加
字典
要获得动态分配,请参阅上文)
创建a
字典
:
class Foo(object): __slots__ = ()
class Bar(object): __slots__ = ()
class Baz(Foo, Bar): __slots__ = ('foo', 'bar')
b = Baz()
b.foo, b.bar = 'foo', 'bar'
你没有
有
要有插槽,所以如果你添加它们,稍后再删除它们,应该不会造成任何问题。
在这里冒险
如果你在作曲
mixins
或使用
abstract base classes
,不打算实例化,一个空
__插槽__
在这些方面,父母似乎是子群体灵活性的最佳选择。
为了演示,首先,让我们用我们想在多重继承下使用的代码创建一个类
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()
现在我们通过多重继承从两者中获得了功能,并且仍然可以拒绝
字典
和
__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等可变长度内置子类化,并且想向它们添加属性,请避免使用它们。
-
如果您坚持通过实例变量的类属性提供默认值,请避免使用它们。
__插槽__
documentation (the 3.7 dev docs are the most current)
我最近对此做出了重大贡献。
对其他答案的批评
目前排名靠前的答案引用了过时的信息,而且波浪很大,在一些重要方面没有达到标准。
不要“只使用
__插槽__
当实例化大量对象时”
我引述:
“你会想用
__插槽__
如果你要实例化同一类的大量(数百、数千)对象。"
例如,抽象基类
collections
模块尚未实例化
__插槽__
为他们宣布。
如果用户希望拒绝
字典
或
__weakref__
创建时,这些东西不能在父类中可用。
__插槽__
在创建接口或混入时有助于可重用性。
诚然,许多Python用户并不是为了可重用性而写作,但当你这样做的时候,有权拒绝不必要的空间使用是有价值的。
__插槽__
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>
>>> 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日)已接受答案的批评
第一段一半是简短的解释,一半是预测性的。这是唯一真正回答这个问题的部分
正确使用
__插槽__
下半部分是一厢情愿的想法,而且不切题:
虽然这有时是一个有用的优化,但如果Python解释器足够动态,只在对象实际添加时才需要字典,那么这是完全没有必要的。
Python实际上做了类似的事情,只是创建了
字典
当它被访问时,创建大量没有数据的对象是相当荒谬的。
第二段过于简单化,忽略了需要避免的实际原因
__插槽__
.下面是
不
避免插槽的真正原因(
实际的
原因,请参阅上面我的其余答案。):
它们改变了具有插槽的对象的行为,这可能会被控制狂和静态打字的小家伙滥用。
__插槽__
.
第三段更是一厢情愿。总的来说,这些内容大多不符合标准,回答者甚至没有撰写,并为该网站的批评者提供了弹药。
内存使用情况证据
创建一些普通对象和开槽对象:
>>> 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
...
访问常规对象及其
字典
并再次检查:
>>> 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的历史是一致的,从
Unifying types and classes in Python 2.2
如果你子类化了一个内置类型,额外的空间会自动添加到实例中以容纳
字典
和
__weakrefs__
(The
字典
在您使用它之前,它不会被初始化,因此您不应该担心您创建的每个实例的空字典所占用的空间。)如果你不需要这个额外的空间,你可以添加短语“
__slots__ = []
“去你的班。