全面讲解 Python 访问类变量与实例变量的步骤
好的,我们来全面、系统地讲解一下 Python 中访问类变量和实例变量的方法、区别以及常见的陷阱。
1. 核心概念
在深入代码之前,我们首先要清晰地理解这两个概念:
-
类变量 (Class Variable):
- 定义:在类的内部,但在任何方法(包括
__init__)的外部定义的变量。 - 归属:属于类本身。这个类的所有实例(对象)共享同一份类变量。
- 用途:通常用于定义所有实例都通用的属性或常量,比如一个计数器、默认配置等。
- 定义:在类的内部,但在任何方法(包括
-
实例变量 (Instance Variable):
- 定义:通常在类的
__init__方法中,通过self.variable_name的形式定义。也可以在其他实例方法中通过self创建。 - 归属:属于特定的实例(对象)。每个实例都有自己独立的一份实例变量,互不影响。
- 用途:用于存储每个对象独有的数据,比如一个人的姓名、年龄,或一个汽车的颜色、里程数。
- 定义:通常在类的
2. 访问方式详解
我们将通过一个示例类 Dog 来展示所有访问方式。
python
运行
class Dog:
# 类变量:所有狗都共享的属性
species = "Canis familiaris"
count = 0 # 用于记录创建了多少只狗
def __init__(self, name, age):
# 实例变量:每只狗独有的属性
self.name = name
self.age = age
Dog.count += 1 # 每次创建实例,类变量 count 加 1
def bark(self):
# 在实例方法中访问实例变量和类变量
print(f"{self.name} says: Woof! I am a {Dog.species}.")
2.1 访问类变量
类变量有两种主要的访问方式:
方式一:通过类名访问 (推荐)
这是最直接、最清晰的方式。它明确地表明你正在操作的是属于类的变量。
python
运行
# 访问类变量 species
print(f"所有狗的物种是: {Dog.species}") # 输出: 所有狗的物种是: Canis familiaris
# 访问类变量 count (此时还没有创建实例)
print(f"当前创建的狗的数量是: {Dog.count}") # 输出: 当前创建的狗的数量是: 0
方式二:通过实例对象访问 (有陷阱)
你也可以通过一个类的实例来访问类变量,前提是该实例没有同名的实例变量。
python
运行
dog1 = Dog("Buddy", 3)
# 通过实例访问类变量 species
print(f"{dog1.name} 的物种是: {dog1.species}") # 输出: Buddy 的物种是: Canis familiaris
# 通过实例访问类变量 count
print(f"通过 dog1 访问,狗的数量是: {dog1.count}") # 输出: 通过 dog1 访问,狗的数量是: 1
陷阱警告:当你通过实例 赋值 一个与类变量同名的变量时,你并不是在修改类变量,而是在为这个实例 创建一个全新的实例变量。从此以后,该实例访问这个变量名时,会优先使用自己的实例变量,而不再是类变量。
python
运行
# 尝试通过实例修改类变量
dog1.species = "Wolf"
# 检查 dog1 的 species
print(f"dog1 现在的物种是: {dog1.species}") # 输出: dog1 现在的物种是: Wolf (这是它自己的实例变量)
# 检查类的 species,发现并未改变!
print(f"Dog 类的物种仍然是: {Dog.species}") # 输出: Dog 类的物种仍然是: Canis familiaris
# 检查另一个实例的 species
dog2 = Dog("Max", 5)
print(f"dog2 的物种是: {dog2.species}") # 输出: dog2 的物种是: Canis familiaris (它访问的是类变量)
2.2 访问实例变量
实例变量必须通过实例对象来访问,通常是在方法内部使用 self,或者在外部使用具体的对象名。
方式一:在类的内部通过 self 访问 (最常用)
在类的所有实例方法中,都使用 self 来访问和操作属于当前实例的变量。
python
运行
# 在上面的 bark 方法中,我们已经演示了
# self.name 访问了实例变量 name
dog1.bark() # 输出: Buddy says: Woof! I am a Canis familiaris. (注意:bark 方法里用的是 Dog.species,所以没受影响)
方式二:在类的外部通过实例名访问
当你创建了一个实例后,可以直接通过这个对象来访问它的实例变量。
python
运行
dog1 = Dog("Buddy", 3)
# 访问实例变量
print(f"狗的名字是: {dog1.name}") # 输出: 狗的名字是: Buddy
print(f"狗的年龄是: {dog1.age}") # 输出: 狗的年龄是: 3
# 修改实例变量
dog1.age = 4
print(f"狗的新年龄是: {dog1.age}") # 输出: 狗的新年龄是: 4
重要:你 不能 通过类名来访问实例变量,因为实例变量属于某个具体的实例,而不是类本身。
python
运行
try:
print(Dog.name)
except AttributeError as e:
print(f"错误: {e}") # 输出: 错误: type object 'Dog' has no attribute 'name'
3. 查找顺序:“Python 查找链” (MRO)
当你通过一个实例(比如 dog1.species)访问一个属性时,Python 会遵循以下顺序进行查找,这个顺序被称为方法解析顺序(Method Resolution Order, MRO):
- 首先,在该实例自己的命名空间(
__dict__)中查找。dog1.__dict__存储了{'name': 'Buddy', 'age': 3}。 - 如果找不到,则到该实例所属的类的命名空间(
Dog.__dict__)中查找。Dog.__dict__存储了{'species': 'Canis familiaris', 'count': 1, ...}。 - 如果还找不到,则到父类的命名空间中查找(如果存在继承关系)。
- 最后,如果一直找到最顶层的
object类都找不到,就会抛出AttributeError异常。
总结:实例变量的查找优先级 高于 类变量。
4. 可变对象作为类变量的特殊陷阱
这是一个非常经典且容易出错的场景。如果类变量是一个可变对象(如列表 list、字典 dict),通过任何实例对它进行 原地修改(in-place modification),都会影响到所有其他实例,因为它们共享的是同一个对象的引用。
错误示例:
python
运行
class Cat:
# 类变量,一个空列表
toys = []
def __init__(self, name):
self.name = name
cat1 = Cat("Kitty")
cat2 = Cat("Lucy")
# cat1 添加一个玩具
cat1.toys.append("毛线球")
# 查看 cat1 的玩具
print(f"{cat1.name} 的玩具: {cat1.toys}") # 输出: Kitty 的玩具: ['毛线球']
# 查看 cat2 的玩具,发现也被修改了!
print(f"{cat2.name} 的玩具: {cat2.toys}") # 输出: Lucy 的玩具: ['毛线球']
# 查看类的 toys 变量,也被修改了
print(f"Cat 类的玩具: {Cat.toys}") # 输出: Cat 类的玩具: ['毛线球']
原因:cat1.toys.append(...) 这个操作并没有给 cat1 创建一个新的 toys 实例变量(因为没有使用赋值 = 操作符),而是直接找到了共享的类变量列表,并对其进行了修改。
正确做法:如果你想让每个实例都有自己独立的可变对象,应该在 __init__ 方法中初始化它。
python
运行
class Cat:
def __init__(self, name):
self.name = name
# 在 __init__ 中为每个实例创建自己的 toys 列表
self.toys = []
cat1 = Cat("Kitty")
cat2 = Cat("Lucy")
cat1.toys.append("毛线球")
print(f"{cat1.name} 的玩具: {cat1.toys}") # 输出: Kitty 的玩具: ['毛线球']
print(f"{cat2.name} 的玩具: {cat2.toys}") # 输出: Lucy 的玩具: [] (不受影响)
5. 总结与最佳实践
| 特性 | 类变量 (Class Variable) | 实例变量 (Instance Variable) |
|---|---|---|
| 定义位置 | 类内部,所有方法之外 | 通常在 __init__ 方法中,使用 self |
| 归属 | 类 | 单个实例 |
| 共享性 | 所有实例共享 | 每个实例独有 |
| 访问方式 | ClassName.var (推荐)instance.var (有陷阱) |
self.var (类内部)instance.var (类外部) |
| 修改方式 | ClassName.var = new_value |
instance.var = new_value |
| 常见用途 | 定义常量、计数器、所有实例的默认值 | 存储每个对象独有的状态 |
最佳实践建议:
- 明确访问:访问类变量时,优先使用
ClassName.variable的形式,这样可以清晰地表达你的意图,避免歧义。 - 避免陷阱:避免通过实例名修改类变量。如果你发现自己在这么做,先问一下自己:“我是想修改所有实例共享的值,还是只想给这个特定实例一个独特的值?”
- 可变对象:永远不要 使用可变对象(list, dict, set)作为类变量来存储每个实例独有的数据。如果需要每个实例都有一个可变对象的初始值,请在
__init__方法中初始化它。
遵循这些原则,你就可以在 Python 中准确、高效地使用类变量和实例变量了
更多推荐
所有评论(0)