相信不少 Python 开发者都听说过 __slots__,知道它可以帮助节省内存。但你是否思考过它背后的原理,以及在实际开发中的其他妙用?让我们一起深入探讨。
从一个性能问题说起
假设你的一个系统需要处理大量的订单对象:
class Order:
def __init__(self, order_id, symbol, price, quantity):
self.order_id = order_id
self.symbol = symbol
self.price = price
self.quantity = quantity
# 创建100万个订单对象
orders = [Order(i, "BTC", 30000, 1) for i in range(1_000_000)]
看起来很普通的代码,但当你用内存分析工具一看,这些对象占用的内存可能远超预期。为什么?
__dict__ 的开销
在 Python 中,普通类的实例属性都存储在 __dict__ 字典中。这种设计非常灵活,允许我们动态添加属性:
order = Order(1, "BTC", 30000, 1)
order.new_field = "动态添加的字段" # 完全合法
但这种灵活性是有代价的:
- 每个实例都要维护一个字典
- 字典本身为了支持快速查找,会预分配一定的空间
- 字典的开销在对象数量大时会累积成可观的内存消耗
__slots__ 登场
让我们改造一下 Order 类:
class Order:
__slots__ = ['order_id', 'symbol', 'price', 'quantity']
def __init__(self, order_id, symbol, price, quantity):
self.order_id = order_id
self.symbol = symbol
self.price = price
self.quantity = quantity
这个改动带来了什么变化?
- 内存占用显著降低(通常可以节省 30% 到 50% 的内存)
- 属性访问速度提升(因为不需要字典查找)
- 代码更加"显式",所有可能的属性一目了然
__slots__ 的工作原理
当我们使用 __slots__ 时,Python 会:
- 在类级别创建一个固定的内存布局,类似 C 语言中的结构体
- 不再为实例创建 __dict__ 和 __weakref__ 属性(除非显式添加到 __slots__ 中)
- 将属性直接存储在预分配的固定大小的数组中,而不是字典里
这带来了两个直接的好处:
- 属性访问更快:直接通过数组偏移量访问,不需要哈希查找
- 内存占用更少:
没有 __dict__ 的开销(每个实例至少节省一个字典的内存)
属性存储更紧凑(类似 C 结构体)
没有哈希表的空间预留
让我们用代码验证这些优势:
import sys
import time
import tracemalloc
class OrderWithDict:
def __init__(self, order_id, symbol, price, quantity):
self.order_id = order_id
self.symbol = symbol
self.price = price
self.quantity = quantity
class OrderWithSlots:
__slots__ = ['order_id', 'symbol', 'price', 'quantity']
def __init__(self, order_id, symbol, price, quantity):
self.order_id = order_id
self.symbol = symbol
self.price = price
self.quantity = quantity
def measure_memory_and_speed(cls, n_objects=1_000_000):
# 启动内存跟踪
tracemalloc.start()
# 创建对象
start_time = time.time()
objects = [cls(i, "BTC", 30000, 1) for i in range(n_objects)]
creation_time = time.time() - start_time
# 测量内存
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
# 测试属性访问速度
start_time = time.time()
for obj in objects:
_ = obj.order_id
_ = obj.symbol
_ = obj.price
_ = obj.quantity
access_time = time.time() - start_time
return {
"内存占用(MB)": peak / 1024 / 1024,
"对象创建时间(秒)": creation_time,
"属性访问时间(秒)": access_time
}
def main():
# 测试普通类
print("测试普通类:")
dict_results = measure_memory_and_speed(OrderWithDict)
for k, v in dict_results.items():
print(f"{k}: {v:.2f}")
print("\n测试使用 __slots__ 的类:")
slots_results = measure_memory_and_speed(OrderWithSlots)
for k, v in slots_results.items():
print(f"{k}: {v:.2f}")
# 计算差异百分比
print("\n性能提升:")
for k in dict_results:
improvement = (dict_results[k] - slots_results[k]) / dict_results[k] * 100
print(f"{k}: 提升 {improvement:.1f}%")
# 展示单个对象的大小差异
normal_obj = OrderWithDict(1, "BTC", 30000, 1)
slots_obj = OrderWithSlots(1, "BTC", 30000, 1)
print(f"\n单个对象大小对比:")
print(f"普通对象: {sys.getsizeof(normal_obj)} bytes")
print(f"普通对象的__dict__: {sys.getsizeof(normal_obj.__dict__)} bytes")
print(f"普通对象总大小: {sys.getsizeof(normal_obj) + sys.getsizeof(normal_obj.__dict__)} bytes")
print(f"Slots对象: {sys.getsizeof(slots_obj)} bytes")
try:
print(f"Slots对象的__dict__: {sys.getsizeof(slots_obj.__dict__)} bytes")
except AttributeError as e:
print(f"Slots对象没有__dict__属性:{e}")
if __name__ == "__main__":
main()
输出如下:
测试普通类:
内存占用(MB): 179.71
对象创建时间(秒): 1.08
属性访问时间(秒): 0.08
测试使用 __slots__ 的类:
内存占用(MB): 95.79
对象创建时间(秒): 0.67
属性访问时间(秒): 0.07
性能提升:
内存占用(MB): 提升 46.7%
对象创建时间(秒): 提升 37.5%
属性访问时间(秒): 提升 4.8%
单个对象大小对比:
普通对象: 48 bytes
普通对象的__dict__: 104 bytes
普通对象总大小: 152 bytes
Slots对象: 64 bytes
Slots对象没有__dict__属性:'OrderWithSlots' object has no attribute '__dict__'
这里注意到,使用了 __slots__ 的类没有 __dict__ 属性,这是因为它的属性是直接存储在数组中的。此外,直接对对象进行 sizeof 操作,是不包含其 __dict__ 的大小的。
当我们使用 sys.getsizeof() 测量单个对象大小时,它只返回对象的直接内存占用,而不包括其引用的其他对象(如 __dict__ 中存储的值)的大小。
不止于节省内存
__slots__ 除了优化性能,还能帮助我们写出更好的代码:
1. 接口契约
__slots__ 实际上定义了一个隐式的接口契约,明确告诉其他开发者,“这个类就这些属性,不多不少”:
class Position:
__slots__ = ['symbol', 'quantity']
def __init__(self, symbol, quantity):
self.symbol = symbol
self.quantity = quantity
这比写文档更有效 - 代码本身就是最好的文档。
2. 防止拼写错误
position = Position("BTC", 100)
position.quantiy = 200 # 拼写错误,会立即抛出 AttributeError
如果没有 __slots__,这个错误可能潜伏很久才被发现。
3. 更好的封装
__slots__ 天然地限制了属性的随意添加,这促使我们思考类的设计是否合理:
class Account:
__slots__ = ['id', 'balance', '_transactions']
def __init__(self, id):
self.id = id
self.balance = 0
self._transactions = []
def add_transaction(self, amount):
self._transactions.append(amount)
self.balance += amount
__slots__ vs @dataclass:该用谁?
既然都是用于数据类的定义,@dataclass 和 __slots__ 是什么关系?让我们先看一个例子:
from dataclasses import dataclass
# 普通dataclass
@dataclass
class TradeNormal:
symbol: str
price: float
quantity: int
# 带slots的dataclass
@dataclass
class TradeWithSlots:
__slots__ = ['symbol', 'price', 'quantity']
symbol: str
price: float
quantity: int
# 结合使用的推荐方式
@dataclass(slots=True) # Python 3.10+
class TradeModern:
symbol: str
price: float
quantity: int
关键点解析:
- 默认情况:@dataclass 装饰器默认不会使用 __slots__,每个实例依然会创建 __dict__
- Python 3.10的改进:引入了 slots=True 参数,可以自动为 dataclass 启用 __slots__
- 动态添加属性的陷阱:
@dataclass
class Trade:
symbol: str
price: float
trade = Trade("BTC", 30000)
trade.quantity = 1 # 可以,但会创建 __dict__
@dataclass(slots=True)
class TradeLocked:
symbol: str
price: float
trade_locked = TradeLocked("BTC", 30000)
trade_locked.quantity = 1 # AttributeError!
最佳实践:@dataclass 和 __slots__ 的协同使用
- Python 3.10+ 的推荐用法:
@dataclass(slots=True, frozen=True)
class Position:
symbol: str
quantity: int
- 早期Python版本的替代方案:
@dataclass
class Position:
__slots__ = ['symbol', 'quantity']
symbol: str
quantity: int
如何选择?
- 使用 @dataclass(slots=True) 的场景:
类的属性在定义后不会改变
需要类型提示和自动生成方法
Python 3.10+环境
注重内存效率
- 使用普通 @dataclass 的场景:
需要动态添加属性
使用了某些需要 __dict__ 的库(如某些ORM)
Python 3.10以下版本
开发阶段,类的结构还在调整
- 直接使用 __slots__ 的场景:
极致的性能要求
类的结构非常简单
不需要dataclass提供的额外功能
注意事项和提示
- 继承关系:
@dataclass(slots=True)
class Parent:
x: int
@dataclass(slots=True)
class Child(Parent):
y: int
# Child会自动继承Parent的slots
- 动态属性检查:
@dataclass(slots=True)
class Trade:
symbol: str
def __setattr__(self, name, value):
if name not in self.__slots__:
raise AttributeError(f"Cannot add new attribute '{name}'")
super().__setattr__(name, value)
此外,某些涉及动态属性的特性会受限:
class Frozen:
__slots__ = ['x']
obj = Frozen()
# 以下操作将不可用:
# vars(obj) # TypeError: vars() argument must have __dict__ attribute
# setattr(obj, 'y', 1) # AttributeError
- 性能优化建议:
如果确定类的结构不会改变,优先使用 @dataclass(slots=True)
在性能关键的代码路径上,考虑使用性能分析工具验证收益
数据类(如 DTO)且实例数量大时,用 __slots__ 是个好选择
如果类的属性集合是确定的,使用 __slots__ 可以获得更好的代码质量
记住:过早优化是万恶之源,先保证代码正确性和可维护性
总结
__slots__ 不仅仅是一个性能优化工具,它还能帮助我们写出更清晰、更健壮的代码。在设计数据密集型应用时,合理使用 __slots__ 可以同时获得性能和代码质量的提升。
实际工作中,可以先写普通的类,当发现性能瓶颈或需要更严格的属性控制时,再考虑引入 __slots__。毕竟,过早优化是万恶之源,而 __slots__ 的使用也确实会带来一些灵活性的损失。