详解 Python 中类变量与实例变量的访问分步流程
好的,我们来详细解析 Python 中访问类变量和实例变量的分步流程。这是一个非常核心且重要的概念,理解了它,你就能彻底明白为什么有时修改一个变量会影响所有实例,而有时又不会。
核心结论
在 Python 中,当你通过一个实例(例如 obj.variable)来访问一个变量时,查找顺序遵循 LEGB 规则的一个特定子集,我们可以称之为 "实例优先" 原则:
- 先找实例自己:在实例的专属命名空间(
__dict__)中查找。 - 再找类:如果实例中没有,就在实例所属的类的命名空间(
__dict__)中查找。 - 最后找父类:如果类中也没有,就沿着继承链向上,在父类的命名空间中查找,直到找到为止或引发
AttributeError。
详细分步流程与示例
我们将通过一个简单的类来演示这个过程。
python
运行
class Dog:
species = "Canis lupus familiaris" # 类变量,所有狗共享
def __init__(self, name, age):
self.name = name # 实例变量,每个狗独有
self.age = age # 实例变量,每个狗独有
# 创建两个实例
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
1. 访问实例变量 (self.name)
当执行 dog1.name 时,流程如下:
Step 1: 检查实例 dog1 的 __dict__
- Python 首先检查
dog1对象内部的字典。这个字典存储了所有属于dog1自己的属性。 - 我们可以通过
dog1.__dict__来查看:python
运行
print(dog1.__dict__) # 输出: {'name': 'Buddy', 'age': 3} - 因为
'name'这个键存在于dog1.__dict__中,Python 直接返回其对应的值'Buddy'。 - 流程结束。
2. 访问类变量 (species)
当执行 dog1.species 时,流程如下:
Step 1: 检查实例 dog1 的 __dict__
- Python 检查
dog1.__dict__:{'name': 'Buddy', 'age': 3}。 - 发现里面没有
'species'这个键。
Step 2: 检查实例所属的类 Dog 的 __dict__
- 由于在实例中未找到,Python 会向上查找,检查
dog1所属的类Dog的字典。 - 我们可以通过
Dog.__dict__来查看(为了简洁,这里只展示部分内容):python
运行
print(Dog.__dict__) # 输出包含: {'species': 'Canis lupus familiaris', '__init__': <function Dog.__init__ at 0x...>, ...} - 因为
'species'这个键存在于Dog.__dict__中,Python 返回其对应的值'Canis lupus familiaris'。 - 流程结束。
同样,当执行 dog2.species 时,它也会因为在 dog2.__dict__ 中找不到而最终在 Dog.__dict__ 中找到同一个值。
关键:修改变量的两种情况
理解访问流程后,最关键的是要区分 “修改” 和 “创建新的实例变量”。
情况一:通过实例修改可变类变量(会影响所有实例)
如果类变量是一个可变对象(如列表 list、字典 dict),通过实例对其进行 原地修改 (in-place modification),会影响所有实例。
python
运行
class Dog:
tricks = [] # 可变类变量
def __init__(self, name):
self.name = name
dog1 = Dog("Buddy")
dog2 = Dog("Max")
# 1. 访问 tricks
print(dog1.tricks) # 输出: [] (来自 Dog 类)
print(dog2.tricks) # 输出: [] (来自 Dog 类)
print(Dog.tricks) # 输出: []
# 2. 通过 dog1 对 tricks 进行原地修改 (调用 append 方法)
dog1.tricks.append("sit")
# 3. 再次访问
print(dog1.tricks) # 输出: ['sit']
print(dog2.tricks) # 输出: ['sit'] <-- 注意!dog2 的 tricks 也变了!
print(Dog.tricks) # 输出: ['sit'] <-- 实际上是类的 tricks 被修改了
为什么?
dog1.tricks的查找流程:在dog1.__dict__中没找到,于是找到了Dog类的tricks列表。dog1.tricks.append("sit")这个操作,是对 找到的那个列表对象 进行修改。它并没有创建新变量,只是在原地改变了那个列表的内容。- 因为
dog1和dog2访问的tricks都是Dog类的那个同一个列表对象,所以一个改了,大家都能看到变化。
情况二:通过实例给类变量名赋值(会创建新的实例变量)
如果通过实例给一个与类变量同名的变量 直接赋值,这不会修改类变量,而是会在该实例中创建一个全新的实例变量。
python
运行
class Dog:
species = "Canis lupus familiaris" # 类变量
dog1 = Dog()
dog2 = Dog()
# 1. 初始访问
print(dog1.species) # 输出: Canis lupus familiaris (来自 Dog 类)
print(dog2.species) # 输出: Canis lupus familiaris (来自 Dog 类)
# 2. 通过 dog1 给 species 直接赋值
dog1.species = "Wolf"
# 3. 再次访问
print(dog1.species) # 输出: Wolf (现在访问的是 dog1 的实例变量)
print(dog2.species) # 输出: Canis lupus familiaris (dog2 仍然访问类变量)
print(Dog.species) # 输出: Canis lupus familiaris (类变量本身没变)
# 查看 dog1 的 __dict__,可以看到新创建的实例变量
print(dog1.__dict__) # 输出: {'species': 'Wolf'}
为什么?
dog1.species = "Wolf"这个操作,Python 会将其解释为:“在dog1这个实例的命名空间中,创建一个名为species的新属性,并赋值为'Wolf'”。- 从此以后,当再次执行
dog1.species时,根据我们最开始的查找流程,Python 会在 Step 1 就于dog1.__dict__中找到这个新的实例变量,并返回'Wolf',它不会再去查找Dog类了。这被称为 “实例变量遮蔽(shadowing)了类变量”。
总结与最佳实践
| 操作 | 通过实例 (obj.var) |
通过类 (Class.var) |
|---|---|---|
| 访问 (读) | 先查实例 __dict__,再查类 __dict__,再查父类。 |
直接查类 __dict__,再查父类。 |
| 赋值 (写) | 创建或修改实例变量。如果变量名与类变量相同,则遮蔽类变量。 | 修改类变量。会影响所有未被遮蔽的实例。 |
| 修改可变对象 | 如果 var 是可变对象且来自类,则修改类变量,影响所有实例。 |
修改类变量,影响所有实例。 |
最佳实践建议:
-
定义默认值:类变量适合用来定义所有实例共享的默认值或常量。
-
避免可变陷阱:强烈建议不要使用可变对象(如
list,dict)作为类变量的默认值,除非你明确希望所有实例共享这个可变对象。如果需要每个实例有一个独立的可变对象(如一个空列表),应该在__init__方法中初始化它。python
运行
# 错误示范 class BadDog: toys = [] # 所有狗共享一个玩具列表 def __init__(self, name): self.name = name # 正确示范 class GoodDog: def __init__(self, name): self.name = name self.toys = [] # 每个狗都有自己的玩具列表 -
明确意图:如果你想修改类变量,为了代码清晰,最好总是使用
ClassName.var的形式,而不是obj.var,这样可以明确你的意图是修改类的属性,而不是创建一个实例属性。
更多推荐
所有评论(0)