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

简介: 本文以真实踩坑案例切入,深入剖析Python类属性与实例属性的核心区别:查找时“先实例后类”,但赋值与修改行为截然不同。重点揭示可变类属性(如列表、字典)引发的共享陷阱,并用“公共储物柜vs私人抽屉”生动比喻助你彻底理解。

讲个真事儿。

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

class Task:
   tags = []
   
   def __init__(self, name):
       self.name = name
   
   def add_tag(self, tag):
       self.tags.append(tag)

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

t1 = Task("写文档")
t2 = Task("改bug")

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

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

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

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

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (85).png

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

先从头说清楚。

在 Python 里,属性分为两种:

  • 类属性:定义在类里面、方法外面的属性。它属于类本身,所有实例共享。
  • 实例属性:定义在 __init__ 里,或者通过 self.xxx = ... 动态添加的属性。它属于每个实例自己。

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 的东西,值是一个新列表。”

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

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

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

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

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

class User:
   def __init__(self):
       self.permissions = []   # 每个实例自己的列表

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

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)。这个顺序有时候很反直觉。

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

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__ 查看:

print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

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

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

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

class Base:
   config = {"timeout": 30, "retries": 3}

class Child(Base):
   pass

然后你修改 Child.config

Child.config["timeout"] = 60

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

print(Base.config)  # {'timeout': 60, 'retries': 3}

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

Child.config = {"timeout": 60, "retries": 3}

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

类属性什么时候该用

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

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

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

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

如何正确管理属性

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

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

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

查看 __dict__ 来理解一切

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

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 列表里找“下一个类”。

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 = [],所有实例就都独立了。从那以后,每次定义类属性,我都会问自己一句:这个属性,是所有实例共享的,还是每个实例独有的?答案清楚了,坑就绕过去了。

目录
相关文章
|
7天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1779 10
|
11天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1643 3
|
12天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
8天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
778 2
|
6天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
801 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
|
20天前
|
人工智能 自然语言处理 安全
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
本文聚焦阿里云2026年推出的三款自研AI办公产品,清晰拆解千问办公、Qoder Teams、Qoder CN的差异化定位与能力边界:千问办公主打职场全场景提效,支持自然语言指令一键完成PPT生成、数据分析等高频办公任务;Qoder Teams面向程序员团队,深度整合AI代码生成、团队协同与企业知识库能力;Qoder CN则专为金融、政务等强合规场景打造,实现数据不出境与VPC私有化部署。文章同步给出分场景选型指南与最新活动定价,帮助不同类型的企业按需组合产品,实现业务岗、研发岗与强合规场景的AI能力全覆盖。
3963 5
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
|
11天前
|
人工智能 自然语言处理 安全
阿里云AI数智鉴密:AI 生成内容如何拿到一张"防篡改的身份证"
隐形水印 + C2PA签名:让AI生成内容“持证上岗”。
1155 0
|
13天前
|
缓存 数据可视化 开发工具
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
DeepSeek Harness 的更新分两层:本体更新(npx 自动最新、npm update -g、源码 git pull)与插件更新(插件市场点更新、命令行覆盖安装)。本文按「准备 → 更新本体 → 更新插件 → 更新后检查」四步走,覆盖新手常见疑问。
1503 1
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
|
6天前
|
人工智能
千问办公官网入口:阿里AI办公QwenWork产品页和免费网页端链接
千问办公官网含两大入口:一是网页端(qwenwork.cn),即开即用,支持浏览器直接访问;二是阿里云产品页 https://t.aliyun.com/U/JNKJuO 提供免费/付费版详情、功能介绍及使用指南。

热门文章

最新文章