跳到主要内容

实例属性和类属性

属性就是对象保存的数据。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,让代码意图更清楚。

属性查找和遮蔽​

可以用简化的顺序理解普通属性读取:

  1. 先查实例属性;
  2. 再查所属类;
  3. 最后查基类。
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__ 可以限制动态属性,但不是安全机制,使用前要评估兼容性。