Python 类变量、实例变量与 ClassVar

1. 核心区别

实例变量

属于某一个具体对象,每个实例各自拥有一份。

通常写在 __init__ 中:

1
2
3
class Student:
def __init__(self, name: str):
self.name = name
1
2
3
4
5
s1 = Student("楠瓜")
s2 = Student("小明")

print(s1.name) # 楠瓜
print(s2.name) # 小明

判断重点不是它是否写在 __init__ 里,而是:

1
self.xxx = value

只要属性赋值给 self,就是实例变量。它也可以在其他实例方法中创建:

1
2
3
class Student:
def set_age(self, age: int):
self.age = age

不过为了结构清晰,通常会在 __init__ 中初始化实例变量。


类变量

属于类本身,由这个类的所有实例共享。

直接在类体中赋值:

1
2
class Student:
school = "北京建筑大学"

推荐通过类访问和修改:

1
2
3
print(Student.school)

Student.school = "新学校"

实例也能读取类变量:

1
2
3
4
5
s1 = Student()
s2 = Student()

print(s1.school)
print(s2.school)

2. 属性查找规则

访问:

1
s1.school

Python 会大致按照以下顺序查找:

1
2
3
4
5
先找 s1 实例自身
↓ 没找到
再找 Student 类
↓ 没找到
继续找父类

所以:

1
2
3
4
5
6
7
8
class Student:
school = "北京建筑大学"


s1 = Student()
s2 = Student()

s1.school = "楠瓜大学"

这里不是修改类变量,而是在 s1 上创建了一个同名实例变量。

1
2
3
print(s1.school)       # 楠瓜大学
print(s2.school) # 北京建筑大学
print(Student.school) # 北京建筑大学

这种现象叫做实例属性遮蔽类属性

真正修改类变量应该写:

1
Student.school = "楠瓜大学"

3. 类体中的类型标注

下面这段代码:

1
2
class Foo:
y: str

只是类型声明,表示:

Foo 的实例预计会有一个 y 属性,并且类型是 str

它没有真正赋值,也没有创建可访问的类属性。

1
2
3
4
5
class Foo:
y: str

def __init__(self, y: str):
self.y = y

此时真正创建实例变量的是:

1
self.y = y

因此:

1
2
3
4
foo = Foo("hello")

print(foo.y) # hello
print(Foo.y) # AttributeError

4. ClassVar 是什么

导入方式:

1
from typing import ClassVar

使用方式:

1
2
class Foo:
x: ClassVar[str] = "共享值"

ClassVar[str] 表示:

  • x 是类变量
  • x 的类型是 str
  • 类型检查器不应该把它当作实例字段

它主要用于帮助:

  • Pylance
  • Pyright
  • mypy
  • dataclass
  • 其他依赖类型标注的框架

它不会在运行时制造一种特殊变量。


5. 只有标注,没有赋值

1
2
class Foo:
x: ClassVar[str]

这只是声明:

x 应当是一个字符串类型的类变量。

但现在并没有真正给它值:

1
print(Foo.x)  # AttributeError

通常由当前类或子类赋值:

1
2
class Foo:
x: ClassVar[str] = "foo"

或者:

1
2
3
4
5
6
class BaseTool:
name: ClassVar[str]


class ReadTool(BaseTool):
name = "read_file"

6. 普通类型标注和 ClassVar 对比

1
2
3
4
5
6
7
8
9
10
from typing import ClassVar


class Foo:
x: ClassVar[str] = "共享" # 类变量

y: str # 实例属性的类型声明

def __init__(self, y: str):
self.y = y # 实例变量真正赋值

可以理解成:

1
2
x:属于 Foo 类
y:属于每个 Foo 对象

访问方式:

1
2
3
4
5
6
7
8
9
foo1 = Foo("对象一")
foo2 = Foo("对象二")

print(Foo.x) # 共享
print(foo1.x) # 共享
print(foo2.x) # 共享

print(foo1.y) # 对象一
print(foo2.y) # 对象二

7. 为什么抽象类常使用 ClassVar

抽象类经常要求不同子类提供固定的类级别信息:

1
2
3
4
5
6
7
8
9
10
11
from abc import ABC, abstractmethod
from typing import ClassVar


class Tool(ABC):
name: ClassVar[str]
description: ClassVar[str]

@abstractmethod
def run(self, command: str) -> str:
...

具体工具子类:

1
2
3
4
5
6
class ReadFileTool(Tool):
name = "read_file"
description = "读取文件内容"

def run(self, command: str) -> str:
return f"读取文件:{command}"

这里的 name 描述的是:

这一类工具叫什么。

而不是:

某一个工具对象叫什么。

所以适合使用类变量。


8. ClassVar 不等于抽象约束

1
2
class Tool:
name: ClassVar[str]

这不会像 @abstractmethod 一样,在运行时强迫子类定义 name

子类即使不写:

1
2
class BadTool(Tool):
pass

可能仍然能够实例化,只是在访问时出错:

1
BadTool.name  # AttributeError

因此:

1
name: ClassVar[str]

主要表达类型和设计意图。

1
2
3
@abstractmethod
def run(self):
...

才是运行时真正强制子类实现的抽象要求。


9. 可变类变量陷阱

下面的列表会被所有实例共享:

1
2
class Student:
hobbies = []
1
2
3
4
5
6
7
s1 = Student()
s2 = Student()

s1.hobbies.append("编程")

print(s1.hobbies) # ['编程']
print(s2.hobbies) # ['编程']

如果每个对象应该有独立列表,应写成实例变量:

1
2
3
class Student:
def __init__(self):
self.hobbies = []

10. 最终判断口诀

1
self.xxx = value

是实例变量。

1
2
class Foo:
xxx = value

是类变量。

1
2
class Foo:
xxx: str

只有类型声明,没有赋值;通常表示预期的实例属性。

1
2
class Foo:
xxx: ClassVar[str] = value

明确声明为字符串类型的类变量。

最后记一句:

描述“这一个对象的数据”,用实例变量。
描述“这一类对象共同的属性”,用类变量。
ClassVar 是给类型检查器看的类变量标记。