《深入 Python 调用栈:*args 与 **kwargs 在内存里到底长什么样?》

2025博客之星年度评选已开启 10w+人浏览 1.8k人参与

**《深入 Python 调用栈:*args 与 kwargs 在内存里到底长什么样?》

在我教授 Python 的这些年里,有两个语法元素几乎贯穿所有课程、项目与面试:*args**kwargs。它们看似简单,却是 Python 函数调用体系中最具代表性的“动态能力”。初学者常把它们当作“可变参数”的语法糖,而资深开发者则知道,它们背后隐藏着 Python 调用栈、对象模型、内存布局与解释器机制的深层逻辑。

这篇文章,我想带你真正走进 Python 内部:
当你写下 def func(*args, **kwargs): 时,解释器到底做了什么?
argskwargs 在内存里是什么结构?
它们如何参与函数调用、栈帧构建与参数绑定?

无论你是刚入门的学习者,还是追求极致性能与理解的高级开发者,我希望这篇文章都能给你带来新的视角与启发。


**一、从 Python 的发展说起:为什么需要 *args 与 kwargs?

Python 自 1991 年诞生以来,一直以“简洁、优雅、灵活”著称。它的函数系统尤其体现了这一点:

  • 支持默认参数
  • 支持关键字参数
  • 支持可变参数
  • 支持解包调用
  • 支持动态绑定

这些能力让 Python 成为“胶水语言”的核心原因之一。你可以轻松写出高度抽象、可扩展的 API,也可以构建灵活的装饰器、回调系统、事件机制。

*args**kwargs 正是 Python 函数系统中最关键的两个“动态入口”。


**二、基础回顾:*args 与 kwargs 到底是什么?

在语法层面:

  • *args:接收任意数量的位置参数,并将其打包成一个 tuple
  • **kwargs:接收任意数量的关键字参数,并将其打包成一个 dict

示例:

def demo(*args, **kwargs):
    print(args, type(args))
    print(kwargs, type(kwargs))

demo(1, 2, 3, name="Alice", age=20)

输出:

(1, 2, 3) <class 'tuple'>
{'name': 'Alice', 'age': 20} <class 'dict'>

这只是表象。
真正的问题是:

解释器是如何把这些参数“打包”成 tuple 和 dict 的?
它们在内存里是什么结构?
为什么是 tuple 和 dict?

接下来,我们将深入 CPython 内部。


三、深入 CPython:函数调用时发生了什么?

当你调用一个函数时,例如:

demo(1, 2, 3, name="Alice")

CPython 会经历以下步骤:

1. 创建调用栈帧(Frame Object)

每个函数调用都会创建一个 PyFrameObject,其中包含:

  • 局部变量表(locals)
  • 全局变量表(globals)
  • 栈空间(value stack)
  • 指令指针(f_lasti)
  • 参数绑定表(fast locals)

2. 参数绑定(Argument Binding)

CPython 使用 PyArg_ParseTupleAndKeywords 机制,将传入的参数绑定到函数定义的参数列表。

当遇到 *args 时:

  • 所有未被绑定的位置参数会被打包成一个 tuple
  • 这个 tuple 是一个真正的 PyTupleObject

当遇到 **kwargs 时:

  • 所有未被绑定的关键字参数会被打包成一个 dict
  • 这个 dict 是一个真正的 PyDictObject

*四、args 在内存里长什么样?

让我们从最简单的例子开始:

def foo(*args):
    pass

foo(10, 20, 30)

1. CPython 会创建一个 PyTupleObject

其结构大致如下(伪结构):

PyTupleObject
 ├── ob_refcnt
 ├── ob_type
 ├── ob_size = 3
 ├── ob_item[0] → PyLongObject(10)
 ├── ob_item[1] → PyLongObject(20)
 └── ob_item[2] → PyLongObject(30)

也就是说:

  • args 是一个 tuple
  • tuple 内部是一个 指针数组
  • 每个元素指向一个 Python 对象(如 PyLongObject)

2. 为什么是 tuple?

因为:

  • tuple 是不可变对象
  • 不可变意味着可以安全共享
  • 解释器可以优化(如缓存小 tuple)

这也是为什么:

def foo(*args):
    args[0] = 100  # ❌ 会报错

**五、kwargs 在内存里长什么样?

示例:

def bar(**kwargs):
    pass

bar(a=1, b=2)

1. CPython 会创建一个 PyDictObject

其结构大致如下:

