☰
Python方法的重载和方法的覆盖
2026/10/11 11:00:26 网站建设 项目流程

前言


先把一个前提讲清楚:这个标题把"重载"和"覆盖"并列,容易让人以为 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则是纯给静态检查器看的另一回事。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询