类方法、静态方法与 property

前面 类与对象基础 提到过: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") # 返回 BeijingDate 实例,不是 Date

三、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()) # 3
Widget.reset()
print(Widget.total()) # 0

四、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)) # 212.0

如果不放在类里,就是模块级函数——那也没错。静态方法只是把它**”归类”到类命名空间里**,让调用方一目了然是干嘛的。

建议:如果一个工具函数只跟某个类相关,用 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) # 28.274.. 没有括号!
print(c.diameter) # 6
c.area = 100 # ❌ AttributeError: can't set attribute

好处:

  • 对外像属性,简洁;
  • 对内是方法,可以有逻辑;
  • 将来把字段变计算/反之,调用方代码不用改

六、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) # 20
c.value = 100
try:
c.value = -500 # ValueError
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 # 会走 setter

@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 # ok
u.age = -1 # ValueError
u.age = "abc" # TypeError

九、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
# employee.py
"""员工类,展示实例方法、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())}")

十四、常见陷阱

  1. self.x@property x 冲突__init__self.x = ... 会调 setter,或者跟 property 名字冲突时报 AttributeError。
  2. 忘了写 setterc.value = 100 报 AttributeError。
  3. property 递归@x.setterself.x = value 会死循环。要 self._x = value
  4. classmethod 与 staticmethod 用反:需要访问 cls 就用 classmethod。
  5. cached_property 与继承:需要 __slots__ 时不能用 cached_property。
  6. @staticmethod 在类里省略:Python 允许普通函数放类里当”未绑定函数”,但不推荐——加 @staticmethod 意图更清晰。

十五、小结与延伸阅读

  • 三种方法:instance / class / static,分别拿到 self / cls / 无;
  • classmethod 常用于备用构造器和类级操作;
  • staticmethod 是”归属这个类的工具函数”;
  • @property 把方法伪装成属性,方便验证/计算/缓存;
  • setter 用 @x.setter
  • 只读属性 + 惰性求值用 functools.cached_property
  • 数据类首选 @dataclass

延伸阅读:

下一篇 抽象类与接口 我们讲怎么”强制”子类实现某些方法。