Python __init__构造方法中怎么设置默认参数和可变参数

周叙
周叙
发布时间:2026-10-31 | 8人浏览过
原创

详细讲解在Python类的__init__构造方法中如何正确设置默认参数与可变参数(*args、**kwargs),剖析形参顺序规范、可变对象默认值陷阱及继承场景下的参数透传。

假设我们正在开发一个数据库连接池类 DBConnection。大部分日常连接只需指定主机地址即可,端口和超时时间可以直接使用推荐值;但在高并发微服务或动态数据同步场景中,不同数据源可能还会携带额外的认证凭证、字符集、SSL 证书路径等动态配置。如果将这些参数硬编码进构造方法,签名就会冗长死板;若直接使用单一字典,又会丢失关键参数的类型提示和自动补全。

在 Python 的类定义中,__init__ 构造方法本质上是一个标准的实例方法,因此它完全继承了 Python 函数参数系统的灵活性。通过在 __init__ 中合理搭配默认参数(Default Arguments)与可变参数(*args 和 **kwargs),我们既能为常规字段提供开箱即用的预设值,又能动态吸收调用方传入的任意扩展属性。本文将具体讲解两种参数的定义语法、严格的排列顺序规则、常见隐患及工程中的最佳实践。

Python __init__构造方法中怎么设置默认参数和可变参数
图 0:__init__ 构造方法核心参数体系结构概览

一、在 __init__ 中设置默认参数

默认参数允许我们在实例化类对象时省略某些实参,从而降低构造调用的成本。设置默认参数的方法是在形参后直接使用等号 = 赋予初始值。

1. 基本语法与调用方式

在下面的示例中,我们定义了 DatabaseClient 类,其中 host 为必填参数,而 port 和 timeout 提供了合理的默认配置:

class DatabaseClient:
    def __init__(self, host, port=3306, timeout=10):
        self.host = host
        self.port = port
        self.timeout = timeout

# 场景 1:完全使用默认参数
client_default = DatabaseClient("127.0.0.1")
print(f"默认连接:{client_default.host}:{client_default.port}, 超时: {client_default.timeout}s")

# 场景 2:按位置或关键字覆盖部分默认参数
client_custom = DatabaseClient("192.168.1.100", port=3307, timeout=30)
print(f"自定义连接:{client_custom.host}:{client_custom.port}, 超时: {client_custom.timeout}s")
示例运行输出
默认连接:127.0.0.1:3306, 超时: 10s
自定义连接:192.168.1.100:3307, 超时: 30s

2. 关键陷阱:避免将可变对象作为默认参数

初学者极易犯的一个错误是在 __init__ 中使用列表(list)、字典(dict)或集合(set)作为默认参数。在 Python 中,函数的默认参数是在模块加载、函数被定义时求值一次并保存在函数对象的内部属性中,而不是在每次调用 __init__ 时重新生成。

如果使用 def __init__(self, tags=[]),该类的所有实例只要没有显式传参,就会共享同一个列表对象。修改一个实例的属性,将直接污染其他实例。

Python __init__构造方法中怎么设置默认参数和可变参数
图 1:可变对象默认参数导致的实例间共享污染与独立副本隔离对比

解决该问题的工业标准做法是将不可变对象 None 作为哨兵值,并在构造方法主体内部进行动态初始化:

class TaskRunner:
    def __init__(self, name, tasks=None):
        self.name = name
        # 如果未传入 tasks,则在每次实例化时为当前对象创建全新的独立列表
        self.tasks = list(tasks) if tasks is not None else []

runner_a = TaskRunner("工作流-A")
runner_b = TaskRunner("工作流-B")

runner_a.tasks.append("执行数据清洗")

print("runner_a 的任务列表:", runner_a.tasks)
print("runner_b 的任务列表:", runner_b.tasks)
示例运行输出
runner_a 的任务列表: ['执行数据清洗']
runner_b 的任务列表: []

二、在 __init__ 中使用可变参数(*args 与 **kwargs)

当对象无法预知调用方会传入多少个参数,或者需要接收动态元数据时,应使用可变参数语法:

  • *args:可变位置参数(Positional Variable Arguments)。它将调用方传入的多余位置实参收集为一个元组(tuple)。
  • **kwargs:可变关键字参数(Keyword Variable Arguments)。它将调用方传入的多余关键字实参收集为一个字典(dict)。

