7.4 KiB
sources, brief
| sources | brief | ||
|---|---|---|---|
|
Python __slots__ 用于限制实例属性集合,并可减少对象内存占用。 |
Python slots
__slots__ 是 Python 类中的一个特殊类属性,用于声明实例允许拥有的属性名称。它可以阻止对象动态添加未声明的属性,并让 Python 使用更紧凑的内部对象表示,从而减少内存占用。
该概念在 summaries/02_Classes_encapsulation 中作为 Python 封装机制的一部分出现,和 python encapsulation、python properties、python object memory 等主题密切相关。
基本含义
普通 Python 对象通常可以在运行时自由添加新属性:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
s = Stock('GOOG', 100, 490.10)
s.blah = 42 # 通常是允许的
这种灵活性是 Python 对象模型的一部分,但也可能带来问题:
- 拼写错误会意外创建新属性;
- 对象结构不够固定;
- 每个实例通常需要维护一个
__dict__,带来额外内存开销。
__slots__ 可以改变这种行为。
用法
在类中定义 __slots__,列出允许的实例属性名:
class Stock:
__slots__ = ('name', '_shares', 'price')
def __init__(self, name, shares, price):
self.name = name
self._shares = shares
self.price = price
这样,Stock 实例只能拥有 name、_shares 和 price 这些属性。
如果尝试设置未声明的属性,会抛出 AttributeError:
s = Stock('GOOG', 100, 490.10)
s.name # 'GOOG'
s.blah = 42 # AttributeError
在 summaries/02_Classes_encapsulation 中,示例展示了类似行为:
s.price = 385.15
s.prices = 410.2
# AttributeError: 'Stock' object has no attribute 'prices'
这里 prices 可能只是 price 的拼写错误。使用 __slots__ 后,这类错误会更早暴露。
与封装的关系
__slots__ 有时看起来像一种封装工具,因为它限制了对象可以拥有的属性集合。但需要注意:
- 它不是严格意义上的访问控制;
- 它不会让属性变成真正私有;
- 它主要限制“能添加哪些属性”,而不是“谁能访问这些属性”。
例如,即使 _shares 被写入 __slots__,外部代码仍然可以访问它:
s._shares
这与 Python 的整体封装哲学一致:Python 更多依赖命名约定,而不是强制私有权限。以下划线开头的属性,如 _shares,表示内部实现细节,应由程序员自觉避免直接访问。相关内容见 python encapsulation。
与 property 的配合
__slots__ 经常和 property 一起出现。例如,公开属性 shares 可以通过 property 管理,而真实数据存储在 _shares 中:
class Stock:
__slots__ = ('name', '_shares', 'price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
这里需要注意:
shares是一个 property,不是实际存储数据的普通实例属性;- 实际值保存在
_shares中; - 因此
__slots__中应包含_shares,而不是一定要包含shares; - 对
self.shares = value的赋值会触发 setter; - setter 再把验证后的值写入
self._shares。
这种组合体现了 Python 中常见的受管理属性模式,相关内容见 python properties 和 managed attributes。
对 dict 的影响
普通 Python 实例通常有一个实例字典 __dict__,用于保存动态属性:
s.__dict__
使用 __slots__ 后,如果没有显式把 __dict__ 加入 slots,实例通常不再拥有普通的 __dict__。这意味着属性不再通过实例字典动态存储,而是通过更固定、更紧凑的结构保存。
因此,在使用 __slots__ 的类上访问:
s.__dict__
通常会失败,或者显示该实例没有 __dict__。
这正是 __slots__ 能减少内存占用的原因之一。相关主题见 python object memory。
主要用途
__slots__ 的主要用途包括:
-
限制属性集合
防止外部或内部代码随意添加未声明属性。 -
发现拼写错误
例如把price错写成prices时,普通对象会创建新属性,而 slots 对象会报错。 -
节省内存
对大量实例组成的数据结构尤其有用。 -
略微提升性能
因为对象内部表示更紧凑,某些属性访问场景可能更高效。
不过,summaries/02_Classes_encapsulation 明确强调:__slots__ 最常见的用途是性能和内存优化,而不是普通日常代码中的封装工具。
适用场景
适合使用 __slots__ 的情况:
- 类被用作轻量数据结构;
- 程序会创建大量实例;
- 实例属性集合固定;
- 内存占用是重要问题;
- 希望避免动态添加属性造成的错误。
例如:
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
如果程序要创建数百万个 Point 对象,__slots__ 可能带来明显内存收益。
不适用场景
不建议使用 __slots__ 的情况:
- 普通业务类;
- 属性集合未来可能变化;
- 需要动态添加属性;
- 希望对象支持灵活调试或扩展;
- 没有明确的内存或性能压力。
过早使用 __slots__ 可能让类变得不够灵活,也可能给继承、调试和扩展带来额外复杂性。
与私有属性的区别
__slots__ 和私有属性约定是两个不同概念:
| 机制 | 作用 | 是否强制访问控制 |
|---|---|---|
_name |
表示内部实现细节 | 否 |
property |
管理属性访问、验证或计算 | 部分控制赋值逻辑 |
__slots__ |
限制实例属性集合、优化内存 | 否 |
例如:
class Stock:
__slots__ = ('name', '_shares', 'price')
这里 _shares 的下划线表示“内部使用”,而 __slots__ 表示实例只能拥有这些属性。二者可以配合使用,但含义不同。
关键注意点
__slots__是类属性,不是实例属性。- 它通常写成字符串元组,如
('name', '_shares', 'price')。 - 声明后,实例不能随意添加未列出的属性。
- 如果未包含
__dict__,实例通常没有普通实例字典。 - 它不等于私有属性,也不提供真正访问控制。
- 它最常用于大量小对象的内存优化。
- 日常代码中不应为了“看起来更封装”而滥用。
小结
__slots__ 是 Python 提供的一种限制实例属性和优化对象内存布局的机制。它可以让类的实例只拥有预先声明的属性,并避免普通实例字典带来的额外开销。虽然它能帮助发现属性拼写错误,也能让对象结构更固定,但其核心价值主要在性能和内存优化,而不是强制封装。
在设计类时,应优先考虑清晰的公共接口、合理的命名约定和必要的 property 管理;只有在属性集合稳定且实例数量巨大时,才应考虑使用 __slots__。
See also: summaries/05_Object_model__00_Overview