Python函数调用中args和*kwargs不是语法糖,而是解释器在字节码层面硬编码的机制:CALL_FUNCTION_EX指令直接处理栈顶元组和字典,co_flags标记形参特性,类型检查严格限定为tuple和dict,装饰器需同时使用二者以完整转发参数,性能开销主要来自类型检查、字典合并及对象新建。
根本不是语法糖,而是解释器在字节码层面硬编码的约定。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 再操作。
*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,但修改它不会影响调用方传入的原字典——这是浅拷贝行为,不是引用传递。