# *args 与 **kwargs

Source: https://codewiki.com/zh/python/args-kwargs/

> - **what**: 在函数定义中，`*args` 把未匹配的位置实参收集成元组，`**kwargs` 把未匹配的关键字实参收集成字典。
> - **trap**: 全盘接收参数的签名会隐藏允许的选项；盲目转发还可能造成关键字重复、保留拼写错误，或让数据越过错误的 API 边界。
> - **fix**: 明确写出具名形参，只在参数确实可变的边界使用 `*` 与 `**`，并在转发前验证或移除包装器拥有的选项。

## 是什么，为什么存在

`*args` 与 `**kwargs` 是两种可变形参（variadic parameter）的惯用写法。在函数定义中，一个前导 `*` 收集多余的位置实参，两个前导 `**` 收集多余的关键字实参。名称本身没有特殊之处：`*items` 与 `**options` 的行为相同，而且往往更能说明内容。

形参（parameter）是函数声明的输入槽，实参（argument）是调用方提供的值或表达式。区分这两个术语后，规则就更容易说清：Python 先把实参绑定到形参，再把剩余实参打包进可变形参。

这套语法解决了两类实际接口问题。有些操作天然需要接收数量可变的同类值，例如接收多个字段的格式化函数。包装器和装饰器也可能需要转发调用，而调用的确切形状由另一个可调用对象决定。

可变形参不能代替接口设计。如果函数支持 `timeout`、`retries` 与 `headers`，把它们写进签名能让拼写错误立即失败，也能为编辑器、文档工具和类型检查器提供有效信息。只有允许的键集合确实开放，或者归下游可调用对象所有时，才应使用 `**kwargs`。

同样的星号用于调用位置时含义相反。`*iterable` 把其中的元素作为位置实参提供，`**mapping` 把以字符串为键的条目作为关键字实参提供。这叫作实参解包（argument unpacking），不同于定义中的可变形参打包。

装饰器、适配器、通过 `super()` 协作的类、测试参数化辅助函数，以及同时接收固定控制项与可扩展载荷的 API 中，都会遇到这两个方向。设计时最重要的问题是：灵活边界从哪里开始，每个选项由哪一层拥有。

## 工作原理

Python 会给函数签名（function signature）中的每个形参分类。`/`、`*` 分隔符与可变形参共同决定调用方能以什么方式提供各个值。

| 形参种类 | 定义形式 | 允许的调用形式 |
| --- | --- | --- |
| 仅限位置 | 位于 `/` 之前 | 按位置传入 |
| 位置或关键字 | 位于 `/` 之后、`*` 之前 | 按位置或关键字传入 |
| 可变位置 | `*items` | 零个或多个位置实参 |
| 仅限关键字 | 位于 `*` 或 `*items` 之后 | 按关键字传入 |
| 可变关键字 | `**options` | 零个或多个未匹配的关键字实参 |

看下面这个签名：

`def render(template, /, context=None, *fragments, escape=True, **attributes): ...`

`template` 仅限位置传入。`context` 可以按位置或关键字传入，`fragments` 接收额外的位置实参，`escape` 仅限关键字传入，`attributes` 接收其他关键字。函数不需要收集额外位置实参时，可以用裸 `*` 标记仅限关键字的形参。

### 位置绑定

Python 从左到右，把每个位置实参（positional argument）分配给可接受位置传入的形参。这些槽位填满后，可变位置形参把其余值接收为一个新元组。没有多余值时，该形参就是空元组。

`/` 之前的形参仅限位置传入。它们在源码中的名称属于实现细节，不是调用方可用的关键字名。公共形参名称可能改变，或者同一拼写需要保留给 `**kwargs` 时，这项规则很有用。

调用中，位置实参不能出现在 `**mapping` 之后。调用语法对带星号的可迭代对象更宽松，但其中的元素仍会进入位置实参流。应根据最终的绑定关系理解调用，不要只看 `*iterable` 表达式在源码中的位置。

### 关键字绑定

每个显式的关键字实参（keyword argument）以及 `**mapping` 中的条目都按名称绑定。关键字不能填入仅限位置的形参。一个形参也不能收到多个值，无论冲突来自位置实参与关键字、两个解包映射，还是显式关键字与映射条目。

具名形参匹配完毕后，`**options` 会把未匹配的关键字接收进一个新字典。如果没有可变关键字形参，任何未匹配的名称都会引发 `TypeError`。没有未匹配的关键字时，该形参就是空字典。

