python*args和**kwargs怎么用定义函数
在 Python 函数定义中,*args 用于接收任意数量的位置参数并自动打包为元组,**kwargs 用于接收任意数量的关键字参数并自动打包为字典。本文详解其语法机制、标准声明顺序、打包与解包辨析及工程最佳实践。
*args 用于接收任意数量的未命名位置实参并打包为元组(tuple),**kwargs 用于接收任意数量的具名关键字实参并打包为字典(dict)。星号(* 与 **)是执行参数打包的核心语法操作符,而 args 与 kwargs 是社区通用的形参命名约定。
一、核心结论与快速理解
在编写通用函数、编写装饰器或继承类重写方法时,开发者往往无法预先确定调用者会传入多少个参数。Python 通过单星号 * 和双星号 ** 提供了“可变参数”(Variable-length Arguments)机制:
- 单星号
*args:收集函数定义中常规位置参数之后出现的所有剩余位置实参,在函数体内以一个tuple对象的形式呈现。 - 双星号
**kwargs:收集所有未能与显式形参匹配的key=value键值实参,在函数体内以一个标准的dict对象的形式呈现。
值得强调的是,语法层面的强制要求仅在于星号 * 和 **。你完全可以将其命名为 *values 或 **options,代码仍可正常执行。但在实际工程与团队协作中,必须严格遵循 PEP 8 命名规范使用 *args 和 **kwargs,以确保代码的可读性与一致性。
二、用 *args 定义函数:收集任意数量的位置参数
当函数需要计算不确定数量的数值之和、拼接多段文本或输出日志时,直接在形参列表中声明 *args 是最自然的解决方案。
1. 基本语法与底层容器验证
下面的代码定义了一个计算任意数量数值总和的函数。我们在函数体内部打印 args 的变量值及其类型:
def calculate_sum(base, *args):
# base 接收第一个必须传入的位置实参
# args 收集其余所有位置实参
print(f"基准值 base: {base}")
print(f"收集到的 args 内容: {args}, 容器类型: {type(args)}")
total = base + sum(args)
return total
# 示例调用 1:传入多个实参
res1 = calculate_sum(10, 20, 30, 40)
print(f"计算结果 1: {res1}")
# 示例调用 2:仅传入基准值
res2 = calculate_sum(100)
print(f"计算结果 2: {res2}")
基准值 base: 10 收集到的 args 内容: (20, 30, 40), 容器类型: <class 'tuple'> 计算结果 1: 100 基准值 base: 100 收集到的 args 内容: (), 容器类型: <class 'tuple'> 计算结果 2: 100
观察输出结果可以得出两个关键事实:
- 即使调用者传入了多个整数,解释器也将其原样封装进了一个
tuple。因此,在函数内部你可以安全地对args执行len()、迭代遍历或切片操作。 - 当调用者没有传入额外的多余参数时,
args并不会是None,而是一个空元组()。这保证了后续针对args的for x in args:循环无需额外判断if args is not None,避免了空指针异常。
因为 args 是元组而不是列表,所以无法在函数内部调用 args.append() 或对元素进行重新赋值。若必须在函数内增删元素,需显式执行 arg_list = list(args) 进行转换。
三、用 **kwargs 定义函数:收集任意数量的关键字参数
在构建数据接口、配置项处理器或数据库查询封装时,参数的名称往往包含业务语义。使用 **kwargs 可以捕获所有调用者显式指定的键值对,无需在函数签名中预先写死每一个可能的参数名。
1. 基本语法与字典特性
下面的示例演示了如何通过 **kwargs 编写一个支持任意配置项的连接构建函数:
def build_connection(host, port=8080, **kwargs):
print(f"核心地址: {host}:{port}")
print(f"额外配置 kwargs: {kwargs}, 容器类型: {type(kwargs)}")
# 使用字典标准方法安全读取配置
timeout = kwargs.get("timeout", 30)
retry_enabled = kwargs.get("retry", False)
print(f"生效配置: timeout={timeout}, retry={retry_enabled}")
# 示例调用
build_connection("api.example.com", port=443, timeout=10, retry=True, auth_type="Bearer")
核心地址: api.example.com:443
额外配置 kwargs: {'timeout': 10, 'retry': True, 'auth_type': 'Bearer'}, 容器类型: <class 'dict'>
生效配置: timeout=10, retry=True
与 *args 类似,若调用端未传递任何多余的具名参数,kwargs 将初始化为一个合法的空字典 {}。在函数体内,建议始终使用 kwargs.get(key, default) 读取参数,这比直接下标取值 kwargs[key] 更具容错性,能优雅地提供缺省值并防止 KeyError。
在通过 key=value 语法调用函数时,key 在 Python 语法层必须是合法的变量名形式(不能是数字开头或包含破折号)。收集到 kwargs 后,所有的键都会自动转化为标准的字符串类型。
四、辨析易混淆点:定义时“打包”与调用时“解包”
很多初学者常把“函数定义时的可变参数”与“函数调用时的解包操作”混为一谈。虽然它们都使用 * 和 ** 符号,但两者的运作方向截然相反:
| 对比维度 | 定义时使用(打包 Packing) | 调用时使用(解包 Unpacking) |
|---|---|---|
| 语法出现位置 | 函数头声明的形参列表:def fn(*args, **kwargs) |
发起调用时的实参列表:fn(*numbers, **options) |
| 执行动作 | 将零散传入的多个实参聚拢、打包成单个集合 | 将现有的序列或映射对象拆散、解构为离散实参 |
| 操作对象 | 调用者在控制台输入的各实参片段 | 预先定义好的 list、tuple、set 或 dict |
| 典型收益 | 使被调函数能够兼容不同长度的调用请求 | 免去手动编写索引或硬编码解开列表的重复工作 |
典型工程实践:透明代理与参数透传
当把“定义时打包”与“调用时解包”结合使用时,就构成了 Python 中极为核心的透明转发模式。这种模式广泛应用于装饰器(Decorator)和面向对象继承(OOP):
import time
# 1. 装饰器中的无损转发
def execution_logger(func):
# 包装层定义:打包所有入参
def wrapper(*args, **kwargs):
start = time.perf_counter()
# 目标函数调用:解包所有入参,原样无损传递
result = func(*args, **kwargs)
cost = time.perf_counter() - start
print(f"[LOG] 函数 {func.__name__} 执行耗时: {cost:.6f}s")
return result
return wrapper
@execution_logger
def query_data(endpoint, timeout=5, retries=3):
return f"Fetched from {endpoint} (retries={retries})"
# 示例调用
msg = query_data("https://api.v1/users", timeout=10, retries=5)
print(f"返回内容: {msg}")
[LOG] 函数 query_data 执行耗时: 0.000004s 返回内容: Fetched from https://api.v1/users (retries=5)
由于 wrapper 在定义处声明了 *args, **kwargs,并在内部调用 func(*args, **kwargs),它可以在完全不知道 func 签名细节的情况下,对任意签名的函数实现透明包裹与耗时统计。
五、形参组合定义时的严格顺序规则
在复杂函数中,可能会同时出现普通位置参数、带默认值的参数、*args、仅限关键字参数以及 **kwargs。Python 解释器要求这些形参在函数定义中必须严格遵守如下排列次序:
def complex_function(
pos_only, # 1. 必选位置参数
default_param=10, # 2. 带有默认值的常规参数
*args, # 3. 可变位置参数 (*args)
kw_only_param, # 4. 仅限关键字参数 (Keyword-only)
**kwargs # 5. 可变关键字参数 (**kwargs)
):
pass
1. 为什么 *args 之后的参数会变成 Keyword-only?
当调用者传入一系列位置实参时,所有超出前置固定形参的未命名值都会被 *args 吞没。因此,声明在 *args 之后的任何形参(如上例中的 kw_only_param),在语法上不可能通过位置匹配得到赋值。调用者必须显式以 kw_only_param="value" 的具名形式传递,否则 Python 会直接抛出异常:
TypeError: complex_function() missing 1 required keyword-only argument: 'kw_only_param'
2. 常见反例与排错清单
-
反例 1:将
**kwargs放在*args之前
代码:def bad_order(**kwargs, *args): pass
报错:SyntaxError: invalid syntax。因为**kwargs已经代表了实参收集的最外围终点,其后不允许再跟任何参数声明。 -
反例 2:多次传递相同参数
若函数显式声明了def send(target, **kwargs): pass,当调用方写出send("192.168.1.1", target="10.0.0.1")时,会触发TypeError: send() got multiple values for argument 'target'。因为target已通过第一个位置实参获得绑定,不能再以同名关键字实参重复提供。
六、总结与实践建议
正确使用 *args 和 **kwargs 能显著提升 Python 代码的扩展性与灵活性。在日常编码中,建议掌握以下原则:
- 优先明确签名:普通业务函数应优先使用显式的命名形参。只有在参数个数无法提前预知、编写通用装饰器或转发封装上层库时,才引入
*args和**kwargs。过多无节制的可变参数会削弱 IDE 的类型提示与自动补全体验。 - 区分打包与解包:牢记函数头定义是“打包聚合成容器”,函数体调用是“解包拆分传参”。
- 严格恪守声明顺序:按“位置参数 → 默认参数 → *args → 仅限关键字参数 → **kwargs”由左至右书写,避免语法错误。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









