279 lines
7.3 KiB
Markdown
279 lines
7.3 KiB
Markdown
---
|
|
sources: [summaries/07_Advanced_Topics__00_Overview.md, summaries/05_Decorated_methods.md]
|
|
brief: staticmethod 与 classmethod 是用于定义类级方法行为的 Python 内置装饰器。
|
|
---
|
|
|
|
# Python staticmethod 与 classmethod
|
|
|
|
`@staticmethod` 与 `@classmethod` 是 Python 类定义中常用的内置装饰器,用于改变方法与实例、类之间的绑定方式。它们都把函数放在类的命名空间中,但对调用时自动传入的参数有不同规则。
|
|
|
|
相关来源:[[summaries/05_Decorated_methods]]
|
|
|
|
## 核心区别
|
|
|
|
在普通实例方法中,第一个参数通常是 `self`,表示当前实例:
|
|
|
|
```python
|
|
class Foo:
|
|
def bar(self):
|
|
print(self)
|
|
```
|
|
|
|
调用实例方法时:
|
|
|
|
```python
|
|
f = Foo()
|
|
f.bar()
|
|
```
|
|
|
|
Python 会自动把实例 `f` 作为第一个参数传入 `bar()`。
|
|
|
|
而 `@staticmethod` 和 `@classmethod` 改变了这种默认绑定方式:
|
|
|
|
```python
|
|
class Foo:
|
|
@staticmethod
|
|
def spam(a):
|
|
...
|
|
|
|
@classmethod
|
|
def grok(cls, a):
|
|
...
|
|
```
|
|
|
|
- `@staticmethod`:不会自动接收实例 `self`,也不会自动接收类 `cls`。
|
|
- `@classmethod`:自动接收类对象作为第一个参数,通常命名为 `cls`。
|
|
|
|
相关概念:Python面向对象编程、Python装饰器、self与cls
|
|
|
|
## `@staticmethod`:静态方法
|
|
|
|
`@staticmethod` 用于定义静态方法。静态方法属于类,但不依赖具体实例,也不依赖类对象本身。
|
|
|
|
示例:
|
|
|
|
```python
|
|
class Foo:
|
|
@staticmethod
|
|
def bar(x):
|
|
print('x =', x)
|
|
|
|
Foo.bar(2)
|
|
```
|
|
|
|
输出:
|
|
|
|
```text
|
|
x = 2
|
|
```
|
|
|
|
这里调用 `Foo.bar(2)` 时,Python 不会额外传入 `self` 或 `cls`,参数 `x` 就是调用者显式传入的 `2`。
|
|
|
|
## 静态方法适合什么场景
|
|
|
|
静态方法适合表示“逻辑上属于这个类,但不需要访问实例状态或类状态”的函数。
|
|
|
|
常见用途包括:
|
|
|
|
- 类内部的辅助函数;
|
|
- 与类相关的工具逻辑;
|
|
- 管理实例创建、资源、持久化、锁等支持代码;
|
|
- 某些设计模式中的类级工具函数。
|
|
|
|
例如,一个类可能需要一些格式化、校验、转换等辅助逻辑。如果这些逻辑不需要访问 `self` 或 `cls`,可以定义为静态方法。
|
|
|
|
## `@classmethod`:类方法
|
|
|
|
`@classmethod` 用于定义类方法。类方法调用时会自动接收类对象作为第一个参数,通常命名为 `cls`。
|
|
|
|
示例:
|
|
|
|
```python
|
|
class Foo:
|
|
def bar(self):
|
|
print(self)
|
|
|
|
@classmethod
|
|
def spam(cls):
|
|
print(cls)
|
|
```
|
|
|
|
调用:
|
|
|
|
```python
|
|
f = Foo()
|
|
f.bar() # 打印实例 f
|
|
Foo.spam() # 打印类 Foo
|
|
```
|
|
|
|
普通实例方法中的 `self` 指向对象实例;类方法中的 `cls` 指向类本身。
|
|
|
|
这使得类方法可以访问类对象,并用类对象创建新实例或操作类级状态。
|
|
|
|
## 类方法最常见用途:替代构造器
|
|
|
|
在 [[summaries/05_Decorated_methods]] 中,`@classmethod` 最重要的用途是定义“替代构造器”。
|
|
|
|
例如,普通构造器可能要求传入年月日:
|
|
|
|
```python
|
|
class Date:
|
|
def __init__(self, year, month, day):
|
|
self.year = year
|
|
self.month = month
|
|
self.day = day
|
|
```
|
|
|
|
如果希望提供一个“创建今天日期”的构造方式,可以写成类方法:
|
|
|
|
```python
|
|
class Date:
|
|
def __init__(self, year, month, day):
|
|
self.year = year
|
|
self.month = month
|
|
self.day = day
|
|
|
|
@classmethod
|
|
def today(cls):
|
|
tm = time.localtime()
|
|
return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)
|
|
```
|
|
|
|
调用:
|
|
|
|
```python
|
|
d = Date.today()
|
|
```
|
|
|
|
这里 `today()` 不需要调用者手动准备年月日,而是由类方法内部根据当前时间构造对象。
|
|
|
|
相关概念:[[concepts/替代构造器]]、封装
|
|
|
|
## 为什么类方法适合继承
|
|
|
|
类方法的一个重要优点是继承友好。
|
|
|
|
如果在类方法中使用:
|
|
|
|
```python
|
|
return cls(...)
|
|
```
|
|
|
|
而不是:
|
|
|
|
```python
|
|
return Date(...)
|
|
```
|
|
|
|
那么当子类调用该方法时,`cls` 会自动变成子类。
|
|
|
|
示例:
|
|
|
|
```python
|
|
class Date:
|
|
@classmethod
|
|
def today(cls):
|
|
tm = time.localtime()
|
|
return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)
|
|
|
|
class NewDate(Date):
|
|
pass
|
|
|
|
d = NewDate.today()
|
|
```
|
|
|
|
此时 `NewDate.today()` 中的 `cls` 是 `NewDate`,所以返回的是 `NewDate` 实例,而不是写死的 `Date` 实例。
|
|
|
|
这正是 `@classmethod` 在对象构造中比静态方法或硬编码类名更灵活的地方。
|
|
|
|
相关概念:Python继承、面向对象设计
|
|
|
|
## 实践示例:`Portfolio.from_csv()`
|
|
|
|
在 [[summaries/05_Decorated_methods]] 的练习中,`@classmethod` 被用于重构 `Portfolio` 对象的创建过程。
|
|
|
|
原来的设计中,读取 CSV、解析数据、创建 `Stock` 对象、创建 `Portfolio` 对象的逻辑分散在外部函数中。这会导致责任不清晰。
|
|
|
|
改进后的设计是让 `Portfolio` 自己负责从 CSV 创建实例:
|
|
|
|
```python
|
|
class Portfolio:
|
|
def __init__(self):
|
|
self.holdings = []
|
|
|
|
def append(self, holding):
|
|
if not isinstance(holding, stock.Stock):
|
|
raise TypeError('Expected a Stock instance')
|
|
self.holdings.append(holding)
|
|
|
|
@classmethod
|
|
def from_csv(cls, lines, **opts):
|
|
self = cls()
|
|
portdicts = fileparse.parse_csv(lines,
|
|
select=['name','shares','price'],
|
|
types=[str,int,float],
|
|
**opts)
|
|
|
|
for d in portdicts:
|
|
self.append(stock.Stock(**d))
|
|
|
|
return self
|
|
```
|
|
|
|
使用方式:
|
|
|
|
```python
|
|
with open('Data/portfolio.csv') as lines:
|
|
port = Portfolio.from_csv(lines)
|
|
```
|
|
|
|
这个例子体现了类方法的典型价值:
|
|
|
|
- 将对象创建逻辑封装进类;
|
|
- 让外部代码不必知道类的内部构造过程;
|
|
- 保持类型检查和内部一致性;
|
|
- 使用 `cls()` 支持未来的子类扩展。
|
|
|
|
相关概念:对象构造、类型检查、封装
|
|
|
|
## 选择 `staticmethod` 还是 `classmethod`
|
|
|
|
可以用以下判断标准:
|
|
|
|
### 使用 `@staticmethod` 的情况
|
|
|
|
当方法满足以下条件时,可以考虑静态方法:
|
|
|
|
- 逻辑属于这个类的语义范围;
|
|
- 不需要访问实例属性;
|
|
- 不需要访问类对象;
|
|
- 只是一个放在类中的辅助函数。
|
|
|
|
### 使用 `@classmethod` 的情况
|
|
|
|
当方法满足以下条件时,应优先考虑类方法:
|
|
|
|
- 需要知道当前调用的类;
|
|
- 需要创建当前类或子类的实例;
|
|
- 需要实现替代构造器;
|
|
- 需要与继承机制协同工作;
|
|
- 需要访问或修改类级状态。
|
|
|
|
## 对比总结
|
|
|
|
| 方法类型 | 装饰器 | 自动传入参数 | 常见用途 |
|
|
|---|---|---|---|
|
|
| 实例方法 | 无 | `self`,当前实例 | 操作实例状态 |
|
|
| 静态方法 | `@staticmethod` | 无 | 类相关辅助函数 |
|
|
| 类方法 | `@classmethod` | `cls`,当前类 | 替代构造器、继承友好的类级逻辑 |
|
|
|
|
## 关键理解
|
|
|
|
`staticmethod` 和 `classmethod` 都是类定义中的方法组织工具,但它们表达的设计意图不同:
|
|
|
|
- `@staticmethod` 表示“这个函数与类有关,但不需要类或实例参与”;
|
|
- `@classmethod` 表示“这个方法作用于类本身,并且可能需要根据当前类创建对象”。
|
|
|
|
在面向对象设计中,`@classmethod` 尤其适合把对象创建逻辑放回类内部,使代码更清晰、更封装,也更适合继承扩展。
|
|
|
|
See also: [[summaries/07_Advanced_Topics__00_Overview]] |