一、本质归属:谁才是变量真正的主人?
类变量,顾名思义,属于类本身。它在类定义时便已诞生,存储于类对象的命名空间之中,是所有实例共享的一份数据。无论你创建多少个实例,类变量在内存中始终只有一份拷贝。
实例变量则完全不同——它属于每一个具体的实例对象。当你通过构造函数初始化一个新实例时,实例变量才随之创建,存储在该实例独有的命名空间里。每个实例都拥有自己的一套实例变量,彼此之间互不干扰。
用一个形象的比喻:类变量好比一间教室里唯一的那块黑板,所有学生看到的都是同一块;实例变量则像每个学生桌上各自的笔记本,内容各不相同,互不影响。
二、底层存储:堆区之上的命名空间博弈
要真正理解二者的差异,必须深入 Python 的内存模型。Python 的内存可简化为两大核心区域:栈帧区用于存储函数调用的上下文信息(局部变量引用、参数地址等),堆区则存放所有实际的数据对象——包括实例、类、模块、函数等。
当解释器执行类定义时,会在堆区创建一个类对象。这个类对象的核心载体是其 __dict__ 字典,也就是类的命名空间。类变量就安静地躺在这个字典里,仅此一份。
当你通过类名创建实例时,解释器会在堆区再开辟一块空间,生成一个实例对象。这个实例对象同样拥有自己的 __dict__ 字典——这就是实例的命名空间,实例变量就存储在这里。
因此,从存储角度来看:
| 维度 | 类变量 | 实例变量 |
|---|---|---|
| 存储位置 | 类对象的 __dict__ 中 |
实例对象的 __dict__ 中 |
| 副本数量 | 仅一份,所有实例共享 | 每个实例一份,相互独立 |
| 内存效率 | 极高,不随实例数量增长而增长 | 随实例数量线性增长 |
这就是为什么类变量适合存放常量、计数器、默认配置等所有实例通用的数据——因为它从根本上避免了重复存储的开销。
三、引用语义:Python 变量的底层真相
在深入讨论之前,我们必须先厘清一个关键前提:Python 中一切变量都是引用。
变量本身并不直接存储数据,它只是一个指向内存中某个对象的"指针"。当你写下 a = 10 时,Python 先在堆区创建一个值为 10 的整数对象,然后让变量 a 指向这个对象的内存地址。id(a) 返回的正是这个地址。
这一机制对类变量和实例变量同样适用。类变量存储的是对某个对象的引用,实例变量存储的也是对某个对象的引用。区别仅在于:这个引用被存放在哪个命名空间里。
正因如此,Python 的数据类型被划分为可变对象(如列表、字典、自定义类实例)和不可变对象(如整数、字符串、元组)。不可变对象一旦创建便不可修改,任何"修改"操作实际上都是创建新对象并让变量指向新地址;可变对象则可以在原地修改内容,变量指向的地址不变。
这一特性直接导致了类变量使用中最经典的陷阱——可变类变量的共享陷阱。
四、访问优先级:实例 → 类 → 父类的三级查找链
当你通过实例访问一个属性时,Python 并非直接去类的命名空间里找,而是遵循一条严格的优先级链路:
第一步:查实例的 __dict__ ——如果存在同名实例变量,直接返回,查找结束。
第二步:查类的 __dict__ ——如果实例中没有,则去类对象的命名空间里寻找类变量。
第三步:查父类的 __dict__ ——如果当前类也没有,则沿继承链向上遍历父类。
最终:抛出 AttributeError ——如果整条链路都没找到,才报错。
这条链路解释了一个看似矛盾的现象:为什么通过实例既能读到类变量,又能"遮蔽"类变量?
答案是:读取时走查找链,能找到类变量;但一旦你给实例属性赋值,就相当于在实例的 __dict__ 里创建了一个同名实例变量,从此这条查找链在第一步就被截断了,类变量被彻底"遮蔽"。
这也是为什么官方文档反复强调:修改类变量应通过类名进行,而非通过实例。通过实例赋值只会创建实例变量,并不会真正修改类变量本身。
五、修改行为:一字之差,天壤之别
修改类变量和修改实例变量,后果截然不同。
修改类变量(正确方式): 通过 类名.变量名 = 新值 进行。此时修改的是类命名空间中的那唯一一份数据,所有实例在下一次访问时都会感知到变化。这是真正意义上的"一处修改,处处生效"。
修改实例变量: 直接 实例.变量名 = 新值 即可。这只会在当前实例的 __dict__ 里创建或更新一条记录,其他实例完全不受影响。
通过实例"修改"类变量(错误方式): 当你写 实例.类变量名 = 新值 时,Python 并不会去改动类的那份数据,而是在该实例的 __dict__ 里新建了一个同名实例变量。从此,这个实例访问的是自己的实例变量,而其他没有同名实例变量的实例,访问的仍然是类变量。结果就是:同一个类的不同实例,对同一个属性名可能看到不同的值——这无疑是一场灾难。
六、生命周期:与谁共存,与谁同亡
类变量的生命周期与类对象绑定。类在定义时被加载到内存,类变量随之诞生;只要类对象不被销毁,类变量就一直存活。即便所有实例都被垃圾回收,类变量依然安在。
实例变量的生命周期则与实例对象绑定。实例创建时,实例变量诞生;实例被回收时,实例变量随之消亡。二者同生共死,干脆利落。
这种生命周期的差异,决定了二者各自适合的应用场景:
- 类变量适合存放:版本号、最大重试次数、所有实例共享的配置、实例计数器、数据库连接池等。
- 实例变量适合存放:用户姓名、年龄、订单金额、缓存数据等每个实例独有的状态信息。
七、命名规范:语义驱动的最佳实践
Python 社区遵循 PEP 8 规范,但类变量和实例变量在命名上有明确的语义区分:
类变量常用全大写加下划线的形式,如 MAX_RETRY_COUNT、DEFAULT_TIMEOUT,暗示这是不可变的常量级数据。也常用 _count、_map 等带单下划线的形式,表示这是类级的批量数据。
实例变量则使用蛇形命名法,如 user_name、order_id,体现个体特征。实例变量几乎不使用全大写——因为实例属性本就该支持个性化修改,用全大写反而会误导其他开发者。
在私有命名上,类变量用单下划线 _tax_rate 表示软私有(类内部共享),用双下划线 __secret_key 防止子类覆盖。实例变量用 _name 表示建议通过方法访问,双下划线极少使用。
八、致命陷阱与避坑指南
陷阱一:可变类变量的隐形共享。 如果你把一个列表定义为类变量,所有实例将共享同一个列表对象。任何实例对该列表的 append、pop 等操作,都会影响其他实例。这是最常见也最隐蔽的 bug 来源。解决方案:在构造函数中用 self.变量名 = [] 重新绑定,将其转为实例变量。
陷阱二:实例变量遮蔽类变量后的"假修改"。 通过实例给类变量赋值,看似修改成功,实际上只是创建了实例变量。其他实例依然看到的是原来的类变量值。调试时可用 print(实例.__dict__) 和 print(类名.__dict__) 直观对比二者内容。
陷阱三:子类继承中的意外覆盖。 子类如果定义了同名类变量,会遮蔽父类的类变量,但不会修改父类本身。这是 Python 命名空间隔离机制的正常表现,但需要开发者心中有数。
九、总结:一张表看清所有差异
| 对比维度 | 类变量 | 实例变量 |
|---|---|---|
| 归属 | 类本身 | 每个实例 |
| 存储 | 类的 __dict__,仅一份 |
实例的 __dict__,每实例一份 |
| 共享性 | 所有实例共享 | 仅当前实例独有 |
| 访问方式 | 类名或实例(走查找链) | 仅实例 |
| 修改方式 | 推荐类名.变量名 | 实例.变量名 |
| 生命周期 | 随类存亡 | 随实例存亡 |
| 适用场景 | 常量、计数器、共享配置 | 实例状态、个体数据 |
理解类变量与实例变量的底层机制,不是为了应付面试,而是为了在实际工程中写出更健壮、更可维护的代码。当你清楚地知道每一个变量存储在哪里、被谁引用、修改后会影响谁,你就真正掌握了 Python 面向对象编程的精髓。