HOWTO · Python

在 Python 中重载构造函数

本教程演示如何在 Python 中重载构造函数

本页内容

Python 不会通过匹配参数来选择多个 __init__ 方法。如果一个类多次定义 __init__,后面的定义会替换前面的定义,因此创建实例时只有后一个定义可用。若要支持多种对象初始化方式,可以在一个初始化方法中处理不同情况、提供命名工厂方法,或为初始化参数设置合适的默认值。

Python 中的构造函数重载意味着什么

在支持按签名重载构造函数的语言中,运行时可以根据参数类型或数量选择构造函数。对于重复定义的 __init__,Python 不会这样做。单个 __init__ 可以接收灵活的参数并主动选择处理方式,但这属于在一个方法内部进行分派,并非原生的重载机制。应明确列出支持的调用形式,让调用者知道这个类接受哪些值。

使用 *args 按参数类型或数量进行分派

*args 参数会将位置参数收集到一个元组中。你可以检查元组长度或参数值的类型,再据此初始化实例。读取参数前先确认它确实存在,并决定类应如何处理不受支持的输入形式。下面的可运行示例支持传入一个整数或一个字符串,并显示选中的值。

运行此示例:

示例中的代码和显示文本均保持英文。

本节中的代码以及显示的标签或输出仍为英文。

class Greeting:
    def __init__(self, *args):
        if len(args) != 1:
            self.message = "Pass one integer or string"
        else:
            value = args[0]
            if isinstance(value, int):
                self.message = str(value)
            elif isinstance(value, str):
                self.message = "Hello from " + value
            else:
                self.message = "Pass an integer or string"


print(Greeting("Delft").message)

在访问 args[0] 前先检查参数数量,可以安全处理没有位置参数的调用。数量检查也会拒绝多余参数。在 Python 中,bool 是 int 的子类,因此 isinstance(True, int) 的结果为 true;如果不希望接受布尔值作为整数,可以改用精确类型检查。

当每种位置参数数量都有明确含义时,按数量选择处理方式很有用。这个初始化方法恰好支持零个、一个或两个参数:不传参数时使用默认范围;传入一个参数时将其作为结束值;传入两个参数时分别作为起始值和结束值。其他数量都会得到明确提示。每个分支都会在读取 args 前检查参数数量,因此零参数和不受支持的数量都不会导致访问不存在的值。

运行此示例:

示例中的代码和显示文本均保持英文。

class RangeSpec:
    def __init__(self, *args):
        if len(args) == 0:
            self.start, self.stop = 0, 10
        elif len(args) == 1:
            self.start, self.stop = 0, args[0]
        elif len(args) == 2:
            self.start, self.stop = args
        else:
            self.start, self.stop = None, None
            self.message = "Pass zero, one, or two values"

    def describe(self):
        if self.start is None:
            return self.message
        return f"Range from {self.start} to {self.stop}"


for spec in (RangeSpec(), RangeSpec(5), RangeSpec(2, 5), RangeSpec(1, 2, 3)):
    print(spec.describe())

这四次调用涵盖了所有受支持的参数数量,以及一个超出支持范围的数量。如果要支持更多调用形式,最好使用具名参数或工厂方法,这样调用者就不必记住每种位置参数数量分别代表什么。

使用 @classmethod 工厂方法作为替代构造方式

@classmethod 会将类作为第一个参数传入,按惯例将其命名为 cls。工厂方法可以先将其他表示形式转换为常规初始化方法所需的值,再返回 cls(...)。这样会创建一个实例;由于 cls 指向调用该方法时所用的类,这种写法也适用于子类。工厂方法提供有名称的创建途径,但不会为 __init__ 增加重载。

运行此示例:

示例中的代码和显示文本均保持英文。

本节中的代码以及显示的标签或输出仍为英文。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @classmethod
    def from_pair(cls, pair):
        return cls(pair[0], pair[1])


point = Point.from_pair((3, 4))
print(point.x, point.y)

当输入格式有明确名称时,可以分别提供工厂方法,例如 from_file、from_row 或 from_coordinates。如果转换过程需要负责验证或规范化数据,就在工厂方法中完成这些工作。让主初始化方法成为设置对象必要状态的唯一入口。

使用默认参数实现可选初始化方式

当参数确实可选且含义清晰时,默认值很合适。调用者可以省略这些值,而不必增加分派逻辑。优先使用数字、字符串或 None 等不可变默认值;对于列表等可变值,通常应在方法内部创建,以免不同实例意外共享同一个值。

运行此示例:

示例中的代码和显示文本均保持英文。

本节中的代码以及显示的标签或输出仍为英文。

class Profile:
    def __init__(self, name="Guest", score=0):
        self.name = name
        self.score = score


for profile in (Profile(), Profile("Mina"), Profile("Mina", 7)):
    print(profile.name, profile.score)

调用者可以不提供任何可选值、只提供名称,或同时提供两个值。当省略参数有多种不同含义、参数之间彼此依赖,或许多位置参数组合难以记忆时,默认值就不太合适。在这些情况下,仅限关键字参数或命名工厂方法可以让调用更加清晰。

选择合适的构造方式

少量常见可选值适合使用默认参数。如果创建过程需要从另一种表示形式开始,或创建途径需要一个描述性名称,可以使用工厂方法。只有当一组精简且稳定的位置参数形式属于预期 API 时,才使用 *args 分派;使用这些值之前,应先验证参数数量和类型。对于复杂的验证逻辑,明确的分支和有帮助的错误信息比巧妙的类型技巧更容易维护。

这些方法也可以组合使用:工厂方法可以先验证数据,再调用同一个初始化方法;默认值则可以处理可选字段。选择最简单的接口,让有效调用一目了然,并让不受支持的调用有可预期的处理方式。核心规则是:Python 中每个类只有一个有效的 __init__ 定义;灵活性来自围绕它编写的逻辑和提供的命名替代方式。