首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的类属性把我带进沟里了,原来实例查找和类查找顺序这么反直觉

Python的类属性把我带进沟里了,原来实例查找和类查找顺序这么反直觉

原创
作者头像
风一样的男子
发布2026-09-15 15:37:31
发布2026-09-15 15:37:31
960
举报
文章被收录于专栏:编程教程编程教程

讲个真事儿。

去年我写了一个小型的任务管理模块,里面有个 Task 类,用来表示一个任务。我心想,每个任务都应该有一个标签列表,方便分类。于是我在类里定义了一个类属性:

代码语言:javascript
复制
class Task:
    tags = []
    
    def __init__(self, name):
        self.name = name
    
    def add_tag(self, tag):
        self.tags.append(tag)

看起来没问题。我创建了两个任务:

代码语言:javascript
复制
t1 = Task("写文档")
t2 = Task("改bug")

t1.add_tag("紧急")
print(t2.tags)  # 输出什么?

我预期 t2.tags 是空的,因为 t2 还没加过标签。结果它输出 ['紧急']

我盯着屏幕看了半天,以为是自己眼花了。t1 加标签,怎么 t2 也跟着有了?它们不是两个独立的对象吗?

那天我花了两个小时才彻底搞明白:类属性和实例属性的查找顺序,以及赋值和修改的区别,比我以为的要反直觉得多。

类属性和实例属性,到底有什么区别

先从头说清楚。

在 Python 里,属性分为两种:

  • 类属性:定义在类里面、方法外面的属性。它属于类本身,所有实例共享。
  • 实例属性:定义在 __init__ 里,或者通过 self.xxx = ... 动态添加的属性。它属于每个实例自己。
代码语言:javascript
复制
class Dog:
    species = "犬科"          # 类属性,所有狗共享
    
    def __init__(self, name):
        self.name = name      # 实例属性,每只狗独有

访问属性时,Python 的查找顺序是:

  1. **先找实例自己的 __dict__**(实例属性)
  2. **如果没找到,再找类的 __dict__**(类属性)
  3. 如果还没找到,沿着继承链往上找父类(MRO)
  4. 最后找不到就抛 AttributeError

这个顺序本身不反直觉。反直觉的地方在于赋值修改的区别。

赋值:在哪个字典里写,就在哪个字典里读

当你写 self.tags = [...] 时,Python 会在实例的 __dict__ 里创建一个新的键值对。这叫做“实例属性赋值”。

当你写 Task.tags = [...] 时,Python 会在类的 __dict__ 里修改。这叫做“类属性赋值”。

但如果你写 self.tags.append(...),情况就完全不一样了。

self.tags 首先触发查找:先看实例 __dict__ 里有没有 tags。如果没有,就去找类 __dict__。找到了类属性 tags,它是一个列表。然后 .append()原地修改这个列表。这个列表是类属性,属于类本身,所有实例共享。

所以 t1 和 t2 的 self.tags 最终指向的是同一个列表对象。t1 往里加东西,t2 看到的自然也是同一个列表。

这就是我踩的坑:我以为 self.tags 是每个实例自己的,但其实它只是一个查找结果。查找找到的是类属性,修改它就是在修改类属性。

用生活场景理解:公共储物柜和私人抽屉

我想到一个比喻。

类属性就像公司里的公共储物柜。所有员工(实例)都可以打开它,往里放东西,或者拿东西。你放进去的东西,别人也能看到。

实例属性就像每个员工工位上的私人抽屉。你自己往里放东西,别人看不到。

当你执行 self.tags.append("紧急") 时,你实际上是在说:“先去我的私人抽屉找 tags,没找到?那去公共储物柜找。找到了?好,往公共储物柜里塞一个‘紧急’。”

而当你执行 self.tags = ["紧急"] 时,你是在说:“在我的私人抽屉里放一个叫 tags 的东西,值是一个新列表。”

前者修改了公共物品,后者创建了私有物品。这就是区别。

可变类属性的坑:列表、字典、集合

这个坑最常出现在列表、字典、集合这些可变对象上。因为它们可以被原地修改,而不是被重新赋值。

代码语言:javascript
复制
class User:
    permissions = []   # 危险:所有用户共享这个列表
    
    def add_permission(self, perm):
        self.permissions.append(perm)

如果你真的希望每个用户有独立的权限列表,应该这样写:

代码语言:javascript
复制
class User:
    def __init__(self):
        self.permissions = []   # 每个实例自己的列表

或者,如果你确实需要一个类级别的默认值,但不想共享,可以用 None 作为占位符:

代码语言:javascript
复制
class User:
    permissions = None
    
    def add_permission(self, perm):
        if self.permissions is None:
            self.permissions = []   # 第一次访问时创建实例属性
        self.permissions.append(perm)

这样,第一次调用 add_permission 时,会在实例的 __dict__ 里创建一个新的 permissions 列表。之后所有操作都在实例自己的列表上进行,不会影响其他实例。

继承中的查找顺序:比你想的复杂

类属性的查找顺序在单继承时还算直观:先实例,再子类,再父类。

但在多继承时,Python 使用 C3 线性化算法 来确定方法的解析顺序(MRO,Method Resolution Order)。这个顺序有时候很反直觉。

看一个经典的“钻石继承”:

代码语言:javascript
复制
class A:
    def who(self):
        print("A")

class B(A):
    def who(self):
        print("B")

class C(A):
    def who(self):
        print("C")

class D(B, C):
    pass

d = D()
d.who()  # 输出什么?

如果你以为输出 A,那就错了。输出是 B。