用 `**` 解包的映射必须使用字符串键。如果被调用函数有 `**kwargs` 形参，字符串键不必是有效的 Python 标识符，因此即使无法写出 `capture(content-type="json")`，`capture(**{"content-type": "json"})` 也可以工作。这类键只能通过字典操作访问。

### 求值与绑定是两步

Python 会先对实参表达式求值，再进入函数体。求值遵循源码顺序，因此后面的重复关键字或意外关键字即使导致绑定失败，实参表达式中的副作用也可能已经发生。不要指望被调用函数阻止构造调用时已经触发的副作用。

随后，绑定过程把完整的位置输入与关键字输入同签名核对。缺少必需形参、没有收集器却多出位置实参、没有收集器却出现意外关键字，以及重复提供值，都会引发 `TypeError`。这些情况下，函数体不会开始执行。

调用方没有提供值时，默认值会填入符合条件的形参。默认值不会吸收拼错的关键字。全盘接收的 `**kwargs` 会改变这种行为，把拼错的名称当作新字典条目接受，因此灵活签名需要显式验证。

### 容器与对象标识

函数内部的可变位置参数值是元组，可变关键字参数值是字典。元组不能调整长度，字典则可在局部修改。这些容器性质无法说明其中对象是否可变。

如果调用方提供一个列表，`args[0]` 与调用方仍引用同一个列表。从任何一处修改该列表，另一处都能观察到。类似地，即使给 `kwargs` 顶层分配新键不会改变调用方解包的映射，修改 `kwargs["headers"]["Accept"]` 仍可能改变调用方拥有的嵌套字典。

因此，打包容器只在最外层提供结构隔离。如果函数需要拥有嵌套的可变值，就应复制它；如果本来就要修改，则应记录并测试这项约定。星号不会创建深拷贝。

### 定义顺序

完整顺序依次是：仅限位置形参、`/`、位置或关键字形参、可变位置形参或裸 `*`、仅限关键字形参，最后是可变关键字形参。并非每种形参都必须出现。

在同一个位置形参组中，带默认值的形参后面不能出现必需形参。仅限关键字形参没有这项限制：调用方会写出名称，因此必需形参与带默认值的形参可以按任意顺序出现。

`*args` 并不表示它后面的每个形参也会被收集。位于其后的具名形参仅限关键字传入，而且会在未匹配的名称进入 `**kwargs` 前完成绑定。因此，`def send(*messages, retry=False, **metadata)` 既保持灵活，也明确声明了自身拥有的控制项。

### 协作式方法调用

多重继承有时会把 `**kwargs` 用作协作通道。每个初始化方法明确写出并消费自己拥有的形参，再调用 `super().__init__(**kwargs)`，让方法解析顺序中的下一个实现消费自己的部分。调用链末端的类应拒绝剩余选项，而不是直接丢弃。

这种模式要求每个参与类都遵守同一契约。只要有一个初始化方法遗漏 `super()`、重复转发某个选项，或者消费了另一个类拥有的名称，调用链就会断裂。它是受控类层次结构中的协议，不是到处接受任意配置的理由。

协作式控制项最好使用仅限关键字的形参，因为即使基类顺序改变，其含义仍然不变。通过多个不相关的初始化方法转发位置参数，会让所有类耦合到同一套槽位顺序，重构风险很高。

### 选择灵活边界

应把收集器放在你能理解其可变性的层次。聚合函数可以拥有全部 `*values`；装饰器可以透明转发两类参数流；HTTP 适配器可以拥有三个具名控制项，并拒绝其他选项。即使实现中都有星号，这些契约也不相同。

如果收集内容同质或范围明确，应按角色命名，例如 `*paths`、`**headers` 或 `**changes`。包装器确实不关心被包装函数的领域时，可以保留 `args` 与 `kwargs`。名称无法强制契约，却能告诉审查者应该寻找哪种契约。

选项集合稳定后，应把它们提升为具名形参。这样通常能改善文档与兼容性，因为新增仅限关键字的形参不会干扰已有的位置调用。完成提升后，只有未知键仍然有意义时，才保留 `**kwargs`。

不要只因下游函数很灵活，就暴露同等灵活性。外层 API 可能需要更严格的安全、兼容性或所有权策略。转发是一项接口决策，不是机械捷径。

测试应从两个方向覆盖边界。既要验证被调用方收到的值，也要验证调用方提供不受支持的形状时看到的错误，因为任何一侧发生漂移，正常路径仍可能继续通过。

即使当前所有调用方都在同一个代码库中，也应把最终签名当作公开文档。

## 示例

### 收集值并命名控制项

