实例属性和类属性
属性就是对象保存的数据。Python 类中最容易混淆的一点是:有些属性属于某一个实例,有些属性属于类本身并被多个实例共享。
可以先记住一句话:写在 self 上的通常是实例属性,写在类体中的通常是类属性。 选择错误会导致数据意外共享,或者修改一个对象时影响其他对象。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 创建和读取实例属性、类属性;
- 解释属性查找和同名属性遮蔽;
- 避免列表、字典等可变类属性的共享陷阱;
- 使用
ClassVar表达类级状态,并理解继承时的行为。
从例子开始
class Student:
school = "清风学校" # 类属性
def __init__(self, name: str, grade: int):
self.name = name # 实例属性
self.grade = grade
first = Student("小林", 3)
second = Student("小周", 4)
print(first.name) # 小林,每个实例不同
print(second.name) # 小周
print(first.school) # 清风学校,共享类属性
print(Student.school) # 清风学校
first 和 second 有各自的 name、grade,但都从 Student 类读取 school。
实例属性
在实例方法中使用 self.attribute = value,会在当前对象上创建或修改属性:
class Counter:
def __init__(self, start: int = 0):
self.value = start
def increment(self) -> None:
self.value += 1
first = Counter()
second = Counter(10)
first.increment()
print(first.value) # 1
print(second.value) # 10
普通实例会把这些属性存放在 __dict__ 中:
print(first.__dict__) # {'value': 1}
实例创建后也可以动态增加属性:
first.label = "主计数器"
这种灵活性适合探索和小型脚本,但业务对象最好在 __init__() 中建立完整、有效的状态,避免不同实例拥有不可预测的字段集合。
类属性
类属性写在类体中,通常用于常量、默认配置或类级统计:
class Product:
currency = "CNY"
tax_rate = 0.13
def __init__(self, price: float):
self.price = price
book = Product(49.9)
print(book.currency) # 可以通过实例读取
print(Product.currency) # 也可以通过类读取
读取 book.currency 时,如果实例没有同名属性,Python 会继续到所属类中查找。修改所有实例共享的默认配置时,应明确写 Product.currency,让代码意图更清楚。
属性查找和遮蔽
可以用简化的顺序理解普通属性读取:
- 先查实例属性;
- 再查所属类;
- 最后查基类。
class User:
role = "user"
user = User()
print(user.role) # user,来自类属性
user.role = "admin"
print(user.role) # admin,实例属性遮蔽了类属性
print(User.role) # user,类属性没有被修改
del user.role
print(user.role) # user,删除实例属性后再次读取类属性
通过实例赋值通常只会创建实例属性,不会改变类属性。通过类赋值才会修改类属性:
User.role = "member"
print(User().role) # member
描述器(例如 property)有更复杂的优先级,但日常代码先掌握上述规则即可。
最危险的陷阱:可变类属性
列表、字典和集合等可变对象如果定义在类体中,会被所有实例共享:
class BadCart:
items = []
def add(self, item: str) -> None:
self.items.append(item)
first = BadCart()
second = BadCart()
first.add("书")
print(second.items) # ['书'],通常不是预期结果
append() 修改的是类属性指向的同一个列表。正确做法是在 __init__() 中为每个实例创建新列表:
class Cart:
def __init__(self):
self.items: list[str] = []
def add(self, item: str) -> None:
self.items.append(item)
类属性使用不可变值通常没有这个问题:
class Settings:
timeout_seconds = 30
共享可变状态并非绝对错误,例如缓存或全局注册表可能确实需要共享;关键是明确写出这种设计,并处理并发、生命周期和清理问题。
用 ClassVar 表达设计意图
typing.ClassVar 告诉类型检查器:该属性属于类,不应作为数据类或普通实例字段处理:
from typing import ClassVar
class User:
total_created: ClassVar[int] = 0
def __init__(self, name: str):
self.name = name
User.total_created += 1
ClassVar 只是类型标注,不会在运行时阻止 user.total_created = 100。要保护状态,仍需通过接口、property 或其他运行时逻辑实现。
类属性与继承
子类可以读取父类的类属性,也可以创建自己的同名类属性:
class Animal:
category = "动物"
class Dog(Animal):
category = "犬科"
print(Animal.category) # 动物
print(Dog.category) # 犬科
如果通过子类修改:
class Cat(Animal):
pass
Cat.category = "猫科"
print(Animal.category) # 动物,创建/修改的是 Cat 自己的属性
但如果类属性是可变对象,子类可能仍然共享父类对象:
class Base:
tags = []
class Child(Base):
pass
Child.tags.append("child")
print(Base.tags) # ['child']
需要隔离时,在子类中重新绑定新对象,或在初始化阶段为实例创建独立数据。
类级统计和类方法
类属性很适合保存统计值,但修改它时应通过类名,避免误创建实例属性:
class Ticket:
issued: ClassVar[int] = 0
def __init__(self):
type(self).issued += 1
first = Ticket()
second = Ticket()
print(Ticket.issued) # 2
如果希望子类分别统计,可以使用 type(self);如果希望所有子类共用一个总数,则写死 Ticket.issued 更明确。配套的 @classmethod 可以提供读取或重置统计的类级接口:
class Ticket:
issued: ClassVar[int] = 0
@classmethod
def issued_count(cls) -> int:
return cls.issued
属性、默认值和数据类
数据类中的可变字段必须使用 default_factory:
from dataclasses import dataclass, field
@dataclass
class Cart:
items: list[str] = field(default_factory=list)
直接写 items: list[str] = [] 会被数据类拒绝或造成错误的共享语义。普通类同样遵循“在初始化时创建可变实例属性”的原则。
__slots__ 的影响
定义 __slots__ 后,实例通常不能再动态增加未声明的属性,也可能没有 __dict__:
class Pixel:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int):
self.x = x
self.y = y
pixel = Pixel(10, 20)
# pixel.color = "red" # AttributeError
__slots__ 不是访问限制或安全机制。它适合实例数量巨大、字段固定且确实需要节省内存的场景;使用前要考虑继承、弱引用和序列化兼容性。
选择属性归属的原则
| 问题 | 更适合实例属性 | 更适合类属性 |
|---|---|---|
| 数据是否每个对象不同? | 是 | 否 |
| 是否需要对象独立修改? | 是 | 否,共享读取 |
| 是否是常量或默认配置? | 通常不是 | 是 |
| 是否是全体对象的统计? | 通常不是 | 是 |
| 是否为列表、字典等可变对象? | 在 __init__() 中创建 | 只有明确需要共享时 |
例如,学生姓名和成绩是实例属性,学校名称可以是类属性;购物车商品列表必须是实例属性,除非你明确要做共享购物车。
常见错误
修改实例属性却以为修改了类属性
user.role = "admin" 通常只影响这个实例。需要改变默认值时写 User.role = "admin",并考虑已经存在的实例是否有同名遮蔽属性。
把可变默认值放在类体或函数参数中
类属性列表和函数默认列表都可能被调用者共享。实例数据应在 __init__() 中创建,函数则使用 None 再初始化。
通过 __dict__ 绕过属性规则
直接修改字典会跳过 property setter 和校验。除调试或序列化外,优先使用公开接口。
练习
练习一:课程注册
定义 Course 类:
name是实例属性;max_students是类属性默认值;- 每个课程有独立的
students列表; - 实现
enroll(student),超过上限时抛出异常。
练习二:对象计数
定义 Connection 类,使用类属性统计创建数量,并提供 @classmethod count() 读取统计值。思考继承后使用 Connection.count 和 cls.count 的差异。
小结
self.name = value创建实例属性,类体中的赋值创建类属性;- 读取实例属性时,实例属性通常会遮蔽同名类属性;
- 可变类属性会被多个实例共享,实例列表和字典应在
__init__()中创建; ClassVar表达类级字段的意图,但不会自动提供运行时保护;- 类属性会参与继承查找,子类可以覆盖或重新绑定;
- 类级统计、常量和默认配置适合类属性,个体状态适合实例属性;
__slots__可以限制动态属性,但不是安全机制,使用前要评估兼容性。