好的,我们来系统地拆解一下 python 中访问类变量和实例变量的完整步骤。这是一个非常核心且容易混淆的概念。
我会遵循以下结构,由浅入深,确保你彻底理解:
- 核心定义:明确什么是类变量和实例变量。
- 查找规则:详细解释 python 在访问属性时遵循的 “查找链”(lookup chain)。
- 代码演练:通过实例代码,一步步追踪查找过程。
- 修改与遮蔽:解释如何修改变量,以及 “遮蔽”(shadowing)现象。
- 可变对象陷阱:探讨当类变量是可变对象(如列表、字典)时的特殊情况。
- 总结与速查表:提供一个清晰的总结,帮助你快速决策。
1. 核心定义
类变量 (class variable):
- 定义位置:在类的内部,但在任何方法的外部。
- 所属对象:属于类本身。
- 共享性:被所有该类的实例(对象)共享。一个实例对类变量的修改(在特定条件下)会影响所有其他实例。
- 作用:通常用于定义所有实例都共有的属性或常量,比如物种(species)、默认计数等。
实例变量 (instance variable):
- 定义位置:通常在类的 __init__方法中,以self.开头。
- 所属对象:属于单个实例。
- 唯一性:每个实例都有自己独立的一份拷贝。修改一个实例的实例变量不会影响其他实例。
- 作用:用于存储每个实例独有的数据,比如名字(name)、年龄(age)等。
代码示例:
class dog:
    # 类变量:所有狗都共享这个属性
    species = "canis lupus familiaris"
    count = 0 # 用于计数狗的数量
 
    def __init__(self, name, age):
        # 实例变量:每个狗对象都有自己的 name 和 age
        self.name = name
        self.age = age
        dog.count += 1 # 每次创建实例,类变量 count 加 12. 查找规则:属性查找链 (attribute lookup chain)
当你使用 instance.attribute 的形式访问一个属性时,python 会严格按照以下顺序进行查找,直到找到为止:
查找实例自身的命名空间:
- python 首先会检查该实例对象是否拥有这个属性。你可以通过 instance.__dict__查看实例的属性字典。
- 如果在 __dict__中找到了attribute,就直接返回它的值。查找结束。
查找类的命名空间:
- 如果实例自身没有这个属性,python 会接着检查该实例所属的类是否拥有这个属性。你可以通过 class.__dict__查看类的属性字典。
- 如果在 __dict__中找到了attribute,就返回它的值。查找结束。
查找父类的命名空间(继承链):
- 如果类自身也没有,python 会沿着继承链向上,依次查找所有父类和祖先类的 __dict__。
- 这个过程遵循 method resolution order (mro),即方法解析顺序。
抛出异常:
- 如果沿着整个继承链都找不到这个属性,python 会抛出 attributeerror异常。
一句话总结: 先找实例,再找类,最后找父类。 (instance -> class -> parent class)
3. 代码演练:追踪查找过程
让我们创建一个实例,并追踪访问不同变量时的路径。
# 沿用上面的 dog 类
dog1 = dog("buddy", 3)
dog2 = dog("max", 5)场景 1:访问实例变量 dog1.name
- 查找 dog1的__dict__:{'name': 'buddy', 'age': 3}。
- 找到了 name,值为'buddy'。
- 查找停止,返回 'buddy'。
场景 2:访问类变量 dog1.species
- 查找 dog1的__dict__:{'name': 'buddy', 'age': 3}。
- 没有找到 species。
- 继续查找 dog1所属的类dog的__dict__。
- 在 dog.__dict__中找到了species,值为"canis lupus familiaris"。
- 查找停止,返回 "canis lupus familiaris"。
场景 3:访问类变量 dog.count
- 这里直接从类 dog开始查找。
- 在 dog.__dict__中找到了count,值为2。
- 查找停止,返回 2。
场景 4:访问一个不存在的变量 dog1.weight
- 查找 dog1的__dict__:{'name': 'buddy', 'age': 3}。
- 没有找到 weight。
- 继续查找 dog的__dict__。
- 也没有找到 weight。
- dog没有父类(除了- object),在- object中也找不到。
- 查找失败,抛出 attributeerror: 'dog' object has no attribute 'weight'。
4. 修改与遮蔽 (shadowing)
修改变量的行为取决于你通过什么来修改它。
4.1 修改实例变量
这是最常见的操作,直接在实例的 __dict__ 中创建或更新属性。
dog1.age = 4 # 修改 dog1 自己的 age print(dog1.age) # 输出: 4 print(dog2.age) # 输出: 5 (不受影响)
4.2 修改类变量
这里有两种方式,效果完全不同:
方式一:通过类名修改(推荐)
这会真正地修改类的 __dict__ 中的值,所有实例都会受到影响。
dog.species = "canis familiaris" # 通过类名修改 print(dog.species) # 输出: canis familiaris print(dog1.species) # 输出: canis familiaris (dog1 会查找到更新后的类变量) print(dog2.species) # 输出: canis familiaris (dog2 也会查找到更新后的类变量)
方式二:通过实例修改(导致遮蔽)
这不会修改类变量,而是会在该实例自己的 __dict__ 中创建一个同名的实例变量。
这个新创建的实例变量会 “遮蔽”(shadow)掉类变量,也就是说,之后再通过这个实例访问该变量时,会直接返回实例自己的那个,而不再去查找类。
# 此时,dog.species 是 "canis familiaris"
print(f"before shadowing, dog1.__dict__: {dog1.__dict__}") # {'name': 'buddy', 'age': 4}
 