1. *args 的使用场景与语法

*args 适合接收数量不确定的同质数据,例如构建一个多边形对象,其顶点坐标数量未知:

class Polygon:
    def __init__(self, name, *points):
        self.name = name
        # points 被自动打包为元组,保存所有未被固定形参匹配的位置实参
        self.points = points

    def count_vertices(self):
        return len(self.points)

# 传入 3 个坐标点
triangle = Polygon("三角形", (0, 0), (1, 2), (2, 0))
# 传入 4 个坐标点
quad = Polygon("四边形", (0, 0), (0, 2), (3, 2), (3, 0))

print(f"{triangle.name} 包含顶点数: {triangle.count_vertices()}, 原始数据: {triangle.points}")
print(f"{quad.name} 包含顶点数: {quad.count_vertices()}")
示例运行输出
三角形 包含顶点数: 3, 原始数据: ((0, 0), (1, 2), (2, 0))
四边形 包含顶点数: 4

2. **kwargs 的使用场景与动态属性绑定

**kwargs 适合接收配置项、标签字典或可选扩展元数据。通过它,构造方法可以在不频繁修改签名的情况下兼顾多种扩展字段:

class ServiceNode:
    def __init__(self, node_id, **options):
        self.node_id = node_id
        # options 被自动打包为字典,可以整体保存或提取
        self.options = options
        
        # 也可以按需提取特定配置,并指定局部备选值
        self.region = options.get("region", "cn-north-1")
        self.is_active = options.get("is_active", True)

# 调用时可以传入任意命名实参
node = ServiceNode(
    1001,
    region="cn-east-2",
    weight=80,
    tags=["k8s", "gpu"],
    cluster_group="compute-01"
)

print("节点ID:", node.node_id)
print("区域属性:", node.region)
print("其余配置选项:", node.options)
示例运行输出
节点ID: 1001
区域属性: cn-east-2
其余配置选项: {'region': 'cn-east-2', 'weight': 80, 'tags': ['k8s', 'gpu'], 'cluster_group': 'compute-01'}

三、参数混合定义时的严格顺序规则

在 Python 语法中,形式参数的定义顺序具有强制性约束。如果在 __init__ 中同时混合使用常规固定参数、默认参数、*args、关键字命名参数以及 **kwargs,必须遵循语言解析规范,否则会在定义阶段直接抛出 SyntaxError: non-default argument follows default argument。

Python __init__构造方法中怎么设置默认参数和可变参数
图 2:Python 构造方法形参定义的五层顺序架构

1. 常规默认参数在 *args 之前

当把默认参数写在 *args 之前时,传入的多余位置参数会优先依次填满该默认形参,然后再将剩余的位置参数打包给 *args:

class OrderItem:
    def __init__(self, item_id, status="pending", *tags, **metadata):
        self.item_id = item_id
        self.status = status
        self.tags = tags
        self.metadata = metadata

# 调用分析:
# 101 匹配 item_id
# "shipped" 匹配了默认形参 status,覆盖了 "pending"
# 后续两个位置实参 "fragile", "express" 被打包给 *tags
# priority=1 作为关键字实参被打包进 **metadata
order = OrderItem(101, "shipped", "fragile", "express", priority=1)

print("状态:", order.status)
print("标签元组:", order.tags)
print("额外元数据:", order.metadata)
示例运行输出
状态: shipped
标签元组: ('fragile', 'express')
额外元数据: {'priority': 1}

2. 仅限关键字参数(Keyword-Only Arguments)与默认值

在 Python 3 中,放置在 *args(或单独的 *)之后的参数被称为仅限关键字参数。它们既可以携带默认值,也可以是无默认值的必填项,但调用方只能通过关键字形式传递,无法通过位置参数匹配。这种模式在防止调用方因位置顺序错位而传错参数时非常有用:

class WebServer:
    # ssl_enabled 是带有默认值的 Keyword-Only 参数
    # *args 收集任意位置参数,**kwargs 收集其余键值对
    def __init__(self, host, port=80, *args, ssl_enabled=False, **kwargs):
        self.host = host
        self.port = port
        self.extra_args = args
        self.ssl_enabled = ssl_enabled
        self.extra_kwargs = kwargs

