在 Python 中何时使用 classmethod、staticmethod 或实例方法

在B站的【408实验室】所发布的《Python完全自学教程》中(https://space.bilibili.com/157232748/lists/8219076),专门讲解了类方法、静态方法和实例方法等有关内容,为了让学习者能够更深刻理解它们,再以本文专门探讨实例方法、类方法和静态方法之间的区别,以及如何判断哪种情况该用哪个。

以下是一个简单的决策规则。

1. 决策规则

观察该方法实际触及的内容:

  • 需要实例(self) → 实例方法
  • 需要类(cls)但不需要特定实例 → @classmethod
  • 两者都不需要 → @staticmethod

实际有哪些使用场景?假设以下 create 方法,它不符合上述规则。它接收与 __init__ 相同的参数并直接传递。虽然它提供了一个不错的接口(Class.create(...)),但没有做任何构造函数尚未完成的工作:

# 为清晰起见进行了简化
@classmethod
def create(cls, amount: Decimal, currency: Currency = Currency.EUR) -> "Expense":
    return cls(amount=amount, currency=currency)

2. 当 classmethod 真正体现价值时

当类方法完成了构造函数不应承担的工作,或从不同的起点构建对象时,它才发挥了真正的作用。加入一个归一化步骤,同一个方法便立即有了存在的意义:

@classmethod
def create(cls, amount: Decimal, currency: Currency = Currency.EUR) -> "Expense":
    return cls(amount=amount.quantize(Decimal("0.01")), currency=currency)

@classmethod 的典型用法是作为替代构造函数。Python 不允许重载 __init__,因此当需要以多种方式构建对象时,每种方式就成为一个类方法。

标准库中提供了丰富的例子,例如 datetime.date

date.today()                      # 从系统时钟构建
date.fromtimestamp(1718539200)    # 从 POSIX 时间戳构建
date.fromisoformat("2026-06-16")  # 从 ISO 8601 字符串构建
date.fromordinal(739418)          # 从预推格里高利历序数构建
date.fromisocalendar(2026, 25, 1) # 从 ISO 年/周/日构建

源码:

    # 附加构造函数

    @classmethod
    def fromtimestamp(cls, t):
        "从 POSIX 时间戳(例如 time.time())构建日期。"
        if t is None:
            raise TypeError("'NoneType' object cannot be interpreted as an integer")
        y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)
        return cls(y, m, d)

    @classmethod
    def today(cls):
        "从 time.time() 构建日期。"
        t = _time.time()
        return cls.fromtimestamp(t)

    ...
    ...

以上每个方法都返回一个 date,但使用的原始材料各不相同。它们必须是类方法,因为需要 cls 来构造实例,并且返回 cls(...) 也使得子类能够正常工作。例如,如果 MyDatedate 的子类,那么 MyDate.today() 将返回 MyDate 实例,而不是 date

你会在整个生态中看到相同的模式:dict.fromkeys(...)int.from_bytes(...),以及 Pydantic 中的 Model.model_validate(...) / model_validate_json(...) 都是类方法,它们从不同的原始材料构建实例。

另一个 classmethod 的用例是类级状态:注册表、缓存、计数器。插件注册表是一个清晰的例子,因为该方法读取并修改的是属于类而非任何实例的状态:

class Handler:
    _registry: dict[str, type["Handler"]] = {}

    @classmethod
    def register(cls, name: str, handler: type["Handler"]) -> None:
        cls._registry[name] = handler

    @classmethod
    def get(cls, name: str) -> type["Handler"]:
        return cls._registry[name]


# 在类上调用,无需实例;它修改的是存活在类上的状态
Handler.register("json", JSONHandler)

3. 什么时候确实是 staticmethod

如果方法既不触及 self 也不触及 cls,它就是一个静态方法,即一个恰好因命名空间而位于类内部的普通函数。当辅助函数与类紧密耦合,且你希望 Expense.normalize(...) 读起来顺畅时,这是合理的选择。此时它成为类 API 的一部分(会出现在 dir(Expense) 中),且无需实例即可调用。

真正的静态方法比前两者更为少见,这本身就能说明一些问题。一个清晰的例子是带有颜色转换辅助方法的 Color 类:

class Color:
    def __init__(self, name: str):
        self.name = name
        self.rgb = COLOR_NAMES.get(name.upper())

    @staticmethod
    def hex2rgb(hex_value: str) -> tuple[int, int, int]:
        return tuple(int(hex_value[i:i + 2], 16) for i in (1, 3, 5))

    @staticmethod
    def rgb2hex(rgb: tuple[int, int, int]) -> str:
        return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"

hex2rgbrgb2hex 既不触及实例也不触及类。它们是纯粹的函数式转换,位于 Color 上,使得 Color.hex2rgb("#ff0000") 在与 API 其他部分并列时读起来很自然。

但这正是值得注意的信号:静态方法可能只是伪装成方法的函数,有时更诚实的做法是将其提取为模块级函数,这样更容易测试和独立使用。

4. 总结

方法类型第一个参数可访问内容常见用例
实例方法self实例及类状态修改对象状态
类方法 (@classmethod)cls仅类状态替代构造函数、注册表
静态方法 (@staticmethod)两者均不可孤立的工具/辅助函数

5. 为什么这在当下更重要

当自己编写代码时,你几乎不会无理由地添加一个方法。而当智能体(agent)编写代码时,你会得到一个看似合理却未经人为选择的结构:一个什么也不做的 create 类方法,一个本该是独立函数的静态方法,一个挂载在错误类上的辅助方法。这就需要你做出判断:该方法所完成的工作是否真正属于该类,还是这仅仅是智能体从其他代码中学到的一种模式?

放慢速度,以批判的眼光审视任何代码并提出这些问题,是值得的。随着 AI 更快地产出更多代码,我们很容易认为“看起来像 Python 就是好的 Python”。但智能体没有品味,它会欣然生成技术上正确但结构上错误的代码。

这也正是撰写此文章的原因:为你提供一个简单的决策规则,让你在审查时能在脑海中运行。

因此,请使用 AI,但要持续培养自己的知识和品味。你懂得越多,就越能更好地评判摆在你面前的代码——无论它是由人还是由智能体写的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CS实验室

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值