在 Python 中 __slots__
的目的是什么——特别是关于我什么时候想使用它,什么时候不想使用它?
原文由 Jeb 发布,翻译遵循 CC BY-SA 4.0 许可协议
在 Python 中 __slots__
的目的是什么——特别是关于我什么时候想使用它,什么时候不想使用它?
原文由 Jeb 发布,翻译遵循 CC BY-SA 4.0 许可协议
引用 雅各布·哈伦的话:
正确使用
__slots__
是为了节省对象空间。不是有一个允许随时向对象添加属性的动态字典,而是有一个不允许在创建后添加的静态结构。 [这种使用__slots__
消除了每个对象一个字典的开销。] 虽然这有时是一个有用的优化,但如果 Python 解释器足够动态,它就完全没有必要了,它只需要字典当实际上有添加到对象时。不幸的是,插槽有副作用。它们以一种可以被控制狂和静态类型怪人滥用的方式改变具有插槽的对象的行为。这很糟糕,因为控制狂应该滥用元类,而静态类型专家应该滥用装饰器,因为在 Python 中,做某事应该只有一种明显的方法。
使 CPython 足够智能以处理节省空间而无需
__slots__
是一项重大任务,这可能是它不在 P3k 更改列表中的原因(目前)。原文由 Jeff Bauer 发布,翻译遵循 CC BY-SA 3.0 许可协议
2 回答5.3k 阅读✓ 已解决
2 回答1.2k 阅读✓ 已解决
4 回答1.5k 阅读✓ 已解决
3 回答1.4k 阅读✓ 已解决
3 回答1.3k 阅读✓ 已解决
2 回答960 阅读✓ 已解决
1 回答1.8k 阅读✓ 已解决
TLDR:
特殊属性
__slots__
允许您明确说明您希望对象实例具有哪些实例属性,以及预期的结果:空间节省来自
__dict__
。__dict__
和__weakref__
创建,如果父类拒绝它们并且您声明__slots__
。快速警告
小警告,你应该只在继承树中声明一个特定的插槽一次。例如:
当你弄错时 Python 不会反对(它可能应该),问题可能不会以其他方式表现出来,但你的对象将占用比它们应该占用的空间更多的空间。 Python 3.8:
这是因为 Base 的插槽描述符有一个与 Wrong 的插槽分开的插槽。这通常不应该出现,但它可以:
最大的警告是多重继承 - 不能组合多个“具有非空插槽的父类”。
为了适应这一限制,请遵循最佳实践:除一个或所有父类之外的所有抽象,他们的具体类和您的新具体类将共同从中继承 - 给抽象空槽(就像抽象基类中的标准库)。
有关示例,请参见下面的多重继承部分。
要求:
要使在
__slots__
中命名的属性实际存储在插槽中而不是__dict__
中,类必须继承自object
在 Python 2 中是显式的)。为了防止创建
__dict__
,您必须继承自object
并且继承中的所有类都必须声明__slots__
并且它们都不能有'__dict__'
条目。如果您想继续阅读,这里有很多细节。
为什么使用
__slots__
:更快的属性访问。Python 的创建者 Guido van Rossum 表示,他实际上创建了
__slots__
以实现更快的属性访问。证明可测量的显着更快的访问是微不足道的:
和
在 Ubuntu 上的 Python 3.5 中,时隙访问快了近 30%。
在 Windows 上的 Python 2 中,我测得它快了大约 15%。
为什么使用
__slots__
:内存节省__slots__
的另一个目的是减少每个对象实例占用的内存空间。我自己对文档的贡献清楚地说明了这背后的原因:
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,我以字节为单位说明了 Python 2.7 和 3.6 中的内存消耗,对于
__slots__
和__dict__
(没有定义插槽)对于字典在 3.6 中增长的每个点(除了对于 0、1 和 2 属性):因此,尽管 Python 3 中的字典较小,但我们看到
__slots__
可以很好地扩展实例以节省我们的内存,这是您想要使用__slots__
的主要原因。为了我的笔记的完整性,请注意在 Python 2 中类的命名空间中每个槽有一次性成本,在 Python 2 中为 64 字节,在 Python 3 中为 72 字节,因为槽使用称为“成员”的属性之类的数据描述符。
__slots__
的演示:要拒绝创建
__dict__
,您必须object
。所有子类object
在 Python 3 中,但在 Python 2 中你必须明确:现在:
或者子类化另一个类定义
__slots__
现在:
但:
要允许
__dict__
在对有槽对象进行子类化时创建,只需将'__dict__'
添加到__slots__
在父类中):和
或者你甚至不需要在你的子类中声明
__slots__
,你仍然会使用来自父母的插槽,但不限制创建__dict__
:和:
但是,
__slots__
可能会导致多重继承问题:因为从具有两个非空插槽的父类创建子类失败:
如果你遇到这个问题,你 可以 从父母那里删除
__slots__
,或者如果你可以控制父母,给他们空槽,或者重构为抽象:将
'__dict__'
添加到__slots__
以获得动态分配:现在:
因此,对于
'__dict__'
在插槽中,我们失去了一些大小优势,但动态分配的好处仍然是我们期望的名称插槽。当您从未开槽的对象继承时,使用
__slots__
中的名称会获得相同类型的语义__slots__
中的名称指向开槽值,而任何其他值放在实例的__dict__
中。避免
__slots__
因为你希望能够动态添加属性实际上不是一个好理由 - 如果需要,只需将"__dict__"
添加到你的__slots__
。如果您需要该功能,您可以类似地添加
__weakref__
到__slots__
显式。子类化命名元组时设置为空元组:
内置的 namedtuple 生成非常轻量级的不可变实例(本质上是元组的大小)但是为了获得好处,如果你对它们进行子类化,你需要自己做:
用法:
并尝试分配一个意外的属性引发
AttributeError
因为我们已经阻止了创建__dict__
:您 可以 允许
__dict__
__slots__ = ()
,但您不能将非空__slots__
与元组的子类型一起使用。最大的警告:多重继承
即使多个父级的非空插槽相同,它们也不能一起使用:
在父母中使用空的
__slots__
似乎提供了最大的灵活性, 允许孩子选择阻止或允许(通过添加'__dict__'
来获得动态分配,参见上一节) 创建的__dict__
:你不必 有 插槽 - 所以如果你添加它们,然后再删除它们,它应该不会造成任何问题。
在这里走出困境:如果你正在编写 mixins 或使用 抽象基类,它们不打算被实例化,一个空的
__slots__
在那些父母看来是最好的方式子类的灵活性。为了演示,首先,让我们创建一个类,其中包含我们希望在多重继承下使用的代码
我们可以通过继承和声明预期的插槽来直接使用上面的内容:
但我们不关心这个,这是简单的单一继承,我们需要另一个我们可能也继承的类,也许有一个嘈杂的属性:
现在如果两个基地都有非空槽,我们就不能做下面的事情。 (事实上 ,如果我们愿意,我们可以给出
AbstractBase
非空槽 a 和 b,并将它们留在下面的声明之外 - 将它们留在里面是错误的):现在我们通过多重继承获得了两者的功能,并且仍然可以拒绝
__dict__
和__weakref__
实例化:避免插槽的其他情况:
__class__
分配给另一个没有它们的类(并且你不能添加它们)时避免它们,除非插槽布局相同。 (我对了解谁在做这件事以及为什么这样做很感兴趣。)您可以从
__slots__
文档(3.7 开发文档是最新的) 的其余部分中梳理出更多注意事项,我最近对这些文档做出了重要贡献。对其他答案的批评
当前的最佳答案引用了过时的信息,并且相当手摇,并且在某些重要方面没有达到目标。
不要“仅在实例化大量对象时使用
__slots__
”我引用:
抽象基类,例如,来自
collections
模块,没有被实例化,但是__slots__
被声明为它们。为什么?
如果用户希望拒绝
__dict__
或__weakref__
创建,这些东西在父类中必须不可用。__slots__
有助于在创建接口或混合时的可重用性。的确,许多 Python 用户并不是为了可重用性而编写代码,但是当您这样做时,可以选择拒绝不必要的空间使用是很有价值的。
__slots__
不破坏酸洗酸洗开槽对象时,您可能会发现它会发出误导性的抱怨
TypeError
:这实际上是不正确的。此消息来自最旧的协议,这是默认协议。您可以使用
-1
参数选择最新的协议。在 Python 2.7 中,这将是2
(在 2.3 中引入),在 3.6 中它是4
。在 Python 2.7 中:
在 Python 3.6 中
所以我会记住这一点,因为这是一个已解决的问题。
对(截至 2016 年 10 月 2 日)已接受答案的批评
第一段一半是简短的解释,一半是预测。这是唯一真正回答问题的部分
后半部分是如意算盘,跑题了:
Python 实际上做了类似的事情,只在访问时创建
__dict__
,但是创建大量没有数据的对象是相当荒谬的。第二段过分简化并遗漏了避免的实际原因
__slots__
。以下 不是 避免插槽的真正原因( 实际 原因,请参阅上面我的其余回答。):然后继续讨论使用 Python 实现该错误目标的其他方法,而不是讨论与
__slots__
的任何事情。第三段更是一厢情愿。这些内容大多是回答者甚至没有创作的不合时宜的内容,并为该网站的批评者提供了弹药。
内存使用证据
创建一些普通对象和开槽对象:
实例化其中的一百万个:
用
guppy.hpy().heap()
检查:访问常规对象及其
__dict__
并再次检查:这与 Python 的历史是一致的,来自 Unifying types and classes in Python 2.2