在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
类包装器没有属性——它只提供作用于底层数据的方法。方法可以简化为类方法。实际上,它可以简化为仅对底层数据数组进行操作的函数。
推荐文章
- 从matplotlib中的颜色映射中获取单个颜色
- 虚拟方法和抽象方法的区别
- 将Pandas或Numpy Nan替换为None以用于MysqlDB
- 使用pandas对同一列进行多个聚合
- 使用Python解析HTML
- django MultiValueDictKeyError错误,我如何处理它
- 如何在for循环期间修改列表条目?
- 我如何在Django中创建一个鼻涕虫?
- 合并两个PHP对象的最佳方法是什么?
- 没有名为'django.core.urlresolvers'的模块
- 蟒蛇导出环境文件
- Django - makemigrations -未检测到任何更改
- SQLAlchemy:引擎、连接和会话差异
- 在Python Pandas中删除多个列中的所有重复行
- 更改pandas DataFrame中的特定列名