这个函数接收一个固定的位置订单 ID、任意数量的商品名称、一个具名控制项以及开放的元数据。领域名称比惯用的 `args` 与 `kwargs` 更能说明内容。

<!-- quick -->

```python
# file: collect_order.py
def summarize_order(order_id, /, *items, currency="USD", **metadata):
    print(f"order: {order_id}")
    print(f"items: {items}")
    print(f"currency: {currency}")
    print(f"metadata: {metadata}")


summarize_order(
    "A-17",
    "notebook",
    "pen",
    currency="EUR",
    priority=True,
    warehouse="west",
)
```

```text
order: A-17
items: ('notebook', 'pen')
currency: EUR
metadata: {'priority': True, 'warehouse': 'west'}
```

<!-- /quick -->

`order_id` 位于 `/` 之前，因此不能按关键字传入。两个商品名称组成一个元组。`currency` 绑定到已经声明的仅限关键字形参，所以只有 `priority` 与 `warehouse` 留给 `metadata`。

输出中字典的插入顺序遵循调用中的关键字顺序。只有 API 明确定义了顺序时，才能让逻辑依赖它；大多数选项处理都应按名称选择键。

### 解包调用方的数据

调用位置的解包能让容器中已有的数据满足明确的签名。可以使用多个 `**` 表达式，前提是同一个键没有被提供两次。

```python
# file: unpack_schedule.py
def schedule(job, owner, /, *, retries=2, urgent=False):
    return (
        f"job={job}, owner={owner}, "
        f"retries={retries}, urgent={urgent}"
    )


identity = ("backup", "Mina")
retry_policy = {"retries": 4}
priority = {"urgent": True}

print(schedule(*identity, **retry_policy, **priority))
```

```text
job=backup, owner=Mina, retries=4, urgent=True
```

元组元素填入 `job` 与 `owner`，两个映射填入仅限关键字的形参。`schedule()` 仍然严格：映射中出现未知键或重复的 `retries` 键时，会在函数体运行前引发 `TypeError`。

这种严格性在配置边界很有用。外部数据应先经过验证与规范化，再执行解包；随后可以让明确的签名捕获配置模式与函数契约之间的漂移。

### 转发调用而不丢失元数据

装饰器的运行时实现通常无法写出每个被包装签名。它可以收集并转发两类参数流，但应为内省工具保留被包装函数的元数据。

```python
# file: trace_price.py
from functools import wraps
from inspect import signature


def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"returned {result}")
        return result

    return wrapper


@trace
def quote_price(sku, quantity, /, *, discount=0):
    return quantity * (1200 - discount)


print(signature(quote_price))
print(quote_price("BK-7", 2, discount=100))
```

```text
(sku, quantity, /, *, discount=0)
calling quote_price: args=('BK-7', 2), kwargs={'discount': 100}
returned 2200
2200
```

`functools.wraps()` 会设置 `__wrapped__` 并复制用于标识函数的元数据。`inspect.signature()` 默认跟随 `__wrapped__`，所以工具看到的是公开的 `quote_price` 契约，而不是实现细节 `(*args, **kwargs)`。

转发之后，被包装的可调用对象仍会收到最终调用，因此 Python 的绑定检查得以保留。转发不会验证装饰器的日志策略：这个小型示例会打印值，生产日志则必须删去凭据与个人数据。

### 转发前消费自己拥有的选项

适配器应把自己拥有的选项与代其他层接收的选项分开。这个版本刻意不设置下游全盘接收器，因此拼写错误会在适配器边界得到明确的错误。

```python
# file: option_boundary.py
def transport(url, /, *, timeout, headers):
    return f"GET {url} timeout={timeout} headers={headers}"


def fetch(url, /, **options):
    timeout = options.pop("timeout", 5)
    headers = options.pop("headers", {})
    if options:
        unknown = ", ".join(sorted(options))
        raise TypeError(f"unknown fetch options: {unknown}")
    return transport(url, timeout=timeout, headers=headers)


print(fetch("https://example.test", timeout=2, headers={"Accept": "text/plain"}))

try:
    fetch("https://example.test", timeuot=2)
except TypeError as error:
    print(error)
```

```text
GET https://example.test timeout=2 headers={'Accept': 'text/plain'}
unknown fetch options: timeuot
```

`options` 已经是函数局部的外层字典，因此删除顶层键不会从用于 `fetch(**config)` 的映射中删除它们。嵌套的 `headers` 字典仍然共享；这里的 `transport()` 只读取它。

如果适配器确实需要继续转发更多选项，应定义允许列表或记录下游签名，用 `pop()` 删除适配器拥有的键，再把其余选项转发一次。还要明确决定：适配器选项会覆盖调用方的值、拒绝该值，还是让调用方的值优先。

