Python 动态属性绑定与 __slots__ 使用教程

在做数据量稍大的实体类(比如电商的商品、社交平台的用户信息)开发时,你有没有遇到过这些小麻烦?

  • 同事手滑给临时测试的学生实例加了一堆s.temp_xxx属性,忘了删除导致后续调试数据混乱?
  • 部署时发现几十万个小对象占用内存远超预期?
  • 维护代码时不确定某个类“规定”了哪些核心属性,只能翻遍所有调用点?

这时候,Python 里的 __slots__ 就能帮你解决这些问题。不过在讲它之前,我们得先回忆下 Python 最“自由”的特性之一:运行时动态绑定属性和方法


1. Python 动态绑定机制

Python 是纯动态语言,不像 Java/C++ 在编译时就锁定了类的属性和方法。在代码运行过程中,你可以随时给单个实例、甚至整个类追加属性或函数。

1.1 动态绑定单个实例的属性

这是最基础的操作,直接给实例赋值就行:

class Student:
    """空的初始类,什么都没定义"""
    pass

# 创建第一个学生实例
s1 = Student()
s1.name = "张三"  # 单独给 s1 绑定 name 属性
print(s1.name)    # 输出:张三

# 创建第二个学生实例
s2 = Student()
# print(s2.name)  # 报错!s2 没被单独绑定 name 属性

1.2 动态绑定单个实例的方法

属性可以单独加,函数也可以单独绑定给一个实例(但要注意用 types.MethodType 包装,把实例本身作为 self 传递):

from types import MethodType

def set_gender(self, gender):
    """要绑定的函数"""
    self.gender = gender

# 单独给 s1 绑定 set_gender 方法
s1.set_gender = MethodType(set_gender, s1)
s1.set_gender("男")
print(s1.gender)  # 输出:男

# s2 用不了这个方法
# s2.set_gender("女")  # 报错!

1.3 动态绑定整个类的属性/方法

如果想让所有实例都能用新属性或新方法,直接给类赋值就行:

def set_grade(self, grade):
    """绑定给整个类的函数"""
    self.grade = grade

# 给 Student 类绑定 set_grade 方法
Student.set_grade = set_grade

# 所有新老实例都能用
s2.set_grade(3)
print(s2.grade)  # 输出:3
s1.set_grade(4)
print(s1.grade)  # 输出:4

2. 用 __slots__ 给类“戴紧箍咒”

动态绑定虽然灵活,但“自由过头”就容易出问题。这时候 __slots__ 就登场了——它是一个元组或列表,定义了类的实例只能拥有的属性名称

2.1 最基础的用法

直接在类内部声明 __slots__ = (允许的属性1, 允许的属性2, ...) 就行:

class Student:
    __slots__ = ("name", "age")  # 只允许绑定 name 和 age

s = Student()
s.name = "李四"
s.age = 22
# s.score = 99  # ❌ 报错!AttributeError: 'Student' object has no attribute 'score'

2.2 继承场景下的 __slots__

这里有两个常见的小坑,要特别注意:

  1. 默认不继承父类的 __slots__限制(除非子类也显式声明):如果父类用了 __slots__,子类没写,那子类的实例会同时拥有父类的允许属性+自己的 __dict__(可以随便加属性)。
  2. 如果子类也写了,允许的属性是「父类+子类」的并集
class GraduateStudent(Student):
    __slots__ = ("score",)  # 只添加 score,不覆盖父类的

g = GraduateStudent()
g.name = "王五"    # ✅ 父类允许的
g.age = 25         # ✅ 父类允许的
g.score = 98       # ✅ 子类允许的
# g.temp_grade = 1 # ❌ 报错!

2.3 现代 Python 开发的注意事项

别以为 __slots__ 只是限制属性——它还有几个核心隐藏特性

2.3.1 节省内存!(这是它最重要的现代用途之一)

Python 默认给每个实例分配一个 __dict__ 字典存属性,字典本身是很占内存的(几十万个实例的话,可能差几十MB甚至GB)。用了 __slots__ 后,Python 会用更紧凑的固定大小结构存属性,不再创建 __dict__

2.3.2 和 @property 完美兼容

有人担心:用了 __slots__ 是不是不能用属性装饰器了?完全不会!只要把 @property 对应的私有变量也加到 __slots__ 里就行:

class RestrictedStudent:
    __slots__ = ("name", "age", "_score")  # 别忘了加私有变量 _score

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, (int, float)) or not (0 <= value <= 100):
            raise ValueError("分数必须是0-100的数字")
        self._score = value

rs = RestrictedStudent()
rs.name = "赵六"
rs.age = 21
rs.score = 95  # ✅ 用 property 设置
print(rs.score)  # 输出:95
# rs.score = "A"  # ❌ 报错

2.3.3 别乱用!什么时候不适合用?

如果你的类需要经常动态扩展属性(比如做爬虫时临时抓字段的类),或者需要用 __dict__ 做属性操作(比如 vars(obj)obj.__dict__.update(...)),就别加 __slots__


3. 实际应用场景建议

根据 __slots__ 的特性,它最适合这几种情况:

  1. 大量创建的实体类:比如日志解析的单条记录、推荐系统的候选商品,能省很多内存。
  2. 框架/库的内部类:防止库的用户手滑给你的内部类加没用的属性,保证代码稳定性。
  3. 需要严格控制属性的业务类:比如金融系统的账户类,只允许定义好的属性(比如 id、balance、create_time),防止数据意外被篡改或污染。

4. 快速对比总结

特性动态绑定(默认)使用 __slots__
灵活性极高,随时加属性/方法低,只能用预定义的属性
内存占用较高(每个实例有字典)较低(紧凑结构)
属性访问速度稍慢(字典查找)稍快(固定偏移量)
支持 __dict__❌(除非显式加到 __slots__
适用场景需要灵活扩展的类大量实例/严格控制属性的类

最后总结一下:__slots__ 不是 Python 开发的“必需品”,但在合适的场景下用它,能让你的代码更稳定、内存更省、性能更好。