古明地觉的编程教室 06月22日 06:35
Python 函数在底层长什么样子?
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了 Python 中函数的底层实现机制,揭示了函数在 C 语言层面是如何通过 PyFunctionObject 结构体来构建的。文章详细解释了 PyFunctionObject 结构体的各个字段,包括 globals、builtins、name、code、defaults 等,并通过示例代码展示了它们在 Python 中的具体表现和作用。此外,文章还介绍了函数特化和矢量调用等优化技术,以及函数版本号在其中的作用,帮助读者更深入地理解 Python 函数的内部工作原理。

💡PyFunctionObject 结构体是 Python 中函数在 C 语言层面的实现,它包含了函数所需的各种信息。

🌍`func_globals` 字段存储函数的全局命名空间,使得函数可以访问全局变量。

🔑`func_code` 字段指向 PyCodeObject 对象,包含了函数的字节码等信息,是函数执行的基础。

⚙️`func_defaults` 和 `func_kwdefaults` 分别存储函数的默认参数和关键字参数默认值。

🔒`func_closure` 字段用于实现闭包,保存了内层函数使用的外层作用域的变量。

🚀Python 底层通过矢量调用和函数特化等技术来优化函数执行效率,提高性能。

原创 古明地觉 2024-11-13 10:26 北京


楔子


函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作。而且在调用函数时会干什么来着,没错,要创建栈帧,用于函数的执行。

那么下面就来看看函数在 C 中是如何实现的,生得一副什么模样。



PyFunctionObject


Python 一切皆对象,函数也不例外,函数这种抽象机制在底层是通过 PyFunctionObject 结构体实现的。

// Include/cpython/funcobject.h#define COMMON_FIELDS(PREFIX) \    PyObject *PREFIX ## globals; \    PyObject *PREFIX ## builtins; \    PyObject *PREFIX ## name; \    PyObject *PREFIX ## qualname; \    PyObject *PREFIX ## code; \    PyObject *PREFIX ## defaults; \    PyObject *PREFIX ## kwdefaults; \    PyObject *PREFIX ## closure;typedef struct {    PyObject_HEAD    COMMON_FIELDS(func_)    PyObject *func_doc;             PyObject *func_dict;            PyObject *func_weakreflist;     PyObject *func_module;          PyObject *func_annotations;     PyObject *func_typeparams;      vectorcallfunc vectorcall;    uint32_t func_version;} PyFunctionObject;

如果将宏展开的话,结构体就是下面这个样子。

typedef struct {    PyObject_HEAD    PyObject *func_globals;     PyObject *func_builtins;     PyObject *func_name;     PyObject *func_qualname;     PyObject *func_code;     PyObject *func_defaults;     PyObject *func_kwdefaults;     PyObject *func_closure;    PyObject *func_doc;             PyObject *func_dict;            PyObject *func_weakreflist;     PyObject *func_module;          PyObject *func_annotations;     PyObject *func_typeparams;      vectorcallfunc vectorcall;    uint32_t func_version;} PyFunctionObject;

我们来解释一下这些字段,并实际获取一下,看看它们在 Python 中是如何表现的。

func_globals:global 名字空间

def foo(a, b, c):    passname = "古明地觉"print(foo.__globals__)  # {..., 'name': '古明地觉'}# 拿到的其实就是外部的 global名字空间print(foo.__globals__ is globals())  # True

函数内部之所以可以访问全局变量,就是因为它保存了全局名字空间。

func_builtins:builtin 名字空间

def foo(a, b, c):    passprint(foo.__builtins__ is __builtins__.__dict__)  # True

注意:在之前的版本中,函数内部是没有这个字段的。

func_name:函数的名字

def foo(name, age):    passprint(foo.__name__)  # foo

当然不光是函数,方法、类、模块都有自己的名字。

import numpy as npprint(np.__name__)  # numpyprint(np.ndarray.__name__)  # ndarrayprint(np.array([1, 2, 3]).transpose.__name__)  # transpose

除了 func_name 之外,函数还有一个 func_qualname 字段。

func_qualname:函数的全限定名

print(str.join.__name__)  # joinprint(str.join.__qualname__)  # str.join

函数如果定义在类里面,那么它就叫类的成员函数,但它本质上依旧是个函数,和普通函数并无区别。只是在获取全限定名的时候,会带上类名。

func_code:函数对应的 PyCodeObject 对象

def foo(a, b, c):    passcode = foo.__code__print(code)  # <code object foo at ......>print(code.co_varnames)  # ('a', 'b', 'c')

函数便是基于 PyCodeObject 构建的。

func_defaults:函数参数的默认值

def foo(name="古明地觉", age=16):    pass# 打印的是默认值print(foo.__defaults__)  # ('古明地觉', 16)def bar():    pass# 没有默认值的话,__defaults__ 为 Noneprint(bar.__defaults__)  # None

注:默认值只会创建一次,所以默认值不应该是可变对象。

func_kwdefaults:只能通过关键字参数传递的 "参数" 和 "该参数的默认值" 组成的字典

def foo(name="古明地觉", age=16):    pass# 打印为 None,这是因为虽然有默认值# 但并不要求必须通过关键字参数的方式传递print(foo.__kwdefaults__)  # Nonedef bar(name="古明地觉", *, age=16):    passprint(bar.__kwdefaults__)  # {'age': 16}

