ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CPython C API 函数对象实战指南:PyFunctionObject、属性访问 API 与函数生命周期 Watcher 机制

CPython C API 函数对象实战指南:PyFunctionObject、属性访问 API 与函数生命周期 Watcher 机制 CPython C API 函数对象实战指南PyFunctionObject、属性访问 API 与函数生命周期 Watcher 机制【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文基于 CPython 源码仓库中的官方文档 Doc/c-api/function.rstFunction Objects 一章编写系统讲解 C 扩展如何创建、检查和操纵 Python 函数对象function类型即types.FunctionType包括PyFunctionObject结构、PyFunction_New/PyFunction_NewWithQualName构造函数、各Get/Set属性 API、PyFunction_GET_*快速访问器、PyFunction_SetVectorcall以及 3.12 引入的函数生命周期监视器 APIPyFunction_AddWatcher等。读完本篇后你能在 C 扩展中正确构造函数、读写其代码对象/默认值/闭包/注解等属性并安全地监听函数对象的创建、销毁与关键属性变更事件。一、函数对象是什么PyFunctionObject 与 PyFunction_Type文档开头指出There are a few functions specific to Python functions——CPython 专门为 Python 函数提供了一组 C API。核心类型有两个PyFunctionObject函数对象的 C 结构体PyFunction_TypePyTypeObject类型的实例代表 Python 的函数类型在 Python 层暴露为types.FunctionType。两者都定义在公共头文件 Include/cpython/funcobject.h 中注意该头文件只在未启用Py_LIMITED_API时可见即属于 CPython 扩展 API不属于 Limited API。从源码结构看PyFunctionObject的字段布局见 funcobject.h 第 3662 行为typedef struct { PyObject_HEAD _Py_COMMON_FIELDS(func_) // 展开为 func_globals/func_builtins/ // func_name/func_qualname/func_code/ // func_defaults/func_kwdefaults/func_closure PyObject *func_doc; // __doc__ 属性可以是任意对象 PyObject *func_dict; // __dict__ 属性dict 或 NULL PyObject *func_weakreflist; // 弱引用列表 PyObject *func_module; // __module__ 属性可以是任意对象 PyObject *func_annotations; // 注解dict 或 NULL PyObject *func_annotate; // 用于填充注解字典的可调用对象 PyObject *func_typeparams; // 活跃类型变量PEP 695元组或 NULL vectorcallfunc vectorcall; // vectorcall 调用入口 uint32_t func_version; // 供 Tier 1 特化器使用的版本号 } PyFunctionObject;其中_Py_COMMON_FIELDS宏展开了 8 个公共字段func_globals__globals__、func_builtins__builtins__、func_name__name__、func_qualname__qualname__、func_code__code__一个 code 对象、func_defaults__defaults__NULL 或元组、func_kwdefaults__kwdefaults__NULL 或字典、func_closure__closure__NULL 或 cell 对象元组。头文件还声明了一条关键不变式func_closure的长度必须等于 code 对象的自由变量数PyCode_GetNumFree(func_code)。实现代码位于 Objects/funcobject.c。PyFunction_Type的类型对象定义见 funcobject.c 第 12751316 行几个值得注意的槽位tp_vectorcall_offset offsetof(PyFunctionObject, vectorcall)函数对象直接内嵌 vectorcall 函数指针调用时走PyVectorcall_CallPy_TPFLAGS_HAVE_VECTORCALL | Py_TPFLAGS_METHOD_DESCRIPTOR函数是方法描述符通过func_descr_get见 第 12661273 行在访问实例属性时自动绑定为PyMethod_New(func, obj)tp_repr为func_repr输出形如function f at 0x...使用func_qualnametp_flags含Py_TPFLAGS_HAVE_GC函数对象参与垃圾回收。文档中还顺带说明了文档索引里的 MethodType 条目但PyFunctionObject/PyFunction_Type本身只描述函数类型。PyFunction_Check类型检查int PyFunction_Check(PyObject *o)文档描述当且仅当o的类型是PyFunction_Type时返回真值参数不得为NULL此函数总是成功。实现上它不是一个函数而是头文件中的宏见 funcobject.h 第 68 行#define PyFunction_Check(op) Py_IS_TYPE((op), PyFunction_Type)即严格类型检查不是PyObject_TypeCheck不接受子类——事实上function类型也无从实例化出子类语义。二、创建函数对象PyFunction_New 与 PyFunction_NewWithQualNamePyFunction_NewPyObject *PyFunction_New(PyObject *code, PyObject *globals)返回与 code 对象code关联的新函数对象globals必须是函数可访问的变量所在的字典。文档明确函数的 docstring 和名称从 code 对象获取__module__从globals获取参数默认值、注解和闭包被置为NULL__qualname__被设置为与 code 对象的co_qualname相同的值。实现上见 funcobject.c 第 375379 行PyFunction_New只是一行转发PyObject * PyFunction_New(PyObject *code, PyObject *globals) { return PyFunction_NewWithQualName(code, globals, NULL); }PyFunction_NewWithQualName3.3PyObject *PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname)与PyFunction_New相同但额外允许设置函数对象的__qualname__属性。qualname应为 unicode 对象或NULL传NULL时__qualname__取 code 对象的co_qualname值。其完整实现 PyFunction_NewWithQualName 揭示了文档描述背后的细节引用管理对globals和code执行_Py_INCREF_DICT/_Py_INCREF_CODE加引用name/qualname/doc/module均持新引用docstring 提取仅当code_obj-co_flags CO_HAS_DOCSTRING时从co_consts[0]取 doc且必须通过PyUnicode_Check校验否则回退为None——这解释了文档说的docstring 从 code 对象获取__module__与__builtins__通过PyDict_GetItemRef(globals, __name__)取module取不到则为 NULL再用_PyDict_LoadBuiltinsFromGlobals(globals)解析出 builtins 字典默认值等字段初始化为 NULLfunc_defaults NULL、func_kwdefaults NULL、func_closure NULL、func_annotations NULL与文档argument defaults, annotations and closure are set to NULL一一对应vectorcall 与版本号vectorcall _PyFunction_Vectorcall内部默认实现声明于 Include/internal/pycore_function.hfunc_version FUNC_VERSION_UNSET延迟引用计数优化从源码结构看第 226235 行只有当 code 对象不带CO_NESTED标志顶层函数或带CO_METHOD标志类作用域内的方法时才调用_PyObject_SetDeferredRefcount因为嵌套函数更可能捕获变量、更需要及时析构最后注册 GC_PyObject_GC_TRACK并触发PyFunction_EVENT_CREATE监视器事件见第五节。错误路径上globals断言非 NULL 且必须是任意字典PyAnyDict_Checkcode断言有co_name任一初始化步骤失败都会释放所有已持有引用后返回NULL。Python 层的等价入口function.new除 C API 外CPython 还在 Python 层提供了function类型的构造器其签名clinic 定义见 funcobject.c 第 10851102 行为function.__new__(code, globals, nameNone, defaultsNone, closureNone, kwdefaultsNone)实现 func_new_impl 会对各参数做严格校验name必须是字符串或 Nonedefaults必须是元组或 Noneclosure必须是 cell 对象组成的元组且长度必须等于code-co_nfreevars否则抛ValueErrorkwdefaults必须是字典或 None。校验通过后内部同样调用PyFunction_New并覆写相应字段最后触发function.__new__审计事件。这与 C API 相比多了一层逐元素的闭包形态校验C API 的PyFunction_SetClosure则只校验容器是否为元组见下文测试中的说明。三、属性读取 API 一览以下读取函数在 Doc/c-api/function.rst 中逐一列出实现均在 Objects/funcobject.c且全部遵循同一模式先用PyFunction_Check校验不是函数对象则调用PyErr_BadInternalCall()对应 Python 层的SystemError并返回NULL校验通过则直接返回对应字段返回的是借用引用调用方不得Py_DECREF。API返回内容可能为 NULL 的情况实现位置PyFunction_GetCode(op)函数关联的 code 对象不会code 必非空L381-L389PyFunction_GetGlobals(op)__globals__字典不会L391-L399PyFunction_GetModule(op)__module__属性会文档注明can be NULLL401-L409PyFunction_GetDefaults(op)位置参数默认值会是元组或 NULLL411-L419PyFunction_GetKwDefaults(op)仅关键字参数默认值会是字典或 NULLL460-L468PyFunction_GetClosure(op)闭包会是 cell 元组或 NULLL499-L507PyFunction_GetAnnotations(op)注解会是可变字典或 NULLL585-L593两点细节值得展开PyFunction_GetModule文档强调返回的是__module__的借用引用、can be NULL、通常是一个模块名字符串但可被 Python 代码设置为任意其他对象。实现确实只是return ((PyFunctionObject *)op)-func_module;。注意这与 Python 属性f.__module__的行为略有差异属性 getter func_get_module 在字段为 NULL 时返回None而 C API 直接返回 NULL。PyFunction_GetAnnotations文档说返回mutable dictionary or NULL但实现 func_get_annotation_dict 比直接读字段更复杂若func_annotations为 NULL 但存在可调用对象func_annotatePEP 649 的惰性注解机制会先调用__annotate__(1)生成注解字典并缓存若字段暂存的是元组形态则惰性转换为字典。因此该 API 是可能执行用户代码的与其余只读字段的 Get 函数不同。快速访问器PyFunction_GET_*无类型检查PyObject *PyFunction_GET_CODE(PyObject *op) PyObject *PyFunction_GET_GLOBALS(PyObject *op) PyObject *PyFunction_GET_MODULE(PyObject *op) PyObject *PyFunction_GET_DEFAULTS(PyObject *op) PyObject *PyFunction_GET_KW_DEFAULTS(PyObject *op) PyObject *PyFunction_GET_CLOSURE(PyObject *op) PyObject *PyFunction_GET_ANNOTATIONS(PyObject *op)文档说明这些函数与对应的PyFunction_Get*等价但不做类型检查传入非PyFunction_Type实例是未定义行为。头文件实现见 funcobject.h 第 88123 行表明它们是static inline函数加同名宏static inline PyObject* PyFunction_GET_CODE(PyObject *func) { return _PyFunction_CAST(func)-func_code; } #define PyFunction_GET_CODE(func) PyFunction_GET_CODE(_PyObject_CAST(func))其中_PyFunction_CAST内嵌了assert(PyFunction_Check(func))即仅在调试断言下兜底。在热路径上例如解释器内部的_PyFunction_VerifyStateless见 funcobject.c 第 13191374 行校验函数是否无状态以供跨解释器共享就直接使用这些快速访问器以避免每次调用都做类型判断。四、属性修改 API 及其类型不变式修改类 API 的共同点都要求参数是Py_None或指定容器类型失败时置异常并返回-1PyFunction_SetKwDefaults文档措辞是returns 0 on success, and returns -1 with an exception set on failure与其余 Set 函数的 0/-1 约定一致。更重要的是从源码结构看这些 setter 在写入字段前后都会执行同一套底层动作handle_func_event(PyFunction_EVENT_MODIFY_XXX, func, defaults); // 通知监视器 _PyEval_StopTheWorld(interp); // 暂停世界保证无其他线程正在调用该函数 func_clear_version(interp, func); // 清除特化器版本号 ... 原子替换字段指针 ... _PyEval_StartTheWorld(interp); Py_XDECREF(old_xxx);StopTheWorld保证替换__defaults__/__code__/__closure__等关键字段期间不会有其他线程正基于旧值执行调用而func_clear_version则使 Tier 1 特化器针对该函数生成的特化 CALL 指令失效见 Objects/funcobject.c 第 251307 行 的内部注释func_version在 code/defaults/kwdefaults 等被修改时清零此后该函数的调用不再被特化。逐个说明PyFunction_SetDefaultsint PyFunction_SetDefaults(PyObject *op, PyObject *defaults)defaults必须是Py_None或元组失败时抛SystemError并返回-1。实现 PyFunction_SetDefaults 中Py_None会被归一化为NULL即清除默认值元组则先Py_INCREF再替换。Python 属性__defaults__的 setter func_set_defaults 语义一致但额外触发object.__setattr__/object.__delattr__审计事件错误类型是TypeError而非SystemError——C API 面向内部调用所以报SystemErrorPython 属性面向用户代码所以报TypeError。PyFunction_SetKwDefaultsint PyFunction_SetKwDefaults(PyObject *op, PyObject *defaults)defaults必须是仅关键字参数默认值的字典或Py_None成功返回 0失败返回 -1 并置异常。实现见 funcobject.c 第 470497 行非字典时置SystemError(non-dict keyword only default args)。PyFunction_SetClosureint PyFunction_SetClosure(PyObject *op, PyObject *closure)closure必须是Py_None或 cell 对象元组失败抛SystemError返回 -1。实现 PyFunction_SetClosure 只校验PyTuple_Check(closure)并不逐个校验元素是否为 cell。测试文件 Lib/test/test_capi/test_function.py 里有一条耐人寻味的注释印证了这一点# NOTE: this works, but goes against the docs: _testcapi.function_set_closure(function_without_closure, (1, 2))即 C API 接受非 cell 的元组元素虽然这违背文档声明——编写扩展时应自行保证元素为 cell 对象。PyFunction_SetAnnotationsint PyFunction_SetAnnotations(PyObject *op, PyObject *annotations)annotations必须是字典或Py_None失败抛SystemError返回 -1。实现 PyFunction_SetAnnotations 除替换func_annotations外还会把func_annotate一并清空——即显式设置注解字典后PEP 649 的惰性注解回调被移除。PyFunction_SetVectorcall3.12void PyFunction_SetVectorcall(PyFunctionObject *func, vectorcallfunc vectorcall)设置函数对象的 vectorcall 字段。文档给出警告extensions using this API must preserve the behavior of the unaltered (default) vectorcall function!——即自定义 vectorcall 必须保持默认_PyFunction_Vectorcall的调用语义。实现 PyFunction_SetVectorcall 与其他修改器一致StopTheWorld 清版本 写指针 StartTheWorld。由于PyFunction_Type声明了tp_vectorcall_offset解释器对函数的每次调用都会读取这个字段分派因此替换它是实现函数包装/拦截例如某些计时、审计扩展的底层手段版本号被清除意味着 Tier 1 特化器会回退到通用 CALL 路径这是必须保持默认行为警告的另一层含义。五、函数生命周期监视器WatcherAPI3.12这是文档篇幅最大的部分也是近年 C API 中最实用的新增能力它允许 C 扩展像审计钩子一样监听当前解释器中函数对象的创建、销毁与关键属性修改。事件类型 PyFunction_WatchEvent文档列出的枚举事件PyFunction_EVENT_CREATEPyFunction_EVENT_DESTROYPyFunction_EVENT_MODIFY_CODEPyFunction_EVENT_MODIFY_DEFAULTSPyFunction_EVENT_MODIFY_KWDEFAULTSPyFunction_PYFUNC_EVENT_MODIFY_QUALNAME3.15 新增头文件 funcobject.h 第 132144 行 中枚举由 X-macro 展开当前仓库实际包含的事件为CREATE、DESTROY、MODIFY_CODE、MODIFY_DEFAULTS、MODIFY_KWDEFAULTS、MODIFY_QUALNAME#define PY_FOREACH_FUNC_EVENT(V) \ V(CREATE) \ V(DESTROY) \ V(MODIFY_CODE) \ V(MODIFY_DEFAULTS) \ V(MODIFY_KWDEFAULTS) \ V(MODIFY_QUALNAME) typedef enum { #define PY_DEF_EVENT(EVENT) PyFunction_EVENT_##EVENT, PY_FOREACH_FUNC_EVENT(PY_DEF_EVENT) #undef PY_DEF_EVENT } PyFunction_WatchEvent;注意文档中的PyFunction_PYFUNC_EVENT_MODIFY_QUALNAME写法是文档笔误源码里的事件名是PyFunction_EVENT_MODIFY_QUALNAME。注册与注销int PyFunction_AddWatcher(PyFunction_WatchCallback callback)为当前解释器注册callback作为函数监视器返回可传给PyFunction_ClearWatcher的 ID出错例如没有更多可用的 watcher ID时返回-1并置异常。int PyFunction_ClearWatcher(int watcher_id)注销先前由PyFunction_AddWatcher返回的watcher_id。成功返回 0出错如该 ID 从未注册返回 -1 并置异常。实现细节见 funcobject.c 第 81114 行监视器存放在PyInterpreterState的func_watchers数组中用位图active_func_watchers标记活跃槽位AddWatcher线性查找第一个空槽返回其下标作为 ID找不到时抛RuntimeError(no more func watcher IDs available)ClearWatcher对越界 ID 抛ValueError(invalid func watcher ID %d)对未注册 ID 抛ValueError(no func watcher set for ID %d)。注意这是每解释器粒度的 APIfor the current interpreter多解释器场景下需要各自注册。回调契约 PyFunction_WatchCallbackint (*PyFunction_WatchCallback)(PyFunction_WatchEvent event, PyFunctionObject *func, PyObject *new_value);文档对回调的约束非常严格逐条对应源码事实借用引用语义new_value是即将存入func的新值的借用引用CREATE/DESTROY事件下new_value为NULL。头文件注释funcobject.h 第 146164 行与文档一致。只读约束回调可以检查但不得修改func否则可能产生不可预测的效果包括无限递归。原因是事件在字段替换前触发见下条若在回调里再走 setter 会重入同一监视器链路。事件触发时机CREATE事件在函数对象完全初始化之后发出PyFunction_NewWithQualName 末尾 的handle_func_event(PyFunction_EVENT_CREATE, op, NULL)MODIFY_*事件在实际修改之前发出各 setter 中先handle_func_event再写字段因此回调内看到的是旧状态DESTROY在析构路径 func_dealloc 中发出。运行时优化豁免文档说明运行时被允许尽可能优化掉函数对象的创建此时不会发出事件。这不会改变 Python 代码语义但意味着监视器不应依赖 CREATE 事件计数来精确统计函数定义次数。DESTROY 时的复活语义在销毁回调中对该函数加引用会复活它推迟到稍后真正析构届时该时刻仍活跃的监视器会再次收到 DESTROY 事件。对应实现是 func_dealloc 中的_PyObject_ResurrectStart/_PyObject_ResurrectEnd包围handle_func_event(PyFunction_EVENT_DESTROY, op, NULL)若回调期间引用计数回升析构直接返回等待下次归零。异常处理契约回调若设置异常必须返回 -1该异常会通过PyErr_WriteUnraisable以unraisable exception打印否则应返回 0。回调进入时可能已存在挂起异常——此时应带着同一异常原样返回 0且不得调用任何可能设置异常的 API除非先保存、清除并在返回前恢复异常状态。实现侧 notify_func_watchers 在cb(...) 0时调用PyErr_FormatUnraisable(Exception ignored in %s watcher callback for function %U at %p, ...)func_event_name辅助函数把枚举转成PyFunction_EVENT_XXX字符串用于报错文本。事件分发的另一重身份JIT/特化器失效handle_func_eventfuncobject.c 第 5279 行在通知监视器之后还做了一件事对MODIFY_CODE/MODIFY_DEFAULTS/MODIFY_KWDEFAULTS/MODIFY_QUALNAME事件在启用 Tier 2 优化_Py_TIER2时调用_Py_Executors_InvalidateDependency(interp, func, 1)失效依赖该函数的 JIT 代码并累加func_modification统计。从源码结构看监视器机制与字节码特化/JIT 的版本失效共用同一条事件通路——这也是为什么修改__code__、__defaults__、__kwdefaults__、__qualname__会触发事件而修改__name__、__doc__、__module__不会它们的 setter 不经过handle_func_event。六、测试如何验证这套 APICPython 标准库测试 Lib/test/test_capi/test_function.py 通过辅助扩展_testcapiC 封装见 Modules/_testcapi/function.c对上述 API 做了端到端验证值得作为使用范式参考每个PyFunction_Get*都用正常函数与非函数对象如None/1两类输入断言前者结果与f.__code__、f.__globals__、f.__defaults__等 Python 属性一致后者期望SystemError对应PyErr_BadInternalCall()。例如code _testcapi.function_get_code(some) self.assertEqual(code, some.__code__) with self.assertRaises(SystemError): _testcapi.function_get_code(None) # not a functionPyFunction_SetDefaults测试覆盖了错误输入非元组、非函数对象均抛SystemError且原值不变、空元组、元组子类tuple subclasses must work因实现只要求PyTuple_Check以及None归一化为 NULL 后属性读取返回None闭包测试验证了无闭包函数返回 NULL、有闭包时长度等于co_freevars、元素为 cell的不变式测试中用types.CellType手工构造 cell 元组调用PyFunction_SetClosure文件末尾注释标明PyFunction_AddWatcher/PyFunction_ClearWatcher由test_capi.test_watchers专门测试# PyFunction_AddWatcher() and PyFunction_ClearWatcher() are tested by test_capi.test_watchers.。这些测试同时印证了本文各节给出的语义Get 系列返回借用引用封装里Py_NewRef一次再返回、Set 系列的 0/-1 与异常类型、以及 C API 与 Python 属性之间的细微差别。七、速查与使用注意把 Doc/c-api/function.rst 的 API 汇总为速查表版本列为文档标注的 versionadded当前仓库版本号为 3.16.0a0见 Include/patchlevel.h故全部 API 均可用API说明错误语义版本PyFunction_Check(o)类型检查非 NULL 参数总是成功-PyFunction_New(code, globals)创建函数__qualname__取co_qualname失败返回 NULL-PyFunction_NewWithQualName(code, globals, qualname)同上且可指定__qualname__unicode 或 NULL失败返回 NULL3.3PyFunction_GetCode/GetGlobals/GetModule/GetDefaults/GetKwDefaults/GetClosure/GetAnnotations读取对应属性返回借用引用非函数输入置SystemError返回 NULL-PyFunction_SetDefaults(op, defaults)Py_None或元组SystemError返回 -1-PyFunction_SetKwDefaults(op, defaults)Py_None或字典置异常返回 -1-PyFunction_SetClosure(op, closure)Py_None或 cell 元组SystemError返回 -1-PyFunction_SetAnnotations(op, annotations)Py_None或字典同时清除func_annotateSystemError返回 -1-PyFunction_SetVectorcall(func, vectorcall)替换 vectorcall 入口须保持默认语义无void3.12PyFunction_GET_*无类型检查的快速访问器误用是 UB--PyFunction_AddWatcher(cb)注册监视器返回 ID无可用 ID 时置异常返回 -13.12PyFunction_ClearWatcher(id)注销监视器ID 非法/未注册时置异常返回 -13.12几条实践性提醒引用计数纪律所有Get返回借用引用跨 API 边界传递前需Py_NewRefSet系列对传入对象自行加引用调用方保留自己原有的引用不变NULL 语义分层__module__、__defaults__、__kwdefaults__、__closure__、__annotations__在 C 层都可能是 NULL而 Python 属性视角下通常呈现为None如func_get_module把 NULL 映射为None跨层比对时留意这一差异修改类 API 会 StopTheWorld 并使特化/JIT 依赖失效因此它们不是廉价字段写入高频路径上应避免反复重写__code__/__defaults__等watcher 回调是无锁重入环境不得修改func、不得随意调用可能设置异常的 API除非妥善保存/恢复异常状态且要容忍函数创建被优化掉导致事件缺失与DESTROY 回调中复活对象引发二次 DESTROY两种边界情况。八、延伸阅读路径官方文档源文件Doc/c-api/function.rst本文的骨架含全部.. c:function::定义与 versionadded 标注类型与结构定义Include/cpython/funcobject.hPyFunctionObject、PyFunction_Check宏、PY_FOREACH_FUNC_EVENT、回调签名以及同文件中的PyClassMethod_Type/PyStaticMethod_Typeclassmethod/staticmethod 类型定义与函数类型同处一文件核心实现Objects/funcobject.cPyFunction_NewWithQualName、各 Get/Set、function.__new__、PyFunction_Type类型表、PyFunction_Type的 dealloc/repr/traverse、_PyFunction_VerifyStateless测试Lib/test/test_capi/test_function.py 与 C 封装 Modules/_testcapi/function.c。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表