PyDictObject
 ├── ma_used = 2
 ├── ma_keys
 │     ├── dk_size
 │     ├── dk_entries
 │     │     ├── entry 0: key="a", hash=..., value=PyLongObject(1)
 │     │     └── entry 1: key="b", hash=..., value=PyLongObject(2)
 └── ma_values (可能为空,取决于版本)

dict 的内部结构非常复杂(涉及哈希表、探测、稀疏数组等),但你只需要记住:

  • kwargs 是一个 dict
  • dict 内部是哈希表
  • key 是字符串(PyUnicodeObject)
  • value 是任意 Python 对象

六、真实内存示例:用 dis 看看解释器做了什么

我们用 dis 模块反编译:

import dis

def demo(*args, **kwargs):
    return args, kwargs

dis.dis(demo)

输出(部分):

LOAD_FAST                0 (args)
LOAD_FAST                1 (kwargs)
BUILD_TUPLE              2
RETURN_VALUE

解释器做的事情非常明确:

  • args 已经是 tuple
  • kwargs 已经是 dict
  • 它们在进入函数之前就被构造好了

**七、实战:*args 与 kwargs 的真实应用场景

1. 构建灵活 API

def api_call(url, *args, **kwargs):
    print("extra args:", args)
    print("options:", kwargs)

2. 装饰器

def logger(func):
    def wrapper(*args, **kwargs):
        print("call:", func.__name__)
        return func(*args, **kwargs)
    return wrapper

3. 元编程:动态代理

class Proxy:
    def __init__(self, target):
        self.target = target

    def __getattr__(self, name):
        def wrapper(*args, **kwargs):
            print("proxy call:", name)
            return getattr(self.target, name)(*args, **kwargs)
        return wrapper

4. 解包调用

params = (1, 2)
options = {"debug": True}

func(*params, **options)

**八、性能分析:*args 与 kwargs 的代价

1. tuple 和 dict 的创建是有成本的

每次函数调用:

  • *args → 创建 tuple
  • **kwargs → 创建 dict

2. 关键字参数比位置参数慢

因为:

  • 需要计算 key 的哈希
  • 需要查找哈希表
  • 需要构造 dict

3. 如何优化?

  • 尽量使用位置参数
  • 避免在高频函数中使用 **kwargs
  • 使用 __slots__ 减少 dict 开销
  • 使用 functools.lru_cache 缓存结果

九、案例实战:构建一个通用事件系统

下面是一个真实项目中使用 *args**kwargs 的例子:

class EventBus:
    def __init__(self):
        self.handlers = {}

    def on(self, event):
        def decorator(func):
            self.handlers.setdefault(event, []).append(func)
            return func
        return decorator

    def emit(self, event, *args, **kwargs):
        for func in self.handlers.get(event, []):
            func(*args, **kwargs)

bus = EventBus()

@bus.on("login")
def handle_login(user, ip):
    print(f"{user} login from {ip}")

bus.emit("login", "Alice", ip="127.0.0.1")

这里:

  • emit 使用 *args**kwargs 将事件参数动态传递给处理函数
  • 事件处理函数可以自由定义参数形式

这是 Python 动态能力的典型体现。


**十、前沿视角:*args 与 kwargs 在未来 Python 中的趋势

随着 Python 3.11+ 的性能提升(如 Faster CPython 项目),函数调用成本正在降低。

未来趋势包括:

  • 更快的参数绑定
  • 更高效的 dict 实现
  • 更智能的 JIT 优化(如 PyPy、Pyjion)

*args**kwargs 仍将是 Python 动态能力的核心。


十一、总结

我们从语法、内存结构、解释器机制、性能分析到实战案例,完整解析了:

  • *args 是一个 tuple(PyTupleObject)
  • **kwargs 是一个 dict(PyDictObject)
  • 它们在函数调用前就被构造好
  • 它们是 Python 动态能力的核心
  • 它们在 API、装饰器、元编程、事件系统中广泛使用
  • 它们有性能成本,但也有优化策略

希望这篇文章能让你真正理解:

*args**kwargs 不只是语法糖,而是 Python 调用体系的灵魂。


十二、互动时间

我很想听听你的经验:

  • 你在使用 *args**kwargs 时遇到过哪些坑?
  • 你是否在某些场景下用它们实现过非常巧妙的设计?
  • 你认为 Python 的动态参数机制未来会如何演进?

欢迎在评论区分享你的故事,我们一起交流、一起成长。


如果你愿意,我还可以继续扩展:

  • 绘制内存结构图
  • 展示 CPython 源码片段
  • 对比 PyPy、Cython 中的实现
  • 写一个更复杂的实战项目

告诉我你的需求,我会继续深入。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铭渊老黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值