dog1.species = "wolf" # 通过实例修改,触发遮蔽
 
print(f"after shadowing, dog1.__dict__: {dog1.__dict__}") # {'name': 'buddy', 'age': 4, 'species': 'wolf'}
print(f"dog.__dict__['species']: {dog.__dict__['species']}") # 'canis familiaris' (类变量没变!)
 
print(dog1.species) # 输出: wolf (访问的是实例自己的 species)
print(dog2.species) # 输出: canis familiaris (dog2 没有被遮蔽,访问的仍是类变量)
print(dog.species)  # 输出: canis familiaris (类变量本身没变)5. 可变对象陷阱 (mutable object pitfall)
这是一个非常经典的面试题和错误来源。当类变量是可变对象(如列表 list、字典 dict、集合 set)时,通过实例对其进行原地修改(in-place modification),会影响所有实例。
原因:因为实例和类共享同一个可变对象的引用。
代码示例:
class cat:
    # 类变量,一个空列表
    tricks = []
 
    def __init__(self, name):
        self.name = name
 
cat1 = cat("kitty")
cat2 = cat("lucy")
 
# 通过实例 cat1 向 tricks 列表添加元素
cat1.tricks.append("play dead")
 
# 查看结果
print(cat1.tricks) # 输出: ['play dead']
print(cat2.tricks) # 输出: ['play dead'] (oh no! cat2 的 tricks 也变了)
print(cat.tricks)  # 输出: ['play dead'] (实际上是类变量被修改了)
 
# 检查它们是否指向同一个对象
print(id(cat1.tricks)) # e.g., 140183245326144
print(id(cat2.tricks)) # e.g., 140183245326144 (和上面的 id 相同)
print(id(cat.tricks))  # e.g., 140183245326144 (和上面的 id 相同)陷阱分析:cat1.tricks.append(...) 这个操作,python 首先在 cat1.__dict__ 中找 tricks,没找到,然后去 cat.__dict__ 中找到了 tricks 列表。接着,它对这个找到的列表对象本身执行了 append 操作。因为所有实例和类都指向这同一个列表对象,所以大家都看到了变化。
如何避免?如果你想让每个实例都有自己独立的可变对象(比如一个空列表),你应该在 __init__ 方法中初始化它。
class cat:
    def __init__(self, name):
        self.name = name
        # 在实例化时,为每个实例创建一个独立的列表
        self.tricks = []
 
cat1 = cat("kitty")
cat2 = cat("lucy")
 
cat1.tricks.append("play dead")
 
print(cat1.tricks) # 输出: ['play dead']
print(cat2.tricks) # 输出: [] (cat2 的列表不受影响)6. 总结与速查表
| 特性 | 类变量 (class variable) | 实例变量 (instance variable) | 
|---|---|---|
| 定义位置 | 类内部,方法外部 | 通常在 __init__方法中,以self.开头 | 
| 所属对象 | 类 | 实例 | 
| 共享性 | 被所有实例共享 | 每个实例独有 | 
| 访问方式 | class.var或instance.var | instance.var | 
| 查找顺序 | 实例查找失败后,再查找类 | 优先查找实例 | 
| 修改方式 | class.var = new_value(影响所有实例) | instance.var = new_value(只影响当前实例) | 
| 通过实例修改 | instance.var = new_value会创建一个同名的实例变量,遮蔽类变量 | 直接修改实例自己的变量 | 
| 可变对象风险 | 如果是可变对象(如列表),通过实例进行原地修改( append等)会影响所有实例 | 每个实例的可变对象都是独立的,无此风险 | 
实践建议:
- 明确意图:如果一个属性对所有实例都通用,用类变量。如果每个实例都需要自己独立的一份,用实例变量。
- 修改类变量用类名:为了代码清晰,避免歧义,修改类变量时始终使用 class.variable的形式。
- 警惕可变类变量:除非你明确希望所有实例共享一个可变对象(例如,一个全局计数器),否则不要将可变对象用作类变量来存储实例相关的状态。
希望这个全面的拆解能帮助你彻底掌握类变量和实例变量的访问机制!
以上就是python中访问类变量与实例变量的完整步骤的详细内容,更多关于python访问类变量与实例变量的资料请关注代码网其它相关文章!
 
             我要评论
我要评论 
                                             
                                             
                                             
                                             
                                             
                                            
发表评论