加上一个 * 表示后面的参数必须通过关键字的方式传递。

func_closure:一个元组,包含了内层函数使用的外层作用域的变量,即 cell 变量。

def foo():    name = "古明地觉"    age = 17    def bar():        print(name, age)    return bar# 内层函数 bar 使用了外层作用域中的 name、age 变量print(foo().__closure__)"""(<cell at 0x000001FD1D3B02B0: int object at 0x7efe79d4a1c8>, <cell at 0x000001FD1D42E310: str object at 0x7efe7921bc30>)"""print(foo().__closure__[0].cell_contents)  # 17print(foo().__closure__[1].cell_contents)  # 古明地觉

注意:查看闭包属性我们使用的是内层函数。

func_doc:函数的 docstring

def foo():    """    hi,欢迎来到我的小屋    遇见你真好    """    passprint(foo.__doc__)"""    hi,欢迎来到我的小屋    遇见你真好"""

当我们在写 Python 扩展的时候,由于编译之后是一个 pyd,那么就会通过 docstring 来描述函数的相关信息。

func_dict:函数的属性字典

def foo(name, age):    passprint(foo.__dict__)  # {}

函数在底层也是由一个类实例化得到的,所以它也可以有自己的属性字典,只不过这个字典一般为空。

func_module:函数所在的模块

import numpy as npprint(np.array.__module__)  # numpy

除了函数,类、方法、协程也有 __module__ 属性。

func_annotations:类型注解

def foo(name: str, age: int):    pass# Python3.5 新增的语法,但只能用于函数参数# 而在 3.6 的时候,声明变量也可以使用这种方式# 特别是当 IDE 无法得知返回值类型时,便可通过类型注解的方式告知 IDE# 这样就又能使用 IDE 的智能提示了print(    foo.__annotations__)  # {'name': <class 'str'>, 'age': <class 'int'>}  

像 FastAPI、Pydantic 等框架,都大量应用了类型注解。

func_typeparams:类型参数

from typing import TypeVarT = TypeVar('T')S = TypeVar('S')def foo[T, S](x: T, y: S) -> list[S, T]:    return (y, x)print(foo.__type_params__)  # (T, S)class A[T, S]:    def __init__(self, x: T, y: S):        self.x: T = x        self.y: S = ya1 = A[int, float](3, 2.71)a2 = A[str, dict]("hello", {})print(A.__type_params__)  # (T, S)print(a1.__type_params__)  # (T, S)print(a2.__type_params__)  # (T, S)

关于类型参数的更具体用法,可以查阅相关文档,说实话如果是在 Python 里面,这种语法我估计一辈子都不会用。

vectorcallfunc vectorcall:矢量调用协议

函数本质上也是一个实例对象,在调用时会执行类型对象的 tp_call,对应 Python 里的 __call__。但 tp_call 属于通用逻辑,而通用往往也意味着平庸,tp_call 在执行时需要创建临时元组和临时字典来存储位置参数、关键字参数,这些临时对象增加了内存分配和垃圾回收的开销。

如果只是一般的实例对象倒也没什么,但函数不同,它作为实例对象注定是要被调用的。所以底层对它进行了优化,引入了速度更快的 vectorcall,即矢量调用。

关于普通调用(tp_call)和矢量调用(vectorcall)的具体细节,后续会详细说明。总之一个实例对象如果支持矢量调用,那么它也必须支持普通调用,并且两者的结果是一致的,当对象不支持矢量调用时,会退化成普通调用。

uint32_t func_version:版本号,用于函数特化

函数特化是指根据函数的调用模式,生成更高效的特定版本代码,特别是针对那些频繁调用的函数。但函数特化有一个前提,就是函数本身不能够发生改变,于是引入了 func_version 字段。

当函数的某些字段的值发生改变时,func_version 会重置为 0,而当底层看到 func_version 为 0 时,就知道函数发生改变了,特化失效。

def foo(x, y=10):    return x + y# 以下操作会将 func_version 重置为 0# 1. 修改默认参数foo.__defaults__ = (20,)# 2. 修改关键字默认参数# 注:必须是指向一个新的字典,版本号才会重置foo.__kwdefaults__ = {"z": 30}# 3. 修改代码对象# (几乎不可能直接修改,但可以通过某些高级技巧)# 4. 修改注解foo.__annotations__["return"] = int# 5. 修改 vectorcall 函数指针# (这是 C 级别的操作,Python 代码通常无法直接触及)

所以只要函数保持不变,Python 就会用特化版本来优化执行,而我们在工作中基本也不会修改上面这几个字段。



小结


以上就是函数的底层结构,在 Python 里面是由 <class 'function'> 实例化得到的。

def foo(name, age):    pass# <class 'function'> 就是 C 里面的 PyFunction_Typeprint(foo.__class__)  # <class 'function'>但这个类底层没有暴露给我们,我们不能直接用,因为函数通过 def 创建即可,不需要通过类型对象来创建。


后续会介绍更多关于函数相关的知识。

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Python 函数 PyFunctionObject 底层实现
相关文章