因为 D 的 MRO 是:D → B → C → A → object。

查找 who 方法时,先找 D(没有),再找 B(有),所以输出 B。

这个顺序不是简单的“深度优先”,而是 C3 线性化计算出来的。它的规则是:子类永远在父类前面,多个父类按声明顺序从左到右,但如果有共同的祖先,要保证祖先在多个路径中只出现一次且位置正确。

你可以用 D.__mro__ 查看:

代码语言:javascript
复制
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

这个顺序在属性查找时同样适用。所以如果你在 B 和 C 里都定义了同名的类属性,D 的实例会优先找到 B 的那个。

一个更隐蔽的坑:子类覆盖类属性

假设你有一个基类,定义了一个类属性:

代码语言:javascript
复制
class Base:
    config = {"timeout": 30, "retries": 3}

class Child(Base):
    pass

然后你修改 Child.config

代码语言:javascript
复制
Child.config["timeout"] = 60

你以为只改了 Child 的配置?错了。因为 Child.config 先查找 Child 的 __dict__,没找到,去 Base 找,找到了那个字典。然后你原地修改了它。Base 的 config 也被改了。

代码语言:javascript
复制
print(Base.config)  # {'timeout': 60, 'retries': 3}

如果你想让 Child 有自己独立的配置,应该重新赋值:

代码语言:javascript
复制
Child.config = {"timeout": 60, "retries": 3}

这样会在 Child 的 __dict__ 里创建一个新的字典,Base 不受影响。

类属性什么时候该用

类属性不是坏东西,用对了地方很方便。以下场景适合用类属性:

  • 常量:比如 PI = 3.14159,所有实例共享,不需要修改。
  • 共享的不可变配置:比如 species = "犬科"
  • 类级别的计数器:但要注意线程安全和并发问题。
  • 单例模式中的实例引用

但以下场景应该避免用类属性:

  • 可变对象作为默认值(列表、字典、集合),除非你明确希望所有实例共享。
  • 需要每个实例独立维护的状态
  • 在继承中可能被修改的配置

如何正确管理属性

一个简单的原则:如果一个属性的值会因实例而异,就放在 __init__ 里作为实例属性。如果一个属性是所有实例共享且不会变的,才放在类里作为类属性。

代码语言:javascript
复制
class Task:
    # 类属性:所有任务共享的常量
    MAX_RETRIES = 3
    
    def __init__(self, name):
        # 实例属性:每个任务独有的
        self.name = name
        self.tags = []
        self.retries = 0

如果你需要在类级别共享一个可变对象,但又希望每个实例可以独立修改,可以用 None 占位,在 __init__ 或首次访问时创建实例属性。

查看 __dict__ 来理解一切

Python 中一切皆对象,类和实例都有自己的 __dict__。查看它们能帮你理解属性到底存在哪里。

代码语言:javascript
复制
class Task:
    tags = []
    def __init__(self, name):
        self.name = name

t = Task("写文档")
print(t.__dict__)        # {'name': '写文档'}
print(Task.__dict__)     # 包含 tags, __init__ 等

当你访问 t.tags 时,Python 先看 t.__dict__,没有;再看 Task.__dict__,有,返回那个列表。

当你执行 t.tags.append("紧急")t.__dict__ 依然只有 nameTask.__dict__ 里的 tags 被修改了。

当你执行 t.tags = ["紧急"]t.__dict__ 里多了一个 tags 键,之后 t.tags 就优先用这个了。

多重继承中的 super() 也依赖 MRO

super() 的行为也由 MRO 决定。它不是在找“父类”,而是在 MRO 列表里找“下一个类”。

代码语言:javascript
复制
class A:
    def __init__(self):
        print("A")

class B(A):
    def __init__(self):
        super().__init__()
        print("B")

class C(A):
    def __init__(self):
        super().__init__()
        print("C")

class D(B, C):
    def __init__(self):
        super().__init__()
        print("D")

d = D()
# 输出顺序:A, C, B, D

因为 D 的 MRO 是 D → B → C → A。super() 从 B 开始,B 的 super() 找到 C,C 的 super() 找到 A。所以初始化顺序是 A → C → B → D。

这个顺序和单继承的直觉完全不同,但它是 C3 线性化的必然结果。

总结

Python 的类属性查找顺序,核心就三句话:

  1. 查找时:先实例,后类,再父类,按 MRO 顺序。
  2. 赋值时self.x = ... 创建实例属性;Class.x = ... 修改类属性。
  3. 修改可变类属性时:如果实例没有自己的同名属性,修改的是类属性,所有实例共享。

记住那个储物柜的比喻:类属性是公共储物柜,实例属性是私人抽屉。查找时先翻自己的抽屉,再翻公共储物柜。直接赋值等于往自己抽屉里放东西,原地修改等于在公共储物柜里动手脚。

我那个任务管理模块后来改成了在 __init__ 里初始化 self.tags = [],所有实例就都独立了。从那以后,每次定义类属性,我都会问自己一句:这个属性,是所有实例共享的,还是每个实例独有的?答案清楚了,坑就绕过去了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 类属性和实例属性,到底有什么区别
  • 赋值:在哪个字典里写,就在哪个字典里读
  • 用生活场景理解:公共储物柜和私人抽屉
  • 可变类属性的坑:列表、字典、集合
  • 继承中的查找顺序:比你想的复杂
  • 一个更隐蔽的坑:子类覆盖类属性
  • 类属性什么时候该用
  • 如何正确管理属性
  • 查看 __dict__ 来理解一切
  • 多重继承中的 super() 也依赖 MRO
  • 总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档