在Python中__slots__的目的是什么——特别是当我想要使用它时,什么时候不使用它?
当前回答
除了其他答案,__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__仅作为内存优化工具存在。
强烈建议使用__slots__来约束属性创建。
使用__slots__ pickle对象将无法使用默认的(最古老的)pickle协议;有必要指定一个更高的版本。
python的其他一些自省特性也可能受到不利影响。
除了其他答案,__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__更好,以保持对对象的更多控制。
在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__ =[]"。
引用雅各布·海伦的话:
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 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
"""
推荐文章
- Python glob多个文件类型
- 如何可靠地打开与当前运行脚本在同一目录下的文件
- Python csv字符串到数组
- 如何将类标记为已弃用?
- 如何在Python中进行热编码?
- 如何嵌入HTML到IPython输出?
- 在Python生成器上使用“send”函数的目的是什么?
- 是否可以将已编译的.pyc文件反编译为.py文件?
- Django模型表单对象的自动创建日期
- 在Python中包装长行
- 如何计算两个时间串之间的时间间隔
- 我如何才能找到一个Python函数的参数的数量?
- getter和setter是糟糕的设计吗?相互矛盾的建议
- 您可以使用生成器函数来做什么?
- 将Python诗歌与Docker集成