python面向对象-面向对象进阶
2.3、类方法(Class Method)
常见用途
| 场景 | 示例 |
|---|---|
| 备选构造器(工厂方法) | datetime.fromtimestamp() |
| 统计/配置 等需要类级共享数据 | 记录实例个数、全局默认配置 |
| 在子类中自动拿到正确子类 | 避免硬编码类名 |
实战示例
1.工厂方法(备用构造器)
class Person: species = "Homo sapiens"
def __init__(self, name): self.name = name
@classmethod def set_species(cls, new_species): cls.species = new_species # 修改类变量 return cls # 便于链式调用
@classmethod def from_dict(cls, data): return cls(data["name"]) # 工厂方法
# 使用Person.set_species("Modern human") # 类名调用p = Person.from_dict({"name": "Alice"}) # 工厂方法print(Person.species, p.name) # Modern human Alice方法内部用 cls(data["name"]) 调用了真正的构造函数 __init__,并返回新建好的实例。
调用者无需直接写 Person(...),只要传字典即可:p = Person.from_dict({"name": "Alice"})等价于p = Person("Alice")
- 子类继承时自动拿到正确子类
class Base: label = "base"
@classmethod def create(cls): print(f"Creating {cls.label}") return cls()
class Derived(Base): label = "derived"
Base.create() # Creating baseDerived.create() # Creating derived注意:如果用 Base() 硬编码,子类就得不到正确类型。
class Base: label = "base" @classmethod def create(cls): print(f"Creating {cls.label}") return Base() # ← 写死成 Base
class Derived(Base): label = "derived"
>>> Derived.create() # 虽然 cls 是 Derived,但永远 new Base()Creating derived # 打印的是子类标签,可对象却是 Base<__main__.Base object at 0x...>在类方法里 永远不要硬编码类名去实例化,应当用 cls(...),保证“谁调用,就生成谁类型的对象”。
容易被忽略的细节
| 要点 | 说明 |
|---|---|
| 描述器协议 | classmethod 对象实现了 __get__,因此只能 挂在类属性上;如果你把它赋值给实例属性,再访问时不会触发描述器,得到的只是裸函数。 |
| 不能跟 staticmethod 混用 | @classmethod 必须紧贴函数,顺序错会失效:@classmethod @staticmethod → 报错或行为异常。 |
| 绑定行为 | 类方法一旦通过 子类 访问,cls 就是 子类本身,这让它在框架代码里特别有用。 |
| 与元类配合 | 在元类里用 classmethod 可以给 元类自身 定义类方法(此时 cls 是元类)。 |
一句话总结 classmethod 把函数变成 “属于类” 的方法,调用时自动把 类本身 作为第一个参数注入,最常用的场景是 工厂方法 和 需要动态拿到真实子类 的框架代码。
2.4、静态方法(@staticmethod)
常见用途
| 场景 | 示例 |
|---|---|
| 工具函数 与类相关,但不需要访问类或实例状态 | 数据校验、单位换算、格式转换 |
| 把旧函数“挂”进类命名空间 | 保持代码组织结构清晰 |
| 接口/协议一致性 | 与 classmethod 一起提供多态入口 |
实战示例
1.工具函数
class MathUtils: pi = 3.1415926
@staticmethod def circle_area(r): return MathUtils.pi * r ** 2 # 显式引用类变量
@staticmethod def is_even(n): return n % 2 == 0
# 使用print(MathUtils.circle_area(3)) # 28.274...print(MathUtils.is_even(7)) # False2.输入校验(与枚举配合)
from enum import Enum
class Color(Enum): RED = 1 GREEN = 2 BLUE = 3
class Paint: @staticmethod def check_color(value): if value not in Color: raise ValueError("非法颜色") return value
# 使用Paint.check_color(Color.RED) # okPaint.check_color(99) # ValueError3.组织代码:把全局函数收进类
import hashlib
class Utils: @staticmethod def md5(data: bytes) -> str: return hashlib.md5(data).hexdigest()
@staticmethod def sha256(data: bytes) -> str: return hashlib.sha256(data).hexdigest()
# 使用digest = Utils.md5(b"hello")容易被忽略的细节
| 要点 | 说明 |
|---|---|
| 描述器协议 | 与 classmethod 一样,只能 挂在类属性上;赋值给实例属性再访问时,不会触发描述器,得到的是裸函数。 |
| 无绑定行为 | 静态方法 不会自动传 self 或 cls,所以内部如果 硬编码类名,子类化时可能失效;此时应改用 classmethod。 |
| 与继承的关系 | 静态方法 不参与动态派生,子类覆盖后,父类版本 不会自动引用子类。 |
| 单元测试友好 | 因为无隐式状态,纯函数式 的静态方法很容易做单元测试。 |
一句话总结 staticmethod 把函数 装进类的命名空间,既不收实例也不收类,最适合写 与类逻辑相关、但完全无状态的纯工具函数。
staticmethod vs classmethod vs 实例方法
| 特性 | 实例方法 | classmethod | staticmethod |
|---|---|---|---|
| 第一个参数 | self(实例) | cls(类) | 无 |
| 能否访问实例属性 | ✅ | ❌(除非手动给实例) | ❌ |
| 能否访问类属性 | ✅ | ✅ | ✅(需硬编码类名) |
| 调用方式 | obj.f() | Cls.f() 或 obj.f() | Cls.f() 或 obj.f() |
| 继承后动态性 | —— | ✅ 自动拿到最派生类 | ❌ 无动态性 |
| 状态依赖 | 依赖实例 | 依赖类 | 无状态,纯函数 |
@property、@xxx.setter、@xxx.deleter 装饰器
基本语法(4 种形态)
class C: @property def x(self): ... # 读
@x.setter def x(self, value): ... # 写
@x.deleter def x(self): ... # 删
@x.getter # 极少用:覆盖读方法 def x(self): ...最小完整示例
class Celsius: def __init__(self, temp=0): self._temp = temp # 真正的存储位置
@property # ① 读 def temperature(self): """读取摄氏度""" return self._temp
@temperature.setter # ② 写 def temperature(self, value): if value < -273.15: raise ValueError("低于绝对零度") self._temp = value
@temperature.deleter # ③ 删 def temperature(self): print("删除温度") del self._temp
# -------- 使用 --------c = Celsius(25)print(c.temperature) # 25 触发 getterc.temperature = 100 # 触发 setterprint(c.temperature) # 100del c.temperature # 删除温度 触发 deleter常见用法场景
| 场景 | 典型代码片段 |
|---|---|
| 惰性计算 / 缓存 | 第一次访问时计算,以后直接返回缓存值 |
| 校验赋值 | age 必须在 0~120 之间 |
| 只读派生属性 | 只提供 getter,不定义 setter |
| 动态计算 / 触发事件 | 设置坐标时自动重绘窗口 |
| 兼容重构 | 把原本公开字段改成方法,但调用方零改动 |
实战进阶示例
- 惰性计算 + 缓存
class Circle: def __init__(self, radius): self.radius = radius self._area = None # 缓存槽
@property def area(self): if self._area is None: print("computing...") self._area = 3.1415926 * self.radius ** 2 return self._area
@area.setter def area(self, value): raise AttributeError("can't set area")- 带校验的可写属性
class Person: def __init__(self, age=0): self.age = age # 走 setter 校验
@property def age(self): return self._age
@age.setter def age(self, value): if not (0 <= value <= 120): raise ValueError("age out of range") self._age = value- 兼容重构:把字段换成 property
老代码:
class Product: def __init__(self): self.price = 100 # 公开字段新需求:售价必须 ≥ 0,且自动打九折。
零破坏重构:
class Product: def __init__(self): self._price = 100
@property def price(self): return self._price * 0.9
@price.setter def price(self, value): if value < 0: raise ValueError("price must >= 0") self._price = value细节与陷阱
| 注意点 | 说明 |
|---|---|
| 优先级别 | property 是 数据描述器,实例同名属性会被 覆盖(无法直接赋值),除非实例用 __dict__ 强行绕过。 |
| 继承行为 | 子类可 通过同名函数 + 装饰器 覆盖父 property 的某一部分(只覆盖 setter 等)。 |
| 文档字符串 | 默认继承 getter 方法的 __doc__,可用 @x.getter 重新指定。 |
| 通过类调用 | Person.full_name 返回 property 自身,可用于 introspection。 |
| 性能 | 每次访问都有一次 Python 级函数调用,对 超热路径 可手动缓存或改用 __slots__ + 描述器优化。 |
一句话总结 property 让你 用属性语法 做 方法事情:读、写、删、文档 全部封装在 干净的小方法 里,外部零感知,内部随便加逻辑,是 Python “裸字段 → 健壮接口” 的无痛升级钥匙。