引言: 在 Python 面向对象编程中,__slots__是一个易被忽视但极具实用价值的特性 ------ 它既能大幅减少类实例的内存占用,又能严格管控实例可拥有的属性,避免意外的属性新增。本文聚焦__slots__这一核心知识点,从底层原理、使用方式到性能优化实战,带你掌握这一提升 Python 程序效率的关键技巧。
一、__slots__的核心背景:Python 实例的默认存储方式
Python 中普通类的实例默认使用字典(__dict__) 存储属性,字典的优势是灵活(可随时新增 / 删除属性),但缺点也很明显:
- 内存占用高:字典为了哈希表的高效查找,会预留大量空闲空间,单个实例的内存开销远大于实际存储的属性数据;
- 属性无管控 :可随意给实例新增属性,容易因拼写错误(如把
name写成naem)引入难以排查的 bug。
python
# 普通类实例的属性存储方式
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p = Person("Alice", 25)
# 实例的属性存储在__dict__字典中
print(p.__dict__) # 输出:{'name': 'Alice', 'age': 25}
# 可随意新增属性(灵活性高,但易出错)
p.gender = "female"
print(p.__dict__) # 输出:{'name': 'Alice', 'age': 25, 'gender': 'female'}
而__slots__的核心作用,就是用固定大小的数组替代字典存储属性,解决上述两个问题。
二、__slots__的基础使用
1. 基本定义与效果
__slots__是类级别的属性,需定义为字符串列表 / 元组,列出实例允许拥有的所有属性名。定义后:
- 实例只能拥有
__slots__中指定的属性,无法新增其他属性; - 实例不再生成
__dict__(节省内存); - 实例的属性存储在数组中,访问速度更快。
python
class Person:
# 定义slots,指定实例仅允许拥有name和age属性
__slots__ = ("name", "age")
def __init__(self, name, age):
self.name = name
self.age = age
# 正常使用指定的属性
p = Person("Bob", 30)
print(p.name) # 输出:Bob
print(p.age) # 输出:30
# 尝试新增未在slots中定义的属性,会报错
try:
p.gender = "male"
except AttributeError as e:
print(e) # 输出:'Person' object has no attribute 'gender'
# 实例不再有__dict__属性
try:
print(p.__dict__)
except AttributeError as e:
print(e) # 输出:'Person' object has no attribute '__dict__'
2. 多继承场景下的__slots__
若子类继承了多个父类,子类的__slots__会合并所有父类的__slots__,但需注意:
- 子类未定义
__slots__时,实例会恢复__dict__,父类的__slots__失效; - 子类定义
__slots__时,实例的属性范围为 "子类 slots + 所有父类 slots"。
python
# 父类1
class Parent1:
__slots__ = ("a", "b")
# 父类2
class Parent2:
__slots__ = ("b", "c")
# 子类:定义自己的slots
class Child(Parent1, Parent2):
__slots__ = ("d",) # 合并后允许的属性:a、b、c、d
c = Child()
c.a = 1
c.b = 2
c.c = 3
c.d = 4
print(c.a, c.b, c.c, c.d) # 输出:1 2 3 4
# 子类:未定义slots(父类slots失效)
class Child2(Parent1):
pass
c2 = Child2()
c2.a = 1
c2.xxx = 999 # 可新增任意属性
print(c2.xxx) # 输出:999
print(c2.__dict__) # 输出:{'xxx': 999}
三、__slots__的核心价值:内存与性能优化
1. 内存优化实战
对于需要创建大量实例的场景(如数据处理、爬虫存储),__slots__的内存优化效果极其显著。
python
import sys
# 普通类(无slots)
class NormalClass:
def __init__(self, x, y):
self.x = x
self.y = y
# 带slots的类
class SlotsClass:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
# 测试单个实例的内存占用
n = NormalClass(1, 2)
s = SlotsClass(1, 2)
print(f"普通实例内存占用:{sys.getsizeof(n) + sys.getsizeof(n.__dict__)} 字节")
# 输出约:56(实例本身) + 48(__dict__)= 104 字节
print(f"Slots实例内存占用:{sys.getsizeof(s)} 字节")
# 输出约:48 字节(无__dict__,仅存储x、y)
# 测试10万个实例的内存占用
import memory_profiler
@memory_profiler.profile
def create_normal_instances():
# 创建10万个普通实例
instances = [NormalClass(i, i+1) for i in range(100000)]
return instances
@memory_profiler.profile
def create_slots_instances():
# 创建10万个slots实例
instances = [SlotsClass(i, i+1) for i in range(100000)]
return instances
# 执行测试(运行后可看到内存使用差异)
# create_normal_instances() # 约占用80MB+内存
# create_slots_instances() # 约占用40MB+内存(节省约50%)
结论 :带__slots__的类实例,内存占用通常能减少 50% 以上,实例数量越多,优化效果越明显。
2. 访问性能优化
由于__slots__用数组存储属性(直接通过索引访问),而非字典(哈希查找),属性访问速度也会小幅提升。
python
import timeit
# 测试属性访问速度
setup = """
class NormalClass:
def __init__(self, x):
self.x = x
class SlotsClass:
__slots__ = ("x",)
def __init__(self, x):
self.x = x
n = NormalClass(10)
s = SlotsClass(10)
"""
# 测试普通实例访问属性
normal_time = timeit.timeit("n.x", setup=setup, number=10000000)
# 测试slots实例访问属性
slots_time = timeit.timeit("s.x", setup=setup, number=10000000)
print(f"普通实例访问耗时:{normal_time:.2f} 秒")
print(f"Slots实例访问耗时:{slots_time:.2f} 秒")
# 输出示例:普通实例耗时1.20秒,Slots实例耗时0.80秒(快约30%)
四、__slots__的使用注意事项
1. 保留特殊属性
若需要实例保留__dict__或__weakref__(弱引用),需在__slots__中显式声明:
python
class Person:
# 保留__dict__(允许新增属性) + 管控指定属性
__slots__ = ("name", "age", "__dict__")
p = Person("Alice", 25)
p.gender = "female" # 可新增属性(因__dict__存在)
print(p.gender) # 输出:female
print(p.__dict__) # 输出:{'gender': 'female'}
2. 不可变属性的实现
若需要属性只读,可结合property和slots:
python
class ReadOnlyClass:
__slots__ = ("_x",) # 私有属性存储值
def __init__(self, x):
self._x = x
@property
def x(self):
return self._x
roc = ReadOnlyClass(10)
print(roc.x) # 输出:10
# 尝试修改x会报错(无setter)
try:
roc.x = 20
except AttributeError as e:
print(e) # 输出:can't set attribute
# 尝试直接修改私有属性也会报错(slots仅管控_x,但若命名为私有,外部不应访问)
try:
roc._x = 20
except AttributeError as e:
print(e) # 输出:'ReadOnlyClass' object attribute '_x' is read-only
3. 适用场景与局限性
适用场景:
- 需要创建大量实例的类(如数据模型、缓存对象);
- 对内存占用敏感的场景(如嵌入式 Python、低内存服务器);
- 需要严格管控属性,避免意外新增属性的场景。
局限性:
- 灵活性降低:无法动态新增属性(除非显式声明
__dict__); - 仅影响实例:
__slots__不限制类属性的新增; - 继承需注意:子类未定义
__slots__会恢复__dict__,导致父类优化失效。
总结
__slots__是类级别的属性,用于指定实例允许拥有的属性列表,核心作用是替代__dict__存储属性;- 主要优势:大幅减少实例内存占用(通常 50% 以上)、小幅提升属性访问速度、严格管控实例属性;
- 使用要点:多继承时会合并父类 slots,需保留__dict__可允许动态新增属性,结合 property 可实现只读属性;
- 适用场景:大量实例创建、内存敏感场景、属性管控需求,普通小体量类无需过度使用