## 陷阱

### 全盘接收会隐藏拼写错误

> **陷阱:** 读取 `kwargs.get("timeout", 5)` 却忽略剩余键，会把 `timeuot=2` 静默解释为使用默认超时。

**修复方法：** 优先使用 `*, timeout=5` 这样的具名仅限关键字形参。确实需要全盘接收时，应消费已识别的键，并拒绝剩余内容。测试一个拼错的选项，因为正常路径测试无法暴露这项错误。

这也是 API 演进问题。接受所有名称的函数无法区分未来选项与当前拼写错误。严格边界能让变更保持明确，也能使弃用路径可观察。

### 转发会产生重复值

> **陷阱:** 包装器调用 `target(timeout=wrapper_timeout, **kwargs)` 时，如果调用方映射中已有 `timeout`，调用就会失败；Python 不会对调用实参采用“后值覆盖前值”。

**修复方法：** 构造调用前先选择策略。可以拒绝调用方提供的值，可以在局部选项字典上用 `setdefault()` 提供后备值，也可以删除该键并明确覆盖它。不要指望显式关键字与解包关键字的顺序解决冲突。

多个 `**` 映射之间也遵循同一规则。`{**defaults, **overrides}` 这样的字典显示会用后面的值覆盖重复键，但函数调用会拒绝重复项。不要把一个上下文的合并语义搬到另一个上下文。

### 打包容器只提供浅层隔离

> **陷阱:** `args` 是新元组，`kwargs` 是新字典，但存放在其中的列表或字典仍可能属于调用方。

**修复方法：** 应在可能发生修改的层次跟踪对象标识。添加字段前复制嵌套的标头字典，尽量使用不可变输入，或者记录函数会修改调用方状态。只测试顶层映射会漏掉这条别名关系。

执行 `kwargs["processed"] = True` 不会修改被解包进调用的映射。执行 `kwargs["headers"]["X-Trace"] = value` 却可能修改嵌套的 `headers` 映射。这两项事实并不冲突，因为外层与内层对象的标识不同。

### 星号解包可能接收错误的可迭代形状

> **陷阱:** `recipient` 是字符串时，`send(*recipient)` 会按字符提供位置实参；解包生成器则会在函数体开始前消费它。

**修复方法：** 标量不应带 `*` 传入，外部数据应在解包前验证容器形状。需要重复使用一次性可迭代对象时，应在所有权边界一次性具体化，并明确相应的内存成本。

只有把可迭代对象消费完并构造位置实参后，长度不匹配才会以 `TypeError` 暴露。如果一个集合在概念上是一个形参，而不是多个槽位，就不要在调用位置解包它。

### 透明包装器不会自动获得精确类型

> **陷阱:** 运行时包装器写成 `def wrapper(*args, **kwargs)` 后，可能丢失有用的静态信息，即使 `functools.wraps()` 已经修复运行时元数据。

**修复方法：** 使用 `ParamSpec` 与被包装函数的返回类型标注保持签名的装饰器，并用 `@wraps` 服务运行时内省。如果包装器增加或删除形参，应公开变化后的契约，而不是宣称完全透明。

`@wraps` 不会让不兼容的包装器变得可调用。包装器插入位置值或删除关键字后，仍可能违反原始签名。应通过装饰后的函数覆盖仅限位置、仅限关键字、带默认值以及错误调用。

### 开放式关键字转发会跨越边界

> **陷阱:** 把整个选项字典依次传过多层，可能让凭据进入日志，或者把 `verify=False` 之类的控制项交给底层 API，而外层接口从未打算公开它。

**修复方法：** 按拥有者划分选项，并为每个下游调用构造经过审查的映射。记录日志前删除敏感内容，在信任边界拒绝未知键，并测试敏感或不受支持的名称无法继续传播。

`database_timeout` 这样的前缀可以减少意外冲突，但多个组件各自拥有设置后，嵌套配置对象会更清楚。一个全局 `**kwargs` 命名空间无法扩展成可靠的配置模型。

<!-- deep -->

## 绑定边界情况

调用语法允许使用多个 `*iterable` 与 `**mapping` 表达式。每个可迭代对象贡献位置值，每个映射贡献关键字键值对。这样可以组合调用，但不会放宽目标签名或重复值规则。

实参表达式从左到右求值，而带星号的位置值仍然参与位置绑定。因此，把显式关键字与后置的星号可迭代对象混用时，有些调用虽然合法，却很难阅读。即使语法允许其他顺序，也应把位置数据组织在关键字数据之前。

