好的,我们来详细拆解一下 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': '中华田园犬'}

关键规则与易错点

  1. 访问 vs. 赋值

    • 访问 (obj.attr):遵循上述的查找流程
    • 赋值 (obj.attr = value):
      • 这总是在实例的 __dict__ 中创建或修改一个属性
      • 永远不会自动修改类变量,除非你明确指定 Dog.attr = value
      • 如果类中已有同名的类变量,obj.attr = value 会在实例中创建一个新的实例变量,从而遮蔽 (Shadow) 掉类变量。
  2. 可变对象作为类变量的陷阱:当类变量是可变对象(如列表 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:总是在实例上创建或修改属性。
  • 类变量:所有实例共享,通常用于存储所有实例共有的数据。
  • 实例变量:每个实例独有,用于存储实例特有的数据。
  • 可变类变量要特别小心,因为对它的修改是全局性的。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