Python 类中的装饰器:从"属性门禁"到"类方法"
引言:装饰器,类的"多功能工具箱"
在 Python 里,@ 开头的那一行,我们叫它装饰器。它不会凭空变出魔法,而是在不修改原有代码结构的前提下,给函数或方法"额外加装功能"。
类里面最常用的装饰器,就像一台多功能工具箱里摆着的几件趁手工具:
| 装饰器 | 一句话作用 | 生活化类比 |
|---|---|---|
@classmethod | 让方法绑定"类",而不是"实例" | 全家的公共事务 |
@staticmethod | 让方法变成"类里的普通函数" | 门口的公用工具箱 |
@property | 把方法伪装成属性,读取/赋值时偷偷把关 | 店里的智能门禁 |
@dataclass | 自动生成 __init__、__repr__ 等方法 | 模板化的一键生成器 |
@abstractmethod | 只声明"必须有什么方法",强制子类实现 | 公司定的"上岗必会技能清单" |
@wraps | 自定义装饰器时,保留原函数的"身份信息" | 给代理配一张"原主身份证" |
本文会按照从常用到小众、从基础到进阶的顺序,逐个拆开讲清楚。先从最常见、也最容易搞混的 @classmethod 和 @staticmethod 讲起。
一、@classmethod:让方法绑定"类"
1.1 核心定义
普通方法的第一个参数是 self,代表"当前这个实例";而 @classmethod 修饰的方法,第一个参数是 cls,代表"当前这个类"。
一句话:
@classmethod让方法属于"类",不依赖某个具体的实例,但知道自己是哪个类。
1.2 语法格式与参数要求
class Student(object):
@classmethod
def from_string(cls, text): # 第一个参数固定是 cls(类本身)
# cls 是约定俗成的名字,类似 self,可以改名但强烈不建议
pass参数要点:
- 第一个参数必须是
cls,它由 Python 自动传入,代表"类对象"; - 调用时不需要手动传
cls,Python 会自动补上; - 方法内部可以用
cls来访问类属性,或用cls(...)来创建实例。
1.3 极简示例
示例 1:用类方法做"另类构造函数"
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
@classmethod
def from_string(cls, text):
# 从 "小明-95" 这样的字符串里拆出名字和成绩
name, score = text.split('-')
return cls(name, int(score)) # 用 cls 创建实例,而不是写死 Student
# 调用方式一:通过"类名"调用(推荐)
s = Student.from_string('小明-95')
print(s.name) # 小明
print(s.score) # 95
# 调用方式二:通过"实例"调用,也能正常工作
s2 = s.from_string('小红-88')
print(s2.name) # 小红为什么用 cls 而不是写死 Student? 因为当子类继承并调用这个方法时,cls 会自动指向子类,这样能创建出"正确的那个类"的实例。
示例 2:统计"这个类"一共创建了多少实例
class Animal(object):
count = 0 # 类属性,所有实例共享
def __init__(self):
Animal.count += 1
@classmethod
def get_count(cls):
# 用 cls.count 而不是 Animal.count,好处是子类也能复用
return cls.count
a = Animal()
b = Animal()
print(Animal.get_count()) # 21.4 适用场景
| 场景 | 说明 |
|---|---|
| 提供多种创建实例的方式 | 如 from_string、from_json、from_dict |
| 访问或修改类属性 | 如统计实例数量、修改全局配置 |
| 工厂方法 / 单例模式 | 用类方法控制"怎么创建、是否复用实例" |
二、@staticmethod:类里的"普通函数"
2.1 核心定义
@staticmethod 修饰的方法,没有 self,也没有 cls。它本质上就是一个"恰好写在类里面的普通函数",和类、实例都没有绑定关系。
一句话:
@staticmethod只是把函数"寄放"在类里,方便按类名分类调用,逻辑上它谁都不依赖。
2.2 语法格式与参数要求
class MathHelper(object):
@staticmethod
def is_even(n): # 没有 self,也没有 cls
return n % 2 == 0参数要点:
- 没有任何强制默认参数,需要什么参数就写什么参数;
- 调用时传几个参数,方法就接收几个参数,Python 不会偷偷帮你塞
self或cls; - 方法内部无法通过
self访问实例属性,也不依赖cls。
2.3 极简示例
示例 1:工具函数归类
class MathHelper(object):
@staticmethod
def is_even(n):
return n % 2 == 0
@staticmethod
def add(a, b):
return a + b
# 通过"类名"调用(推荐)
print(MathHelper.is_even(4)) # True
print(MathHelper.add(3, 5)) # 8
# 通过"实例"调用,同样可以
m = MathHelper()
print(m.is_even(7)) # False示例 2:与实例无关的通用校验
class Order(object):
def __init__(self, amount):
self.amount = amount
@staticmethod
def is_valid_amount(value):
# 校验逻辑和"某个订单"无关,谁都能用
return isinstance(value, (int, float)) and value > 0
print(Order.is_valid_amount(100)) # True
print(Order.is_valid_amount(-5)) # False2.4 适用场景
| 场景 | 说明 |
|---|---|
| 与实例、类都无关的工具函数 | 如校验、格式化、纯计算 |
| 逻辑上属于该类,但不碰数据 | 想用类名做"命名空间"来归组函数 |
| 保持接口整洁 | 让调用方知道"这个函数和订单相关" |
三、核心差异:实例方法 vs 类方法 vs 静态方法
3.1 生活化类比(记住这个就不混了)
想象一个三口之家:
- 实例方法(
self):你自己回家开门、做饭——需要知道"我"是谁,只能处理自己的私事; - 类方法(
cls):全家一起商量"装修风格""家族姓氏"——需要知道"我们家",但不需要某个具体的人; - 静态方法(无参数):门口的公用工具箱——扳手螺丝刀谁都能用,不依赖任何人,放哪都能用。
一句话总结:实例方法关心"我",类方法关心"我们这类",静态方法谁也不关心。
3.2 对比表格
| 维度 | 实例方法 | 类方法 @classmethod | 静态方法 @staticmethod |
|---|---|---|---|
| 第一个参数 | self(实例) | cls(类) | 无强制默认参数 |
| 能否访问实例属性 | ✅ 能 | ❌ 不能(没有实例) | ❌ 不能(没有实例) |
| 能否访问类属性 | ✅ 能(通过类) | ✅ 能(通过 cls) | ✅ 能(但需写死类名) |
| 典型调用方式 | 实例.方法() | 类.方法() | 类.方法() |
| 本质 | 绑定实例 | 绑定类 | 普通函数 |
3.3 一个类里同时出现三种方法
class Student(object):
school = '清华附中' # 类属性
def __init__(self, name):
self.name = name # 实例属性
def introduce(self): # 实例方法:需要 self
return f'我是 {self.name}'
@classmethod
def get_school(cls): # 类方法:需要 cls
return cls.school
@staticmethod
def is_valid_name(name): # 静态方法:谁都不需要
return len(name) > 0
s = Student('小明')
print(s.introduce()) # 我是 小明
print(Student.get_school()) # 清华附中
print(Student.is_valid_name('小明')) # True四、@property:把方法伪装成"属性门禁"
4.1 直接暴露属性的危险
class Student(object):
pass
s = Student()
s.score = 9999 # 成绩改成 9999?程序不报错,但逻辑完全错误生活化理解:仓库大门敞开,有人把标价 100 的东西改成 10000——系统不拦,但生意没法做了。
4.2 传统方案:getter/setter 方法
class Student(object):
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value使用:
s = Student()
s.set_score(60) # ✅ 正常
print(s.get_score()) # 60
s.set_score(9999) # ❌ ValueError: score must between 0 ~ 100!问题:每次都要写 set_score() / get_score(),太啰嗦,不像属性那样直观。
4.3 @property 基本用法:鱼与熊掌兼得
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value使用:
s = Student()
s.score = 60 # ✅ 像属性一样赋值,实际调用 setter
print(s.score) # 60,像属性一样读取,实际调用 getter
s.score = 9999 # ❌ ValueError,被拦截了!生活化理解:顾客按门铃(s.score = 60),门禁系统自动检查(setter 里的验证),没问题才开门。顾客感觉像直接开门,实际背后有全套安检。
4.4 装饰器背后的原理(简化版)
@property 装饰 score() 方法
↓
创建另一个装饰器 @score.setter
↓
s.score = 60 → 自动转成 s.score(60) 调用 setter
s.score → 自动转成 s.score() 调用 getter你写 s.score = 60,Python 偷偷帮你调用了 set_score 逻辑——语法简洁,功能完整。
4.5 只读属性:只给看,不给改
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2025 - self._birth # 根据 birth 自动计算使用:
s = Student()
s.birth = 2000 # ✅ 可写
print(s.age) # 25,自动算出来
s.age = 30 # ❌ AttributeError: can't set attribute生活化理解:birth 是出生日期登记处(可改),age 是自动计算的年龄显示牌(只读)——你改不了年龄,只能改出生日期,年龄自动跟着变。
只读属性的适用场景:
| 场景 | 原因 |
|---|---|
| 数据由其他字段计算得出 | 如 age = 当前年 - birth,手动改 age 会导致数据不一致 |
| 敏感数据只供查看 | 如订单号、创建时间,防止误修改 |
| 简化接口 | 调用方不需要知道这是计算出来的,直接用就行 |
4.6 致命陷阱:方法名与实例变量重名
错误示范:
class Student(object):
@property
def birth(self):
return self.birth # ❌ 危险!发生了什么?
s.birth → 调用 birth() 方法
↓
return self.birth → 又访问 s.birth
↓
又调用 birth() 方法
↓
无限递归 → RecursionError: maximum recursion depth exceeded生活化理解:你问前台"我的房间号是多少",前台说"请报房间号查询"——你报不出,前台再问,死循环了。
正确做法:内部变量加下划线
class Student(object):
@property
def birth(self):
return self._birth # ✅ 用 _birth 存实际数据
@birth.setter
def birth(self, value):
self._birth = value规则:@property 方法名用 birth,内部存数据的变量用 _birth(单下划线开头,表示"内部使用")。
4.7 实际应用案例
案例 1:温度转换器——只读属性的典型场景
class Temperature(object):
def __init__(self, celsius):
self.celsius = celsius # 触发 setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('温度不能低于绝对零度!')
self._celsius = value
@property
def fahrenheit(self):
"""华氏度,只读,自动计算"""
return self._celsius * 9 / 5 + 32
# 使用
t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0,自动转换
t.celsius = 30 # ✅ 修改摄氏度
print(t.fahrenheit) # 86.0,华氏度自动更新
t.fahrenheit = 100 # ❌ AttributeError,只读!生活化理解:温度计上摄氏度是主刻度(可调),华氏度是副刻度(自动换算显示)——你调不了副刻度,只能调主刻度,副刻度跟着变。
案例 2:电商订单——金额计算与验证
class Order(object):
def __init__(self, price, quantity):
self.price = price
self.quantity = quantity
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError('价格不能为负数!')
self._price = value
@property
def quantity(self):
return self._quantity
@quantity.setter
def quantity(self, value):
if not isinstance(value, int) or value <= 0:
raise ValueError('数量必须是正整数!')
self._quantity = value
@property
def total(self):
"""订单总额,只读,自动计算"""
return self._price * self._quantity
@property
def discount_total(self):
"""满100打9折,只读"""
if self.total >= 100:
return self.total * 0.9
return self.total
# 使用
order = Order(50, 3)
print(order.total) # 150
print(order.discount_total) # 135.0,自动打折
order.quantity = 1 # 修改数量
print(order.total) # 50,自动重算
print(order.discount_total) # 50,不满足打折条件
order.price = -10 # ❌ ValueError: 价格不能为负数!生活化理解:收银台自动算总价和折扣——你只输入单价和数量,总额和折后价自动显示,改任何一个数字,结果立刻刷新。
五、@dataclass:数据类,省掉一堆"样板代码"
5.1 核心作用
写一个"存数据"的类,通常要手写 __init__、__repr__、__eq__ 这些样板代码。@dataclass 会自动帮你生成它们,让你只关注"字段是什么"。
一句话:
@dataclass是"数据容器类"的一键生成器——声明字段,剩下的交给它。
5.2 极简示例
不用 @dataclass 之前:
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self): # 手动写打印格式
return f'Point(x={self.x}, y={self.y})'
def __eq__(self, other): # 手动写相等判断
return self.x == other.x and self.y == other.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1) # Point(x=1, y=2)
print(p1 == p2) # True用 @dataclass 之后:
from dataclasses import dataclass
@dataclass
class Point:
x: int # 类型注解,同时声明字段
y: int
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1) # Point(x=1, y=2) —— 自动生成 __repr__
print(p1 == p2) # True —— 自动生成 __eq__
print(p1.x, p1.y) # 1 2 —— 自动生成 __init__三行代码顶掉了原来十几行样板代码。
5.3 常用参数与场景
from dataclasses import dataclass, field
@dataclass
class Config:
host: str = 'localhost' # 有默认值
port: int = 8080
tags: list = field(default_factory=list) # 可变默认值要这样写(见误区)| 场景 | 说明 |
|---|---|
| 配置文件、DTO、返回值模型 | 纯数据,需要打印和比较 |
| 需要"冻结"的数据 | 用 @dataclass(frozen=True) 变成只读 |
| 大量重复样板代码 | 减少手写 __init__/__repr__ 的负担 |
六、@abstractmethod:抽象方法,给子类立"规矩"
6.1 核心作用
@abstractmethod 用来声明一个"必须被实现,但父类不实现"的方法。配合 ABC(抽象基类),可以强制所有子类都实现某些方法,否则子类无法实例化。
一句话:
@abstractmethod是公司定的"上岗必会技能清单"——不掌握这些技能,就不给你发工牌(不能创建实例)。
6.2 极简示例
from abc import ABC, abstractmethod
class Animal(ABC): # 继承 ABC,表示这是一个抽象基类
@abstractmethod
def speak(self): # 只声明"必须有 speak",不写实现
pass
# a = Animal() # ❌ TypeError: Can't instantiate abstract class Animal
class Dog(Animal):
def speak(self): # ✅ 实现了抽象方法
print('汪汪')
class Cat(Animal):
def speak(self):
print('喵喵')
d = Dog()
d.speak() # 汪汪如果某个子类忘了实现 speak,创建实例时就会直接报错:
class Fish(Animal):
pass
# f = Fish() # ❌ TypeError: Can't instantiate abstract class Fish这样就把"漏实现方法"的错误,从"运行到一半才崩"提前到了"定义类时立刻发现"。
6.3 适用场景
| 场景 | 说明 |
|---|---|
| 定义统一接口/协议 | 各种动物都要会 speak,但叫声不同 |
| 插件/驱动架构 | 规定插件必须实现 load、run 等方法 |
| 框架的基类 | Django、SQLAlchemy 等大量使用抽象基类 |
七、@wraps:自定义装饰器时,保留函数的"身份"
7.1 核心作用
当你自己写一个装饰器去包装类里的方法时,被包装的方法会丢失原本的名字和文档字符串(__name__、__doc__ 等元信息)。@wraps 就是用来"把身份信息搬回来"的。
一句话:
@wraps是给"代理"配一张"原主身份证",让别人问"你是谁"时,报的是原函数的名字。
7.2 极简示例
不写 @wraps 会怎样:
def log(func):
def wrapper(*args, **kwargs):
print(f'调用中...')
return func(*args, **kwargs)
return wrapper
class Calculator(object):
@log
def add(self, a, b):
'加法运算'
return a + b
print(Calculator.add.__name__) # wrapper ❌ 名字丢了
print(Calculator.add.__doc__) # None ❌ 文档丢了加上 @wraps 修复:
from functools import wraps
def log(func):
@wraps(func) # 关键:保留 func 的名字、文档等元信息
def wrapper(*args, **kwargs):
print(f'调用中...')
return func(*args, **kwargs)
return wrapper
class Calculator(object):
@log
def add(self, a, b):
'加法运算'
return a + b
print(Calculator.add.__name__) # add ✅ 名字回来了
print(Calculator.add.__doc__) # 加法运算 ✅ 文档回来了7.3 适用场景
| 场景 | 说明 |
|---|---|
| 自己写日志/计时装饰器 | 打印函数名、读文档时才不会乱 |
| 编写框架 / 工具库 | 让 IDE、文档生成器能正确识别方法 |
| 调试和反射 | inspect、help() 依赖这些元信息 |
八、常见误区与避坑指南
8.1 误区一:混淆 @classmethod 和 @staticmethod
class Student(object):
school = '一中'
@staticmethod
def get_school_static():
return Student.school # 只能写死类名,子类继承后就读不到子类的 school
@classmethod
def get_school_class(cls):
return cls.school # cls 自动指向正确的类判断口诀:方法里要不要知道"是哪个类"?
- 要访问/创建"当前类"(尤其涉及继承)→ 用
@classmethod; - 纯粹是个通用工具函数,和类无关 → 用
@staticmethod。
8.2 误区二:把 @staticmethod 当成"不能调用的方法"
class A(object):
@staticmethod
def f(x):
return x
a = A()
print(a.f(1)) # ✅ 通过实例调用也完全可以
print(A.f(1)) # ✅ 通过类调用也完全可以@staticmethod 不是"禁止通过实例调用",它只是"不接收 self/cls"。两种方式都能调。
8.3 误区三:@classmethod 里把 cls 当 self 用
class A(object):
name = '类属性'
@classmethod
def bad(cls):
return cls.instance_attr # ❌ 类方法里根本没有实例属性
@classmethod
def good(cls):
return cls.name # ✅ 只能访问类属性cls 是类对象,不是实例,所以访问不到实例属性(那些要靠 self 的东西)。
8.4 误区四:@property 忘记写 setter,却试图赋值
class Student(object):
@property
def score(self):
return self._score
s = Student()
s.score = 60 # ❌ AttributeError: can't set attribute原因:只写了 getter,没写 setter,这是只读属性。修正:补上 @score.setter。
8.5 误区五:@property 的 setter 里又调用 self.score
@score.setter
def score(self, value):
self.score = value # ❌ 又触发 setter,无限递归!修正:操作内部变量 _score:
@score.setter
def score(self, value):
self._score = value # ✅8.6 误区六:在 __init__ 里绕过 setter
class Student(object):
def __init__(self, score):
self._score = score # ❌ 直接操作内部变量,绕过验证
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if value < 0 or value > 100:
raise ValueError('invalid')
self._score = value
s = Student(9999) # 成功!验证失效了修正:__init__ 里也要走 setter:
def __init__(self, score):
self.score = score # ✅ 触发 setter 验证8.7 误区七:@dataclass 的可变默认值写错
from dataclasses import dataclass
@dataclass
class Bad:
tags: list = [] # ❌ 所有实例共享同一个 list,改一个全乱修正:用 field(default_factory=list):
from dataclasses import dataclass, field
@dataclass
class Good:
tags: list = field(default_factory=list) # ✅ 每个实例独立一个 list8.8 误区八:@abstractmethod 忘记继承 ABC
from abc import abstractmethod
class Animal(object): # ❌ 忘了继承 ABC
@abstractmethod
def speak(self):
pass
# a = Animal() # 居然能创建!抽象方法失效了修正:必须继承 ABC:
from abc import ABC, abstractmethod
class Animal(ABC): # ✅
@abstractmethod
def speak(self):
pass8.9 误区九:自定义装饰器不写 @wraps,导致方法名错乱
def timer(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
class A(object):
@timer
def run(self):
pass
print(A.run.__name__) # wrapper ❌ 本应是 run修正:在 wrapper 上补 @wraps(func)(见第七节)。
九、知识链条:一张图串起所有类装饰器
普通方法(self):只关心"我"这个实例
↓
需要操作类本身、提供另类构造 → @classmethod(cls)
需要放一个无关的通用函数 → @staticmethod(无参数)
↓
需要给属性加"门禁"(验证/只读/计算) → @property
↓
需要快速生成数据容器类 → @dataclass
↓
需要强制子类实现统一接口 → @abstractmethod(配合 ABC)
↓
自己写装饰器时保留函数身份 → @wraps选型口诀:
- 关心"我" → 实例方法
- 关心"我们这类" →
@classmethod - 谁都不关心 →
@staticmethod - 要给属性把关 →
@property - 存数据的类 →
@dataclass - 定接口规矩 →
@abstractmethod - 写装饰器 →
@wraps
十、实战练习
练习 1:用 @classmethod 实现"另类构造"
给 Student 类加一个类方法 from_dict,能从一个字典创建实例:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
# 你的代码:实现 from_dict
# 测试
s = Student.from_dict({'name': '小明', 'score': 90})
if s.name == '小明' and s.score == 90:
print('测试通过!')
else:
print('测试失败!')参考答案
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
@classmethod
def from_dict(cls, d):
return cls(d['name'], d['score']) # 用 cls 创建实例练习 2:用 @property 实现只读属性
给 Screen 对象加上 width、height 属性,以及只读属性 resolution(分辨率 = 宽 × 高):
class Screen(object):
pass
# 测试
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
print('测试通过!')
else:
print('测试失败!')参考答案
class Screen(object):
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError('宽度必须为正数!')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError('高度必须为正数!')
self._height = value
@property
def resolution(self):
"""分辨率 = 宽 × 高,只读"""
return self._width * self._height练习 3:用 @dataclass 简化数据类
把下面的 Book 类改写成 @dataclass 版本,要求能打印、能比较相等:
class Book(object):
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
# 你的代码:用 @dataclass 改写参考答案
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
price: float
b1 = Book('活着', '余华', 28.5)
b2 = Book('活着', '余华', 28.5)
print(b1) # Book(title='活着', author='余华', price=28.5)
print(b1 == b2) # True练习 4:用 @abstractmethod 定接口
定义一个抽象基类 Shape,要求所有图形都必须实现 area 方法,然后让 Circle、Square 实现它:
# 你的代码参考答案
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return math.pi * self.r ** 2
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Circle(2).area()) # 约 12.566
print(Square(3).area()) # 9十一、小结
@classmethod:第一个参数是cls,绑定"类",用于另类构造、操作类属性;@staticmethod:没有self/cls,就是"类里的普通函数",用于无关的通用工具;- 核心区别:实例方法关心"我"、类方法关心"我们这类"、静态方法谁都不关心;
@property:把方法伪装成属性,读取/赋值时偷偷把关,核心价值是控制访问;@dataclass:自动生成__init__/__repr__/__eq__,是数据容器类的一键生成器;@abstractmethod:配合ABC,强制子类实现统一接口,把错误提前到定义时发现;@wraps:自定义装饰器时保留原函数的名字、文档等身份信息;- 关键避坑:
@property内部变量用_xxx防递归;@dataclass可变默认值用field(default_factory=...);@abstractmethod必须继承ABC;写装饰器别忘@wraps。
记住口诀:关心"我"用 self,关心"类"用 cls,谁都不关心用 static;管属性找 property,存数据找 dataclass,定规矩找 abstractmethod,写装饰器别忘 wraps。 掌握这七件工具,Python 类的设计就能又安全、又清晰、又省心。