前言
先把一个前提讲清楚:这个标题把"重载"和"覆盖"并列,容易让人以为 Python 同时支持两者。实际上 Python 只支持方法覆盖(overriding),不支持传统意义上的方法重载(overloading)。
- 方法覆盖:子类定义与父类同名的方法,运行时调用的是子类的版本。这个 Python 完全支持。
- 方法重载:在同一个作用域里,用同一个名字、不同的参数列表定义多个函数/方法,由编译器或运行环境根据实参个数或类型自动选择。Java、C++ 支持它,Python 不支持——在 Python 里,同一个名字第二次定义会把第一次的直接覆盖掉,前一个定义连影子都不剩。
需要区分的是:Python 虽然有typing.overload这个名字相似的装饰器,但它只服务于静态类型检查器,运行时不做任何分派——文档明确写着,被@overload装饰的定义会被后面那个不带@overload的定义覆盖,运行时直接调@overload装饰的函数会抛NotImplementedError。
所以本文讲三件事:覆盖怎么写;为什么"重载"在 Python 里行不通;以及当真的需要"按参数个数或类型分派"时,用什么方式替代。
一、方法覆盖:Python 支持的写法
覆盖就是子类重写父类方法。它依赖属性查找:先实例、再沿 MRO 找类,子类在 MRO 里排在父类前面,所以子类版本被选中。
# 适用于 Python 3.8+
class Base:
def greet(self):
return "base"
class Child(Base):
def greet(self): # 与父类同名 → 覆盖
return "child"
print(Child().greet()) # child要在覆盖的同时保留父类的行为,用super()调一下:
# 适用于 Python 3.8+
class Base:
def greet(self):
return "base"
class Child(Base):
def greet(self):
return super().greet() + "+child"
print(Child().greet()) # base+child注意覆盖时保持调用签名兼容。子类如果偷偷改掉了参数个数,多态调用点就会在某处突然抛TypeError。
二、为什么"重载"在 Python 里行不通
看下面这段"看起来像重载"的代码:
# 适用于 Python 3.8+
class Calculator:
def add(self, a, b):
return a + b
def add(self, a, b, c): # 这个名字把上面那个覆盖掉了
return a + b + c
calc = Calculator()
print(calc.add(1, 2, 3)) # 6
# calc.add(1, 2) # TypeError: add() missing 1 required positional argument类体也是一个作用域,两个def add同名,后一个直接覆盖前一个。所以calc.add(1, 2)会失败——不是"没匹配到两个参数的重载版本",而是那个版本根本不存在。
这背后的原因是:Python 是动态类型语言,函数对象只认名字,不像静态编译语言那样在编译期根据签名生成多份符号。变量名在模块/类命名空间里就是唯一的键。
因此,Python 里想说"支持多种调用形式",只能靠一个函数体内部判断,或者靠运行时的类型分派。
三、替代方案一:默认参数与可变参数
如果"不同重载版本"的差异只在于参数个数,用默认参数(default argument)或可变参数(*args/**kwargs)就够了。
# 适用于 Python 3.8+
class Calculator:
def add(self, a, b, c=0):
return a + b + c
calc = Calculator()
print(calc.add(1, 2)) # 3
print(calc.add(1, 2, 3)) # 6如果差异在于类型,朴素做法是在函数体里分支,但那正是多态想消灭的写法:
# 适用于 Python 3.8+
def render(value):
if isinstance(value, str):
return "str:" + value
if isinstance(value, int):
return f"int:{value}"
return str(value)这种写法勉强可用,但每当新增类型就要回来改这个函数,违反了开闭原则。
四、替代方案二:单分派(singledispatch)
标准库functools提供了单分派泛型函数(single-dispatch generic function):把函数变成"按第一个参数的类型选择实现"的入口。
# 适用于 Python 3.8+
from functools import singledispatch
@singledispatch
def describe(value):
return f"unknown:{value}"
@describe.register
def _(value: int):
return f"int:{value}"
@describe.register
def _(value: str):
return f"str:{value}"
print(describe(10)) # int:10
print(describe("hi")) # str:hi
print(describe(1.5)) # unknown:1.5要点:singledispatch从 Python 3.4 起提供,只按第一个参数的类型分派;用@describe.register配上类型注解来注册实现;被@singledispatch装饰的那个原函数注册在object上,作为兜底。
用于类方法时,用functools.singledispatchmethod(需要Python 3.8 及以上)。此时分派依据是第一个非self/ 非cls的参数:
# 适用于 Python 3.8+
from functools import singledispatchmethod
class Formatter:
@singledispatchmethod
def format(self, value):
return str(value)
@format.register
def _(self, value: int):
return f"int:{value}"
@format.register
def _(self, value: str):
return f"str:{value}"
f = Formatter()
print(f.format(3)) # int:3
print(f.format("a")) # str:a
print(f.format(1.5)) # 1.5文档特别提醒:singledispatchmethod 要和别的装饰器(如@classmethod)叠加时,必须放在最外层,否则register拿不到正确的绑定。
分派是沿类型的 MRO 找最近的注册实现,也会考虑注册在抽象基类上的实现——所以bool是int的子类,format(True)会走到int的实现,输出int:True。
下表对比三个容易混淆的概念:
| 概念 | 运行时是否可依据参数个数/类型选择 | Python 支持情况 |
|---|
| 方法覆盖 overriding | 否,只看 MRO 选同名方法 | 支持 |
| 传统方法重载 overloading | 是 | 不支持,同名后者覆盖前者 |
typing.overload | 否,仅供类型检查器 | 提供,但运行时不分派 |
singledispatch | 按第一个参数类型 | 3.4+ 提供 |
singledispatchmethod | 按第一个非 self 参数类型 | 3.8+ 提供 |
常见坑点
1. 以为同名不同参数的方法算重载
❌
# 适用于 Python 3.8+
class C:
def f(self, a): ...
def f(self, a, b): ...✅ 只有第二个存在;要支持两种调用,写成def f(self, a, b=None):。
2. 用@typing.overload指望运行时按类型选
❌ 以为加了@overload就能运行时分派——它只是给类型检查器的声明,运行时会抛NotImplementedError。
✅ 运行时按类型分派用functools.singledispatch/singledispatchmethod。
3.singledispatchmethod没放在最外层
❌
# 适用于 Python 3.8+
class C:
@classmethod
@singledispatchmethod
def f(cls, x): ...✅ 让@singledispatchmethod处于最外层,register才能正确工作。
4. 指望singledispatch按多个参数分派
❌ 以为它能像多重分派(multiple dispatch)那样同时看两个参数。
✅ 它只看第一个(方法里是第一个非self)参数的类型。
5. 覆盖父类方法时改了签名
❌ 父类def save(self, path),子类写成def save(self),多态调用点传入参数就TypeError。
✅ 覆盖方法保持兼容签名;新增能力用带默认值的参数。
6. 覆盖后忘记保留父类行为
❌ 子类重写__init__却不调super().__init__(),父类设置的属性全丢。
✅ 需要父类逻辑时用super().方法名(...)显式调用。
7. 在函数体里堆isinstance分支冒充重载
❌ 每加一种类型都要回来改函数,还容易漏掉子类。
✅ 简单场景用singledispatch,面向对象的场景用方法覆盖。
8. 注册实现时忘了类型注解
❌@describe.register下面的函数没写value: int,注册不进int,永远走兜底实现。
✅ 注册函数必须带类型注解,注解即分派依据。
总结
| 需求 | 推荐做法 |
|---|
| 子类改变父类行为 | 方法覆盖,必要时super()保留父类逻辑 |
| 参数个数不同 | 默认参数、*args/**kwargs |
| 按参数类型分派(函数) | functools.singledispatch(3.4+) |
| 按参数类型分派(方法) | functools.singledispatchmethod(3.8+) |
| 只想让类型检查器知道多种签名 | typing.overload(运行时无效) |
记住一句话就能避开绝大多数困惑:Python 有覆盖,没有传统重载。同名方法会互相覆盖;需要"多种形态"时,要么把差异交给继承与多态,要么用singledispatch系列做运行时类型分派,typing.overload则是纯给静态检查器看的另一回事。