# 尝试通过位置参数传值给 ssl_enabled 会被 *args 截获,而不是赋值给 ssl_enabled
srv = WebServer("0.0.0.0", 443, "extra_pos1", ssl_enabled=True, worker_num=4)

print(f"主机: {srv.host}:{srv.port}")
print("SSL 是否开启:", srv.ssl_enabled)
print("未命名位置参数:", srv.extra_args)
print("扩展关键字参数:", srv.extra_kwargs)
示例运行输出
主机: 0.0.0.0:443
SSL 是否开启: True
未命名位置参数: ('extra_pos1',)
扩展关键字参数: {'worker_num': 4}

四、操作步骤:实战构造多层级对象与参数透传

在实际生产项目中,设置可变参数最常见的一个目的就是配合类的继承机制(Inheritance)实现父类构造方法的透明转发。子类无需将父类的全部形参重新完整声明一遍,只需截获自己所需的参数,然后通过 super().__init__(*args, **kwargs) 将剩余内容原样解包转发给父类。

Python __init__构造方法中怎么设置默认参数和可变参数
图 3:子类构造截获特有字段后通过 **kwargs 解包透传给父类执行流程

我们按照上述设计步骤编写完整代码:

class BaseApp:
    def __init__(self, name="DefaultApp", max_conns=100, **kwargs):
        self.name = name
        self.max_conns = max_conns
        self.extra_config = kwargs

class SecureApp(BaseApp):
    def __init__(self, auth_token, *args, cipher_mode="AES-256", **kwargs):
        # 1. 拦截并处理当前子类特有的属性
        self.auth_token = auth_token
        self.cipher_mode = cipher_mode

        # 2. 将剩余的位置和关键字参数透传给父类
        # 注意:此处使用 *args 与 **kwargs 进行解包转发,而不是传递 tuple/dict 对象本身
        super().__init__(*args, **kwargs)

# 实例化子类:
# 传参包含:子类必须的 token,以及用于父类初始化的 name 和动态扩展选项
app = SecureApp(
    "auth-sec-98124",
    name="PaymentGateway",
    max_conns=5000,
    cipher_mode="ChaCha20",
    env="production",
    audit_log=True
)

print("子类独有令牌:", app.auth_token)
print("子类加密套件:", app.cipher_mode)
print("父类名称属性:", app.name)
print("父类连接上限:", app.max_conns)
print("父类捕获的扩展参数:", app.extra_config)
示例运行输出
子类独有令牌: auth-sec-98124
子类加密套件: ChaCha20
父类名称属性: PaymentGateway
父类连接上限: 5000
父类捕获的扩展参数: {'env': 'production', 'audit_log': True}
透传注意细节
在调用 super().__init__(*args, **kwargs) 时,变量名左侧的星号 * 和双星号 ** 不可省略。如果写成 super().__init__(args, kwargs),Python 会将元组和字典当作两个独立的位置参数传递,从而引发形参数量不匹配错误。

五、构造方法参数设计决策与选型

在设计 Python 类的 __init__ 构造方法时,并非可变参数用得越多越好。过度使用 *args 和 **kwargs 会破坏代码的自解释性,导致 IDE 无法提供字段提示,调用方也难以查明到底可以传入哪些参数。我们需要根据参数的稳定性、频次和业务边界进行分层选择。

Python __init__构造方法中怎么设置默认参数和可变参数
图 4:Python 类构造方法参数设计与选型路径总结

在设计 __init__ 签名时,推荐遵循以下工程原则:

  1. 核心依赖显式化:对象生命周期中不可或缺的字段(如数据源 URL、数据库连接句柄、服务 ID 等),必须声明为没有默认值的常规位置形参,在缺失时立即报错阻断。
  2. 默认值使用不可变对象:针对配置项,优先赋予数字、字符串、元组或布尔值等不可变类型;若确实需要集合或列表容器,必须使用 None 作为默认值,在 __init__ 内部以 self.field = list(val) if val is not None else [] 形式初始化。
  3. 优先考虑 Keyword-Only 强化安全:对于容易引发混淆的布尔值标志或敏感选项,放置在 *args 之后作为仅限关键字参数,强制调用方写出属性名,提高代码阅读性。
  4. 谨慎使用纯 **kwargs:如果类的参数极其多,不应直接写成 def __init__(self, **kwargs),而应结合配置类(如 dataclass 或 pydantic 模型)统一管理,避免构造接口退化成黑盒。
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表