好的,我们来全面、系统地讲解一下 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):

  1. 首先,在该实例自己的命名空间(__dict__)中查找。dog1.__dict__ 存储了 {'name': 'Buddy', 'age': 3}
  2. 如果找不到,则到该实例所属的类的命名空间(Dog.__dict__)中查找。Dog.__dict__ 存储了 {'species': 'Canis familiaris', 'count': 1, ...}
  3. 如果还找不到,则到父类的命名空间中查找(如果存在继承关系)。
  4. 最后,如果一直找到最顶层的 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
常见用途 定义常量、计数器、所有实例的默认值 存储每个对象独有的状态

最佳实践建议:

  1. 明确访问:访问类变量时,优先使用 ClassName.variable 的形式,这样可以清晰地表达你的意图,避免歧义。
  2. 避免陷阱:避免通过实例名修改类变量。如果你发现自己在这么做,先问一下自己:“我是想修改所有实例共享的值,还是只想给这个特定实例一个独特的值?”
  3. 可变对象永远不要 使用可变对象(list, dict, set)作为类变量来存储每个实例独有的数据。如果需要每个实例都有一个可变对象的初始值,请在 __init__ 方法中初始化它。

遵循这些原则,你就可以在 Python 中准确、高效地使用类变量和实例变量了

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