Skip to content

Python __slots__:给实例的"属性背包"加上拉链

引言:从"随便装"到"只能装规定物品"

默认情况下,Python 实例就像一个没有拉链的背包——你可以随时往里塞任何东西(属性、方法),想塞多少塞多少。这种灵活性很爽,但也容易出问题:

  • 手一滑把 name 写成 nmae,程序不报错,但数据悄悄"丢了";
  • 创建几十万个小对象时,每个对象都背着一个"万能背包",内存浪费严重。

__slots__ 就是给这个背包加上拉链和固定格子——只允许装指定的物品,多一件都不行。


一、默认行为:实例是"自由背包"

1.1 动态绑定属性

python
class Student(object):
    pass

s = Student()
s.name = 'Michael'   # 动态给实例绑定一个属性
print(s.name)        # Michael

实例创建后,可以随时"贴标签"——s 这个背包里多了 name 这个格子。

1.2 动态绑定方法

python
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)   # 25

1.3 关键:实例绑定的方法,对其他实例无效

python
s2 = Student()
s2.set_age(25)   # AttributeError: 'Student' object has no attribute 'set_age'

生活化理解:你给自己的背包缝了个小口袋(set_age),别人的背包并没有——各自独立。

1.4 给类绑定方法:所有实例共享

python
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 基本用法

python
class Student(object):
    __slots__ = ('name', 'age')   # 用 tuple 定义允许绑定的属性名称

测试:

python
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__,内存大幅节省
属性可控性任何人都能乱加属性严格白名单,接口清晰

内存对比(简化理解):

python
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__ 对子类无效

python
class Student(object):
    __slots__ = ('name', 'age')

class GraduateStudent(Student):
    pass

g = GraduateStudent()
g.score = 9999   # ✅ 居然成功了!

生活化理解:父亲给自己背包加了拉链,但儿子出生时默认还是无拉链背包——父亲的限制管不到儿子。

3.2 子类如何真正受限?

子类必须自己再定义一次 __slots__

python
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__ 能限制类属性

python
class Student(object):
    __slots__ = ('name', 'age')
    count = 0   # 类属性,不受 __slots__ 限制

Student.count = 100   # ✅ 正常
s = Student()
print(s.count)        # 100

__slots__ 只限制实例属性,类属性随便改。

5.2 误区二:在 __slots__ 里写方法名

python
class Student(object):
    __slots__ = ('name', 'set_age')   # ❌ 错误!

    def set_age(self, age):
        self.age = age

__slots__ 只能写数据属性,不能写方法。方法定义在类里,不占实例的格子。

5.3 误区三:子类想完全继承父类限制,却忘了写 __slots__

python
class Student(object):
    __slots__ = ('name',)

class GraduateStudent(Student):
    pass   # ❌ 子类实例又能随便加属性了

g = GraduateStudent()
g.hack = 'free'   # 成功,限制失效

修正

python
class GraduateStudent(Student):
    __slots__ = ()   # 空 tuple:不新增格子,但继承父类限制

5.4 误区四:以为 __slots__ 能阻止给类动态加方法

python
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:游戏角色——防止属性写错

python
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:百万级坐标点——内存优化

python
# 需要处理 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 只能有 nameage,但 p.nmae = 'Bob' 没报错,找出原因并修复:

python
class Person(object):
    __slots__ = ['name', 'age']

p = Person()
p.nmae = 'Bob'   # 期望报错,实际没报
参考答案

__slots__ 用了 list 而非 tuple。虽然某些 Python 版本允许 list,但官方推荐 tuple,且 list 可能行为不一致。

修复:

python
class Person(object):
    __slots__ = ('name', 'age')   # 用 tuple

练习 2:继承的陷阱

python
class Animal(object):
    __slots__ = ('name',)

class Dog(Animal):
    pass

d = Dog()
d.name = '旺财'   # ✅
d.bark = '汪汪'   # 期望报错,实际成功,为什么?
参考答案

Dog 没有定义自己的 __slots__,所以继承了"自由背包"特性。

修复:

python
class Dog(Animal):
    __slots__ = ()   # 不新增属性,但保持限制

或:

python
class Dog(Animal):
    __slots__ = ('breed',)   # 新增 breed,同时继承 name

八、小结

  1. 默认情况下,Python 实例像"无拉链背包",随时可加属性/方法——灵活但易出错;
  2. __slots__ 给背包加固定格子,用 tuple 声明允许的属性名;
  3. 好处:防拼写错误、大幅省内存、接口更清晰;
  4. 关键陷阱__slots__ 只限当前类,子类必须重新定义才能维持限制;
  5. 子类规则:子类可用属性 = 自身 __slots__ + 父类 __slots__
  6. 适用场景:属性固定的小对象(坐标、配置、游戏角色)、百万级实例的内存优化。

记住:__slots__ 不是"银弹",属性经常变的类别用;但属性固定的场景,它是安全+性能的双赢选择。