Python函数调用中args和*kwargs不是语法糖,而是解释器在字节码层面硬编码的机制:CALL_FUNCTION_EX指令直接处理栈顶元组和字典,co_flags标记形参特性,类型检查严格限定为tuple和dict,装饰器需同时使用二者以完整转发参数,性能开销主要来自类型检查、字典合并及对象新建。

Python 函数调用时参数如何被拆包和收集
根本不是语法糖,而是解释器在字节码层面硬编码的约定。CALL_FUNCTION_EX 指令专门处理 *args 和 **kwargs,它把栈顶的两个对象(位置参数元组、关键字参数字典)直接塞进函数帧的局部变量中。你写的 def f(*a, **k),CPython 会生成特殊标记的 co_flags(比如 CO_VARARGS | CO_VARKEYWORDS),让运行时知道该从哪取值。
这意味着:*args 不是“把列表转成参数”,而是“把栈上已有的元组整体当位置参数传入”;**kwargs 同理,不是“字典展开”,而是“把栈上字典对象直接绑定为 **k 所指的映射”。
为什么 *args 必须是 tuple、**kwargs 必须是 dict
因为解释器只认这两种类型——源码里 ceval.c 的 call_function_ex 分支明确检查:
- 如果传了非
tuple给*args,抛TypeError: argument after * must be an iterable, not X - 如果传了非
dict给**kwargs,抛TypeError: argument after ** must be a mapping, not X
注意:这里的“必须是 tuple”指调用侧传入的实参类型,不是形参名 a 在函数体内不能被修改。函数体内拿到的 a 确实是 tuple,但你可以把它转成 list 再操作。
立即学习“Python免费学习笔记(深入)”;
*args 和 **kwargs 在装饰器里为什么总要一起出现
因为装饰器要完整转发原始调用,而你无法预知被装饰函数需要什么参数形式。漏掉任意一个,就会破坏调用契约:
- 只写
def deco(f): def wrapper(*args): return f(*args)→ 原函数若带关键字参数,全丢进*args,触发TypeError - 只写
def wrapper(**kwargs)→ 所有位置参数丢失,同样报错 - 正确写法必须是
def wrapper(*args, **kwargs): return f(*args, **kwargs)
这也是为什么 functools.wraps 要重写 __signature__:它得模拟出原函数的参数结构,否则 IDE 和 inspect.signature() 看到的永远只是 (*args, **kwargs)。
性能开销在哪?哪些场景能省掉它们
主要开销在调用时的两次对象检查 + 一次字典合并(**kwargs),以及函数帧内多分配两个局部变量。不常被注意的点是:
- 每次调用都新建
tuple和dict—— 即使你传的是空*[]和**{},也会触发构造 - 用
functools.partial预绑定了参数的函数,底层仍走*args/**kwargs路径,没省掉检查 - 如果确定函数只接收固定参数,就别用
*args/**kwargs做兜底;高频路径上(如事件循环回调)应显式声明参数,避免无谓的元组/字典创建
最易被忽略的是:*args 收集后是不可变 tuple,想改就得转 list;而 **kwargs 是可变 dict,但修改它不会影响调用方传入的原字典——这是浅拷贝行为,不是引用传递。










