前面 类与对象基础 提到过:Python 类里有三种方法(实例方法、类方法、静态方法),还有一个把方法伪装成属性的 @property。这些不是”高级技巧”,而是日常写业务经常用到的工具。这一篇讲透它们各自的定位、用法、和典型场景。
一、三种方法的对比
1 2 3 4 5 6 7 8 9 10 11
| class Foo: def instance_method(self): """实例方法:第一个参数 self(当前实例)。"""
@classmethod def class_method(cls): """类方法:第一个参数 cls(类本身)。"""
@staticmethod def static_method(): """静态方法:不接收任何隐式参数。"""
|
调用:
1 2 3 4 5 6
| f = Foo()
f.instance_method() f.class_method() Foo.class_method() Foo.static_method()
|
三者的核心区别:
| 类型 |
拿到的第一个参数 |
用途 |
| 实例方法 |
self(实例) |
操作/查询单个实例 |
| 类方法 |
cls(类) |
备用构造器、访问/修改类属性 |
| 静态方法 |
无 |
逻辑上跟类相关,但不需要实例或类的工具函数 |
二、classmethod:备用构造器
最常见的用途:给类提供”另一种创建方式”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Date: def __init__(self, year, month, day): self.year, self.month, self.day = year, month, day
@classmethod def from_string(cls, s): """从 'YYYY-MM-DD' 字符串创建。""" y, m, d = map(int, s.split("-")) return cls(y, m, d)
@classmethod def today(cls): import datetime t = datetime.date.today() return cls(t.year, t.month, t.day)
d1 = Date(2025, 9, 19) d2 = Date.from_string("2025-09-19") d3 = Date.today()
|
- 常规
__init__ 只能接受一种参数形式;
- classmethod 让类拥有多个构造入口;
cls(y, m, d) 而不是 Date(y, m, d)——这样子类继承时也能用:
1 2 3 4
| class BeijingDate(Date): pass
BeijingDate.from_string("2025-09-19")
|
三、classmethod:操作类状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Widget: _count = 0
def __init__(self): Widget._count += 1
@classmethod def total(cls): return cls._count
@classmethod def reset(cls): cls._count = 0
Widget() Widget() Widget() print(Widget.total()) Widget.reset() print(Widget.total())
|
四、staticmethod:类的工具函数
什么时候用 staticmethod?——“这个函数逻辑上属于这个类,但不需要访问 self 或 cls。”
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Temperature: def __init__(self, celsius): self.celsius = celsius
@staticmethod def c_to_f(c: float) -> float: return c * 9 / 5 + 32
@staticmethod def f_to_c(f: float) -> float: return (f - 32) * 5 / 9
print(Temperature.c_to_f(100))
|
如果不放在类里,就是模块级函数——那也没错。静态方法只是把它**”归类”到类命名空间里**,让调用方一目了然是干嘛的。
建议:如果一个工具函数只跟某个类相关,用 staticmethod;如果和多个类都相关,就写成模块级函数。
五、@property:把方法伪装成属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Circle: def __init__(self, r): self.r = r
@property def area(self): return 3.14159 * self.r ** 2
@property def diameter(self): return 2 * self.r
c = Circle(3) print(c.area) print(c.diameter) c.area = 100
|
好处:
- 对外像属性,简洁;
- 对内是方法,可以有逻辑;
- 将来把字段变计算/反之,调用方代码不用改。
六、setter 和 deleter
想让 property “可写”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Celsius: def __init__(self, value): self._value = value
@property def value(self): return self._value
@value.setter def value(self, v): if v < -273.15: raise ValueError("温度不能低于绝对零度") self._value = v
@value.deleter def value(self): print("重置温度") self._value = 0
c = Celsius(20) print(c.value) c.value = 100 try: c.value = -500 except ValueError as e: print(e) del c.value
|
语法要点:三个装饰器函数同名,Python 会把它们串成 property 对象。
七、只读属性的常见用法
计算属性:
1 2 3 4 5 6 7 8 9 10 11
| class Rectangle: def __init__(self, w, h): self.w, self.h = w, h
@property def area(self): return self.w * self.h
@property def perimeter(self): return 2 * (self.w + self.h)
|
懒加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Report: def __init__(self, data): self._data = data self._cache = None
@property def summary(self): if self._cache is None: print("计算中...") self._cache = self._expensive_calculation() return self._cache
def _expensive_calculation(self): return sum(self._data)
|
Python 3.8+ 有更优雅的 functools.cached_property:
1 2 3 4 5 6 7 8 9 10
| from functools import cached_property
class Report: def __init__(self, data): self._data = data
@cached_property def summary(self): print("计算中...") return sum(self._data)
|
cached_property 第一次访问计算,之后直接返回缓存。写业务里非常好用。
八、property 用于验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class User: def __init__(self, name, age): self.name = name self.age = age
@property def age(self): return self._age
@age.setter def age(self, value): if not isinstance(value, int): raise TypeError("age 必须是 int") if not 0 <= value <= 150: raise ValueError("age 不合理") self._age = value
u = User("咖飞", 3) u.age = 4 u.age = -1 u.age = "abc"
|
九、property 的性能
property 每次访问都跑一遍函数,比直接访问属性慢一些(大概 5-10 倍)。对性能极敏感的场景(比如循环里访问几百万次)可以直接暴露字段。日常业务代码性能损耗可以忽略。
十、classmethod + property(Python 3.9-3.11)
Python 3.9 到 3.11 一度支持 @classmethod @property 叠加:
1 2 3 4 5 6 7
| class Config: _default = "prod"
@classmethod @property def env(cls): return cls._default
|
但 Python 3.12 把这个组合废弃了(因为语义混乱)。以后请写成常规 classmethod(Config.env())或者用 metaclass。
十一、”数据类”的现代做法
如果你的类主要是”装数据+少量逻辑”,用 @dataclass 更简洁,前面 类与对象基础 讲过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from dataclasses import dataclass
@dataclass class Book: title: str price: float stock: int = 0
@property def is_available(self): return self.stock > 0
def sell(self, qty=1): if qty > self.stock: raise ValueError("库存不足") self.stock -= qty
|
dataclass 帮你写好 __init__、__repr__、__eq__,你只专注业务逻辑。
十二、什么时候用什么
| 场景 |
选择 |
| 操作/查询单个实例 |
实例方法 |
| 备用构造器(多种入参形式) |
classmethod |
| 修改/查询类级别的状态 |
classmethod |
| 工具函数,逻辑上归属这个类,不需要 self / cls |
staticmethod |
| “属性但需要计算” |
@property |
| “属性且需要校验” |
property + setter |
| “计算一次就缓存” |
cached_property |
十三、一个完整例子:员工类
综合展示四种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| """员工类,展示实例方法、classmethod、staticmethod、property 的组合。"""
from dataclasses import dataclass, field from datetime import date from functools import cached_property
class Employee: _next_id = 1 company = "咖飞科技"
def __init__(self, name: str, hire_date: date, salary: float): self.id = Employee._next_id Employee._next_id += 1 self.name = name self.hire_date = hire_date self._salary = salary
def raise_salary(self, percentage: float): self._salary *= (1 + percentage / 100)
@property def salary(self): return self._salary
@property def years_of_service(self): return (date.today() - self.hire_date).days // 365
@cached_property def title(self): y = self.years_of_service if y >= 5: return "高级工程师" if y >= 2: return "工程师" return "初级工程师"
@classmethod def from_dict(cls, d: dict): return cls( name=d["name"], hire_date=date.fromisoformat(d["hire_date"]), salary=d["salary"], )
@classmethod def set_company(cls, name: str): cls.company = name
@staticmethod def is_working_day(d: date) -> bool: return d.weekday() < 5
def __repr__(self): return f"Employee(id={self.id}, name={self.name!r}, title={self.title})"
if __name__ == "__main__": e = Employee("咖飞", date(2020, 1, 1), 10000) print(e) print(f"工龄 {e.years_of_service} 年") print(f"薪资 {e.salary}")
e2 = Employee.from_dict({ "name": "小王", "hire_date": "2024-06-01", "salary": 8000, }) print(e2)
Employee.set_company("咖飞集团") print(f"当前公司 {Employee.company}")
print(f"今天上班?{Employee.is_working_day(date.today())}")
|
十四、常见陷阱
self.x 和 @property x 冲突:__init__ 里 self.x = ... 会调 setter,或者跟 property 名字冲突时报 AttributeError。
- 忘了写 setter:
c.value = 100 报 AttributeError。
- property 递归:
@x.setter 里 self.x = value 会死循环。要 self._x = value。
- classmethod 与 staticmethod 用反:需要访问 cls 就用 classmethod。
- cached_property 与继承:需要
__slots__ 时不能用 cached_property。
@staticmethod 在类里省略:Python 允许普通函数放类里当”未绑定函数”,但不推荐——加 @staticmethod 意图更清晰。
十五、小结与延伸阅读
- 三种方法:instance / class / static,分别拿到 self / cls / 无;
- classmethod 常用于备用构造器和类级操作;
- staticmethod 是”归属这个类的工具函数”;
@property 把方法伪装成属性,方便验证/计算/缓存;
- setter 用
@x.setter;
- 只读属性 + 惰性求值用
functools.cached_property;
- 数据类首选
@dataclass。
延伸阅读:
下一篇 抽象类与接口 我们讲怎么”强制”子类实现某些方法。