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

目录
相关文章
|
20天前
|
数据采集 JavaScript 前端开发
房产数据对比爬虫:同时爬取链家+贝壳+安居客,做房价横向对比
本文详解如何突破链家/贝壳(JS渲染+字体反爬)与安居客(动态Token+请求头校验)的差异化反爬机制,基于适配器模式构建跨平台爬虫架构,结合站大爷隧道代理实现IP隔离,并通过小区名+户型+面积模糊匹配,完成房价横向对比分析,助力识别虚高挂牌与低价引流。
181 1
|
18天前
|
人工智能 数据挖掘
阿里千问办公官网入口链接在哪?QwenWork.cn官网,自动帮你干活的AI Agent!
千问办公提供两大官方入口:一是网页端(qwenwork.cn),支持免安装即用;阿里千问办公官网:https://t.aliyun.com/U/JNKJuO 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
871 2
|
19天前
|
SQL 安全 API
OpenClaw 2.0 便捷安装来了,免费领 Agent 数据套件,100 万免费 Token 一键到位
OpenClaw 2.0 刚发布,装机量和技术社区讨论度一路走高。一个跑在电脑上的个人 Agent,能用自然语言操作应用、读取数据、完成多步骤任务——能力越强,企业和开发者越关心同一个问题:Agent 的行为谁来管?数据通道谁来守? 阿里云数据库团队给出的答案是一套适配 OpenClaw 2.0 的能力套件:AIDBS Agent 安全用数套件。一键便捷安装 OpenClaw 2.0 的同时,为企业预装 Agent 行为防护、数据通道安全与专业数据能力。公测期间免费,还送上限 100 万 Qwen3.8-Max Token。
187 0
OpenClaw 2.0 便捷安装来了,免费领 Agent 数据套件,100 万免费 Token 一键到位
|
20天前
|
BI API 语音技术
阿里云百炼平台新人免费额度完整实操指南:领取规则、额度查看、用完即停与API代码调用全解
对于初次接触大模型开发、原型验证、智能应用搭建的个人开发者与企业技术团队来说,平台新人免费额度是降低试错成本的重要资源。开发者可以借助免费额度完成模型能力验证、应用原型调试、业务场景POC测试,不用一开始就投入按量付费成本。百炼大模型服务平台提供面向新开通用户的新人免费推理额度,但这套权益附带地域限制、有效期、额度隔离、账号共享、扣费防护等一系列复杂规则,如果对规则理解不到位,容易出现额度看不到、提前过期、子账号消耗完主账号额度、免费阶段产生意料之外账单等问题。本文完整梳理新人免费额度全部规则,讲解额度获取、多途径查看剩余额度、用完即停防护功能,附带可直接运行curl、Python调用代码,梳
263 1
|
19天前
|
机器学习/深度学习 弹性计算 缓存
2026年阿里云服务器新人优惠活动,租用阿里云服务器一年要多少钱?最低38元1年
2026年阿里云新人专享:轻量应用服务器2核2G/200M带宽/40G SSD/不限流量,新用户专享价38元(每日10点、15点秒杀)或68元(随时购)。另有99元ECS及企业级199元特价机型可选,均支持企业认证购买。
|
19天前
|
缓存 人工智能 API
阿里云Qwen3.8-Flash大模型详细介绍:多模态MOE模型,支持百万级上下文窗口,能一次性处理超长文档、代码仓库
本文全面拆解阿里云通义千问Qwen3.8-Flash多模态大模型,介绍其百万级上下文窗口、全场景能力矩阵、多地域部署、分层定价与多重限时优惠,同时给出API调用示例,帮助开发者与企业兼顾推理效果、响应速度与调用成本,是生产级AI应用的高性价比选型指南。
|
20天前
|
人工智能 IDE API
保姆级Cursor IDE部署教程:自定义模型接入Coding Plan、Token Plan双套餐,代码开发全流程
在现代软件工程开发场景中,传统代码编辑器只能完成基础的代码编辑,而Cursor作为一款基于VS Code深度改造的AI原生编程IDE,内置强大的Agent智能体能力,能够读取整个项目目录、解析多文件源码、理解完整工程架构,支持自然语言驱动代码生成、跨文件重构、自动debug、编写单元测试、生成项目文档。Cursor支持自定义OpenAI兼容接口端点,能够直接对接百炼平台的Coding Plan与Token Plan两大订阅套餐。Coding Plan专门面向代码开发场景做优化,提供更高的调用额度与编码场景优惠;Token Plan通用性更强,支持全系列大模型,兼顾代码、文案、数据分析等多种任务
254 0
|
20天前
|
人工智能 数据可视化 安全
从零上手百炼大模型平台:免费Tokens领取、API代码示例、可视化应用构建教程
阿里云百炼作为一站式大模型服务平台,兼顾开发者与业务人员两类使用者。汇集千问系列以及众多第三方大模型,支持文本、图像、视频、语音全模态能力;提供可视化无代码与高代码两套开发模式,业务人员可以零代码搭建智能体知识库,开发者依靠兼容OpenAI的API快速迁移原有项目,配套百炼CLI命令行工具可以在终端自动化执行各类AI任务。
246 0
|
24天前
|
存储 JSON 数据格式
Python的生成器把我内存救活了,原来return和yield差距这么大
本文通过真实案例对比`return`与`yield`:前者一次性加载50万条JSON数据致内存飙至14GB,后者用生成器逐行处理仅耗80MB。深入剖析二者在执行机制、内存模型及编程思维(批处理vs流式)的本质差异,并详解生成器的惰性求值、状态保持、一次性等特性及典型应用场景。
78 0
|
25天前
|
数据采集 安全 JavaScript
Python的默认参数把我坑惨了,原来可变对象当默认值等于给自己埋雷
本文详解Python经典陷阱:可变对象作默认参数。因默认值在函数定义时仅创建一次,`def f(lst=[])` 中的列表会被所有调用共享,导致数据意外累积、污染。推荐统一用 `None` 占位,在函数内初始化,避免隐蔽bug。
83 0

热门文章

最新文章