Python __slots__:给实例的"属性背包"加上拉链
引言:从"随便装"到"只能装规定物品"
默认情况下,Python 实例就像一个没有拉链的背包——你可以随时往里塞任何东西(属性、方法),想塞多少塞多少。这种灵活性很爽,但也容易出问题:
- 手一滑把
name写成nmae,程序不报错,但数据悄悄"丢了"; - 创建几十万个小对象时,每个对象都背着一个"万能背包",内存浪费严重。
__slots__ 就是给这个背包加上拉链和固定格子——只允许装指定的物品,多一件都不行。
一、默认行为:实例是"自由背包"
1.1 动态绑定属性
class Student(object):
pass
s = Student()
s.name = 'Michael' # 动态给实例绑定一个属性
print(s.name) # Michael实例创建后,可以随时"贴标签"——s 这个背包里多了 name 这个格子。
1.2 动态绑定方法
def set_age(self, age):
self.age = age
from types import MethodType
s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
s.set_age(25)
print(s.age) # 251.3 关键:实例绑定的方法,对其他实例无效
s2 = Student()
s2.set_age(25) # AttributeError: 'Student' object has no attribute 'set_age'生活化理解:你给自己的背包缝了个小口袋(set_age),别人的背包并没有——各自独立。
1.4 给类绑定方法:所有实例共享
def set_score(self, score):
self.score = score
Student.set_score = set_score # 给类绑定方法
s.set_score(100)
print(s.score) # 100
s2.set_score(99)
print(s2.score) # 99生活化理解:给全班统一发了一种新文具,人手一份。
二、__slots__:限制实例能装什么
2.1 基本用法
class Student(object):
__slots__ = ('name', 'age') # 用 tuple 定义允许绑定的属性名称测试:
s = Student()
s.name = 'Michael' # ✅ 允许
s.age = 25 # ✅ 允许
s.score = 99 # ❌ AttributeError: 'Student' object has no attribute 'score'生活化理解:__slots__ 就像给背包缝了固定数量和标签的格子——name 格和 age 格。想塞 score?没这个格子,直接卡住。
2.2 为什么要有 __slots__?
| 场景 | 不用 __slots__ | 用 __slots__ |
|---|---|---|
| 拼写错误 | s.nmae = 'x' 静默成功,后续 s.name 找不到 | 立即报错,快速定位 bug |
| 内存占用 | 每个实例自带一个 __dict__ 字典,灵活但重 | 没有 __dict__,内存大幅节省 |
| 属性可控性 | 任何人都能乱加属性 | 严格白名单,接口清晰 |
内存对比(简化理解):
class WithoutSlots(object):
pass
class WithSlots(object):
__slots__ = ('name', 'age')
import sys
w1 = WithoutSlots()
w1.name = 'A'
w1.age = 20
w2 = WithSlots()
w2.name = 'A'
w2.age = 20
# 实际项目中创建 10 万个实例时,WithSlots 可能节省 40%+ 内存三、__slots__ 的继承规则:子类不自动继承限制
3.1 父类的 __slots__ 对子类无效
class Student(object):
__slots__ = ('name', 'age')
class GraduateStudent(Student):
pass
g = GraduateStudent()
g.score = 9999 # ✅ 居然成功了!生活化理解:父亲给自己背包加了拉链,但儿子出生时默认还是无拉链背包——父亲的限制管不到儿子。
3.2 子类如何真正受限?
子类必须自己再定义一次 __slots__:
class GraduateStudent(Student):
__slots__ = ('school',) # 子类自己的格子
g = GraduateStudent()
g.name = 'Michael' # ✅ 继承自父类的格子
g.age = 25 # ✅ 继承自父类的格子
g.school = 'MIT' # ✅ 子类自己的格子
g.score = 99 # ❌ 还是不行规则:子类实例允许的属性 = 自身的 __slots__ + 父类的 __slots__。
四、知识链条:从动态绑定到 __slots__
Python 动态语言特性
↓
实例可以随时绑定任意属性/方法(自由背包)
↓
灵活但带来问题:拼写错误难发现、内存浪费、接口不可控
↓
解决方案:__slots__ 定义白名单
↓
限制实例属性 + 节省内存(无 __dict__)
↓
注意:__slots__ 只对当前类有效,子类需重新定义五、常见误区与避坑指南
5.1 误区一:以为 __slots__ 能限制类属性
class Student(object):
__slots__ = ('name', 'age')
count = 0 # 类属性,不受 __slots__ 限制
Student.count = 100 # ✅ 正常
s = Student()
print(s.count) # 100__slots__ 只限制实例属性,类属性随便改。
5.2 误区二:在 __slots__ 里写方法名
class Student(object):
__slots__ = ('name', 'set_age') # ❌ 错误!
def set_age(self, age):
self.age = age__slots__ 只能写数据属性,不能写方法。方法定义在类里,不占实例的格子。
5.3 误区三:子类想完全继承父类限制,却忘了写 __slots__
class Student(object):
__slots__ = ('name',)
class GraduateStudent(Student):
pass # ❌ 子类实例又能随便加属性了
g = GraduateStudent()
g.hack = 'free' # 成功,限制失效修正:
class GraduateStudent(Student):
__slots__ = () # 空 tuple:不新增格子,但继承父类限制5.4 误区四:以为 __slots__ 能阻止给类动态加方法
class Student(object):
__slots__ = ('name',)
def set_score(self, score):
self.score = score
Student.set_score = set_score # ✅ 仍然可以!__slots__ 管的是实例属性,类的方法随时能加。但注意 self.score = score 会报错,因为 score 不在 __slots__ 里。
六、实际应用案例
案例 1:游戏角色——防止属性写错
class GameCharacter(object):
__slots__ = ('name', 'hp', 'mp', 'level')
def __init__(self, name):
self.name = name
self.hp = 100
self.mp = 50
self.level = 1
hero = GameCharacter('勇者')
hero.hp = 80 # ✅ 正常扣血
hero.hpp = 80 # ❌ AttributeError,立刻发现拼写错误!不用 __slots__ 时,hero.hpp = 80 会静默创建一个无用属性,bug 藏到很久以后才爆发。
案例 2:百万级坐标点——内存优化
# 需要处理 100 万个 GPS 坐标点
class PointWithoutSlots(object):
pass
class PointWithSlots(object):
__slots__ = ('x', 'y')
import sys
p1 = PointWithoutSlots()
p1.x = 1
p1.y = 2
p2 = PointWithSlots()
p2.x = 1
p2.y = 2
print(sys.getsizeof(p1)) # 约 56 字节(不含 __dict__ 内部开销)
print(sys.getsizeof(p2)) # 约 48 字节,实际差距更大当创建 PointWithSlots 实例 100 万个时,相比无 __slots__ 版本可节省数百 MB 内存——这对数据处理、游戏开发、科学计算至关重要。
七、实战练习
练习 1:修复拼写错误防护
下面代码想限制 Person 只能有 name 和 age,但 p.nmae = 'Bob' 没报错,找出原因并修复:
class Person(object):
__slots__ = ['name', 'age']
p = Person()
p.nmae = 'Bob' # 期望报错,实际没报参考答案
__slots__ 用了 list 而非 tuple。虽然某些 Python 版本允许 list,但官方推荐 tuple,且 list 可能行为不一致。
修复:
class Person(object):
__slots__ = ('name', 'age') # 用 tuple练习 2:继承的陷阱
class Animal(object):
__slots__ = ('name',)
class Dog(Animal):
pass
d = Dog()
d.name = '旺财' # ✅
d.bark = '汪汪' # 期望报错,实际成功,为什么?参考答案
Dog 没有定义自己的 __slots__,所以继承了"自由背包"特性。
修复:
class Dog(Animal):
__slots__ = () # 不新增属性,但保持限制或:
class Dog(Animal):
__slots__ = ('breed',) # 新增 breed,同时继承 name八、小结
- 默认情况下,Python 实例像"无拉链背包",随时可加属性/方法——灵活但易出错;
__slots__给背包加固定格子,用tuple声明允许的属性名;- 好处:防拼写错误、大幅省内存、接口更清晰;
- 关键陷阱:
__slots__只限当前类,子类必须重新定义才能维持限制; - 子类规则:子类可用属性 = 自身
__slots__+ 父类__slots__; - 适用场景:属性固定的小对象(坐标、配置、游戏角色)、百万级实例的内存优化。
记住:__slots__ 不是"银弹",属性经常变的类别用;但属性固定的场景,它是安全+性能的双赢选择。