每个 `**` 操作数都必须是映射，而不能只是键值对的可迭代对象。它的键必须是字符串。任何关键字来源之间只要出现重复字符串键，就会引发 `TypeError`，即使目标函数会把未匹配名称收进 `**kwargs`。

不是标识符的字符串键与非字符串键并不相同。`capture(**{"content-type": "json"})` 可以把 `"content-type"` 放进可变关键字字典，因为该键是字符串。`capture(**{1: "json"})` 会引发 `TypeError`，而且直接关键字语法也无法写出带连字符的键。

仅限位置形参会刻意分开两个命名空间。给定 `def replace(name, /, **changes)`，调用 `replace("record", name="display")` 会把第一个值绑定到仅限位置形参，把关键字 `name` 留给 `changes`。如果没有 `/`，该形参会收到两个值，绑定随即失败。

这种模式适合底层通用 API，但普通调用方可能感到意外。只有关键字命名空间确实需要这个名称时才使用它，不要把它当作选项模型划分不当的日常补丁。

### 错误发生在函数体之前

Python 会先完成实参求值与绑定，再执行函数体中的第一条语句。因此，被调用函数内部的 `try` 无法捕获自身的实参缺失、值重复或意外关键字错误。确实需要恢复时，应由调用方或外层包装器捕获该 `TypeError`。

不要用一个宽泛的 `TypeError` 捕获同时包住绑定与函数执行。被调用函数可能因为自身缺陷在函数体中引发 `TypeError`，将它当成签名错误会掩盖问题。适配器可以用 `inspect.Signature.bind()` 验证即将发生的调用，而不执行函数体。

`bind()` 应用签名的绑定规则并返回 `BoundArguments`；必需形参缺失或值冲突时，它会引发 `TypeError`。`bind_partial()` 刻意允许缺少必需实参，适合偏应用，不适合验证完整调用。`apply_defaults()` 可以把省略的默认值填入已有的绑定结果。

## 内省与类型

`inspect.signature(callable)` 会公开形参名称、种类、默认值与注解。形参种类分别对应 `POSITIONAL_ONLY`、`POSITIONAL_OR_KEYWORD`、`VAR_POSITIONAL`、`KEYWORD_ONLY` 与 `VAR_KEYWORD`。这种词汇比把 `**kwargs` 之前的一切都叫作“普通参数”更准确。

`inspect.signature()` 默认会跟随 `functools.wraps()` 创建的 `__wrapped__` 链。因此，装饰器示例报告的是原始签名。改变公开调用契约的自定义装饰器可能需要明确设置 `__signature__`，但这份元数据必须与包装器实际接受的调用一致。

`*values: int` 上的注解描述每个被收集的位置值都是 `int`，不是把运行时元组整体标成 `int`。类似地，`**labels: str` 描述每个关键字值都是 `str`；有效调用边界上的键必然是字符串。普通 Python 执行不会强制这些注解。

如果有限个关键字名称对应不同的值类型，可以把 `**kwargs` 标为 `Unpack[SomeTypedDict]`。类型检查器便能分析必需键、可选键、值类型与意外名称。运行时收到的仍然是字典，不可信数据仍需验证。

对于透明的高阶函数，`ParamSpec` 可以捕获可调用对象签名中的位置部分与关键字部分。用 `*args: P.args` 和 `**kwargs: P.kwargs` 标注包装器，再返回 `Callable[P, R]`，可以保留被包装对象与其调用方之间的关系。它不能代替服务运行时元数据的 `@wraps`。

类型系统无法挽救刻意含糊的接口。如果适配器接受任意键，却只转发其中一部分，应尽量直接表达允许的模式。静态精度与运行时拒绝策略应描述同一个所有权边界。

<!-- /deep -->

[检查点: python/args-kwargs](https://codewiki.com/zh/python/args-kwargs/#checkpoint)

## 延伸阅读

- [Python 教程：任意实参列表](https://docs.python.org/3.14/tutorial/controlflow.html#arbitrary-argument-lists)
- [Python 语言参考：函数定义](https://docs.python.org/3.14/reference/compound_stmts.html#function-definitions)
- [Python 语言参考：调用](https://docs.python.org/3.14/reference/expressions.html#calls)
- [Python `inspect.Signature.bind()`](https://docs.python.org/3.14/library/inspect.html#inspect.Signature.bind)
- [Python 类型规范：可调用对象形参与 `Unpack`](https://typing.python.org/en/latest/spec/callables.html#unpack-for-keyword-arguments)
