详解 Python 类变量与实例变量的访问流程步骤
·
好的,我们来详细拆解一下 Python 中访问类变量和实例变量的完整流程。这是一个非常核心且容易混淆的知识点。
我将通过 **“提出问题 -> 分步解析 -> 总结规则 -> 实例验证”** 的结构,为你彻底讲清楚。
核心问题
当你写下 obj.attr 这样的代码时,Python 内部到底发生了什么?它是如何决定返回的是实例变量还是类变量的?
访问流程详解 (查找顺序)
Python 在查找一个属性时,遵循一个特定的顺序,这个顺序通常被称为 **“MRO”(Method Resolution Order)** 的一部分,但对于属性查找,我们可以简化理解为以下步骤:
第一步:查找实例的命名空间 (__dict__)
当你通过一个实例(比如 obj)来访问属性时,Python 会首先检查这个实例自身是否拥有该属性。
- 每个实例都有一个内置的字典
__dict__,它存储了与该实例绑定的所有属性(即实例变量)。 - Python 会执行类似
if 'attr' in obj.__dict__:的检查。 - 如果找到了:直接返回
obj.__dict__['attr']的值。流程终止。 - 如果没找到:流程进入第二步。
第二步:查找类的命名空间 (__dict__)
如果实例自身没有这个属性,Python 会转而检查该实例所属的类(比如 obj.__class__)。
- 每个类也有一个
__dict__,存储了类的属性,包括类变量和方法。 - Python 会执行类似
if 'attr' in obj.__class__.__dict__:的检查。 - 如果找到了:返回
obj.__class__.__dict__['attr']的值。流程终止。 - 如果没找到:流程进入第三步。
第三步:沿着继承链向上查找
如果在类中也找不到,Python 会继续在类的父类、祖父类等祖先类中重复第二步的查找过程。
- 它会按照
__mro__(Method Resolution Order) 属性定义的顺序,依次在每个父类的__dict__中查找。 - 如果在某个父类中找到了:返回该父类
__dict__中的值。流程终止。 - 如果直到继承链的顶端(通常是
object类)都找不到:- 如果你使用的是默认的访问方式 (
obj.attr),Python 会抛出AttributeError异常。 - 如果你使用的是
getattr(obj, 'attr', default_value)函数,它会返回你指定的default_value。
- 如果你使用的是默认的访问方式 (
流程图总结
plaintext
访问 obj.attr
|
+-- 1. 检查 obj.__dict__ 是否有 'attr'?
| |
| +-- 是 -> 返回 obj.__dict__['attr'] (结束)
| |
| +-- 否 -> 进入下一步
|
+-- 2. 检查 obj.__class__.__dict__ 是否有 'attr'?
| |
| +-- 是 -> 返回 obj.__class__['attr'] (结束)
| |
| +-- 否 -> 进入下一步
|
+-- 3. 沿着 obj.__class__.__mro__ 顺序,依次检查每个父类的 __dict__
| |
| +-- 在某个父类中找到 -> 返回该父类 __dict__['attr'] (结束)
| |
| +-- 所有父类都未找到 -> 抛出 AttributeError
实例代码验证
让我们用一个具体的例子来走一遍这个流程。
python
运行
class Dog:
# 类变量:所有 Dog 实例共享
species = "Canis lupus familiaris"
count = 0
def __init__(self, name, age):
# 实例变量:每个 Dog 实例独有
self.name = name
self.age = age
Dog.count += 1 # 每次创建实例,类变量 count 加 1
# 创建两个实例
dog1 = Dog("旺财", 3)
dog2 = Dog("小白", 5)
# --- 场景 1: 访问实例变量 ---
# 访问 dog1 的 name 属性
print(f"dog1 的名字是: {dog1.name}")
# 内部流程:
# 1. 检查 dog1.__dict__,发现 {'name': '旺财', 'age': 3},找到了 'name'。
# 2. 返回 '旺财'。流程结束。
# 3. 不会再去检查 Dog 类。
print(f"dog1.__dict__: {dog1.__dict__}")
# 输出: dog1.__dict__: {'name': '旺财', 'age': 3}
# --- 场景 2: 访问类变量 (通过实例) ---
# 访问 dog1 的 species 属性
print(f"dog1 的物种是: {dog1.species}")
# 内部流程:
# 1. 检查 dog1.__dict__,里面只有 'name' 和 'age',没有 'species'。
# 2. 流程继续,检查 dog1 的类,即 Dog 类。
# 3. 检查 Dog.__dict__,发现 {'species': 'Canis lupus familiaris', ...},找到了 'species'。
# 4. 返回 'Canis lupus familiaris'。流程结束。
print(f"Dog.__dict__ 中是否有 species: {'species' in Dog.__dict__}")
# 输出: Dog.__dict__ 中是否有 species: True
# --- 场景 3: 访问类变量 (通过类) ---
# 访问 Dog 的 count 属性
print(f"总共创建了 {Dog.count} 只狗。")
# 内部流程:
# 1. 因为是直接通过类访问,所以直接检查 Dog.__dict__。
# 2. 找到 'count',其值为 2。
# 3. 返回 2。流程结束。
# --- 场景 4: 实例“遮蔽”(Shadowing) 类变量 ---
# 给 dog1 实例也添加一个名为 species 的属性
dog1.species = "中华田园犬"
print(f"修改后,dog1 的物种是: {dog1.species}")
print(f"修改后,Dog 类的物种仍然是: {Dog.species}")
print(f"dog2 的物种仍然是: {dog2.species}")
# 内部流程分析 (访问 dog1.species):
# 1. 检查 dog1.__dict__,现在里面有 {'name': '旺财', 'age': 3, 'species': '中华田园犬'}。
# 2. 找到了 'species'。
# 3. 返回 '中华田园犬'。流程结束。它不会再去看类的 species 变量了。
# 这就是“遮蔽”效应。
print(f"修改后 dog1.__dict__: {dog1.__dict__}")
# 输出: 修改后 dog1.__dict__: {'name': '旺财', 'age': 3, 'species': '中华田园犬'}
关键规则与易错点
-
访问 vs. 赋值:
- 访问 (
obj.attr):遵循上述的查找流程。 - 赋值 (
obj.attr = value):- 这总是在实例的
__dict__中创建或修改一个属性。 - 它永远不会自动修改类变量,除非你明确指定
Dog.attr = value。 - 如果类中已有同名的类变量,
obj.attr = value会在实例中创建一个新的实例变量,从而遮蔽 (Shadow) 掉类变量。
- 这总是在实例的
- 访问 (
-
可变对象作为类变量的陷阱:当类变量是可变对象(如列表
list、字典dict)时,所有实例共享同一个对象。对它的修改会影响所有实例。python
运行
class Cat: toys = [] # 可变的类变量 def __init__(self, name): self.name = name cat1 = Cat("咪咪") cat2 = Cat("花花") cat1.toys.append("毛线球") print(f"{cat1.name} 的玩具: {cat1.toys}") # 输出: 咪咪 的玩具: ['毛线球'] print(f"{cat2.name} 的玩具: {cat2.toys}") # 输出: 花花 的玩具: ['毛线球'] (cat2 的玩具也变了!) print(f"Cat 类的玩具: {Cat.toys}") # 输出: Cat 类的玩具: ['毛线球'] # 分析:cat1.toys.append(...) 是访问操作。因为 cat1 实例没有 toys 属性,所以找到了 Cat 类的 toys 列表并对其进行了修改。
总结
- 访问
obj.attr:优先查实例,再查类,最后查父类。 - 赋值
obj.attr = value:总是在实例上创建或修改属性。 - 类变量:所有实例共享,通常用于存储所有实例共有的数据。
- 实例变量:每个实例独有,用于存储实例特有的数据。
- 可变类变量要特别小心,因为对它的修改是全局性的。
更多推荐


所有评论(0)