news 2026/10/16 2:15:52

KBEngine 内置 Python 3.7 的 List 对象 C API 完全指南:PyList_* 函数族详解与引擎源码实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
KBEngine 内置 Python 3.7 的 List 对象 C API 完全指南:PyList_* 函数族详解与引擎源码实践
  • 游戏开发
  • 后端

【免费下载链接】kbengine

A MMOG engine of server.

项目地址:https://gitcode.com/gh_mirrors/kb/kbengine
点击查看免费下载

KBEngine 是一个使用 C++ 编写核心、Python 编写游戏逻辑的分布式游戏服务器引擎,其在kbe/src/lib/python下内置了一份定制化的 CPython 3.7.3 解释器(版本号见 patchlevel.h),引擎的 C++ 代码与 Python 脚本层正是通过这份 C API 交互。本指南以该解释器自带的官方参考文档 List Objects(list.rst) 为骨架,完整梳理PyList_*类型检查、创建、读写、修改、切片、排序、转元组及内存池的全部接口,并结合 listobject.c 的底层实现与引擎各模块的真实调用代码,讲透引用计数语义、宏与函数差异和常见陷阱,帮助你写出能在 KBEngine 扩展模块中安全、正确地操作 Python list 的 C/C++ 代码。

一、背景:为什么引擎 C++ 需要直接操作 Python list

KBEngine 的服务器进程(baseapp、cellapp、dbmgr 等)在 C++ 层完成高并发网络、AOI、导航等核心逻辑,而实体属性、AI、玩法规则用 Python 脚本编写。双方交换批量数据时,Python list 是最常用的容器:实体属性批量下发、方法参数收集、序列化与反序列化(pickle)、EntityDef 装饰器参数解析等场景都会产生 list。

引擎源码中到处可见PyList_*的调用,例如:

  • sequence.cpp 中Sequence::seq_concat、seq_repeat、seq_slice用PyList_New+PyList_SET_ITEM构造拼接、重复、切片结果;
  • vector2.cpp 的ScriptVector2::__py_pyList用PyList_New(VECTOR_SIZE)+PyList_SetItem把向量转成 Python list;
  • fixedarray.cpp 用PyList_New(0)生成空初始化数据,并在__py_reduce_ex__中用PyList_New(len)+PyList_SET_ITEM实现实体定长数组的序列化;
  • py_entitydef.cpp 用PyList_Size+PyList_GetItem遍历inspect.getfullargspec返回的 list,校验 EntityDef 装饰器方法参数。

理解这些调用的语义,是读懂乃至扩展 KBEngine 底层模块的基础。

二、类型对象与类型检查

2.1 PyListObject 与 PyList_Type

PyListObject

PyListObject是PyObject的一个子类型,代表一个 Python list 对象。其内部结构(见 listobject.c 中的定义)由ob_item(PyObject **元素数组)、Py_SIZE(当前逻辑长度)与allocated(已分配容量)构成,扩容逻辑由list_resize负责——这正是 Python list 采用“动态数组 + 预分配容量”策略、且append均摊 O(1) 的原因。

PyTypeObject PyList_Type

PyList_Type是PyTypeObject类型的实例,代表 Python 的 list 类型本身,与 Python 层的list是同一个对象(PyList_Type的初始化见 listobject.c)。任何模块要注册、判断或实例化 list 类型都要用到它。

2.2 PyList_Check 与 PyList_CheckExact

int PyList_Check(PyObject *p) int PyList_CheckExact(PyObject *p)
  • PyList_Check(p):若p是 list 对象、或 list 类型的子类型实例,返回真(内部等价于PyObject_TypeCheck(p, &PyList_Type))。
  • PyList_CheckExact(p):仅当p是 list 对象且不是子类型实例时返回真(内部直接比较Py_TYPE(p) == &PyList_Type)。

两者返回 0 表示“不是”,不做异常设置。选择建议:若后续要按PyListObject的内部布局直接访问ob_item(例如PyList_GET_ITEM),应使用PyList_CheckExact,因为子类型可能重载了序列行为;若只是泛型序列处理,用PyList_Check更宽容。

三、创建与填充:PyList_New 的“NULL 元素”陷阱

PyObject* PyList_New(Py_ssize_t len)

成功时返回一个长度为len的新 list,失败返回NULL。其实现(listobject.c)会先尝试从空闲链表(free list)复用对象,否则PyObject_GC_New新分配;当len > 0时用PyMem_Calloc申请元素数组(自动清零),并设置Py_SIZE(op) = size、op->allocated = size。

文档中有一条必须牢记的 note:

若len大于 0,返回 list 的所有元素槽位均被置为NULL。因此在把所有槽位用PyList_SetItem填上真实对象之前,不能对它调用PySequence_SetItem之类的抽象 API,也不能把它暴露给 Python 代码。

也就是说,PyList_New(5)之后若立刻被 Python 侧访问或打印,会因槽位是 NULL 而崩溃或触发断言。正确模式是“创建后立即用PyList_SetItem/PyList_SET_ITEM逐个填充”,KBEngine 的 vector2.cpp 就是标准示范:

PyObject* pyList = PyList_New(VECTOR_SIZE); // 先建空槽位列表 for (int i = 0; i < VECTOR_SIZE; ++i) PyList_SetItem(pyList, i, PyFloat_FromDouble(v[i])); // 逐个填充 return pyList;

PyList_New的两个相关实现细节:

  • len < 0会调用PyErr_BadInternalCall()并返回NULL,属于编程错误;
  • len <= 0时ob_item置为NULL,得到空列表。

四、读取列表:长度与元素

Py_ssize_t PyList_Size(PyObject *list) // 等价于 len(list) Py_ssize_t PyList_GET_SIZE(PyObject *list) // 宏,无错误检查

PyList_Size返回 list 的长度,等价于 Python 层的len(list);实现在 listobject.c,先用PyList_Check校验,失败时调用PyErr_BadInternalCall()并返回-1。PyList_GET_SIZE是其宏形式,跳过类型检查直接返回Py_SIZE(list),仅在确认参数确实是PyListObject时使用。

PyObject* PyList_GetItem(PyObject *list, Py_ssize_t index) PyObject* PyList_GET_ITEM(PyObject *list, Py_ssize_t i)
  • PyList_GetItem返回index位置的元素。只支持正数索引,不支持从尾部负数索引;越界时返回NULL并设置IndexError异常(实现见 listobject.c,异常对象使用缓存的"list index out of range"字符串,避免重复创建)。
  • 返回的是借用引用(borrowed reference):调用方不能Py_DECREF它,同时要注意在持有期间对象可能被其他代码修改而失效。
  • PyList_GET_ITEM是宏形式,无错误检查,仅限确定索引有效时使用。

KBEngine 在 py_entitydef.cpp 中组合使用这两个接口遍历方法参数名列表:

Py_ssize_t argsSize = PyList_Size(pyGetMethodArgsResult); // 取长度 for (Py_ssize_t i = 1; i < argsSize; ++i) { PyObject* pyItem = PyList_GetItem(pyGetMethodArgsResult, i); // 取元素(借用引用) const char* ccattr = PyUnicode_AsUTF8AndSize(pyItem, NULL); defContext.argsvecs.push_back(ccattr); }

五、写入与修改:引用窃取语义是关键

int PyList_SetItem(PyObject *list, Py_ssize_t index, PyObject *item) // 0 成功,-1 失败 void PyList_SET_ITEM(PyObject *list, Py_ssize_t i, PyObject *o) // 宏,无错误检查

PyList_SetItem把index位置的元素设为item,实现见 listobject.c:越界时返回-1并设置IndexError(同时会Py_XDECREF(newitem),所以传入的引用在失败时也会被释放,调用方不应再使用);成功时通过Py_XSETREF(*p, newitem)完成“新引用 + 释放旧元素”。

文档对二者给出两条关键 note:

PyList_SetItem:该函数“窃取”(steals)对item的一个引用,并释放被替换位置原有元素的一个引用。

PyList_SET_ITEM:同样窃取o的引用,但与PyList_SetItem不同,它不会释放被替换位置元素的引用——若该位置原本已有对象,其引用会被泄漏。因此这个宏通常只用于填充全新列表(原本无内容)。

含义:

  • 调用PyList_SetItem/PyList_SET_ITEM之后,调用方不再拥有item的引用(要么已转移给 list,要么在失败时已被释放),一般不再对其Py_DECREF;
  • 填充新建列表时常用PyList_SET_ITEM(零成本、不检查),但要保证槽位原本为空;对已有内容的位置改写用PyList_SetItem才能正确释放旧值。
int PyList_Insert(PyObject *list, Py_ssize_t index, PyObject *item) // 0 成功,-1 失败 int PyList_Append(PyObject *list, PyObject *item) // 0 成功,-1 失败
  • PyList_Insert等价于list.insert(index, item),把item插入到index之前。底层ins1(listobject.c)先做list_resize(self, n+1)扩容,再处理负索引归一化、元素后移、Py_INCREF(v)后写入。注意index允许为负(与 Python 语义一致),且越界会被夹到 [0, n] 区间;长度达到PY_SSIZE_T_MAX时设置OverflowError。
  • PyList_Append等价于list.append(item),底层app1(listobject.c)同样先扩容再Py_INCREF(v)后写入尾部。二者失败时返回-1并设置异常。

与PyList_SetItem的窃取语义不同,PyList_Insert/PyList_Append不窃取item的引用,调用方仍拥有自己的那份引用,需自行Py_DECREF(若不再使用)。

六、切片操作:GetSlice / SetSlice

PyObject* PyList_GetSlice(PyObject *list, Py_ssize_t low, Py_ssize_t high) int PyList_SetSlice(PyObject *list, Py_ssize_t low, Py_ssize_t high, PyObject *itemlist)
  • PyList_GetSlice返回[low, high)区间元素构成的新 list,等价于list[low:high];失败返回NULL并设置异常。底层list_slice(listobject.c)先PyList_New(len)再对每个元素Py_INCREF复制,即浅拷贝。不支持负索引(与 Python 切片不同),且未做边界钳制,调用前应自行确保0 <= low <= high <= 长度。
  • PyList_SetSlice把[low, high)区间替换为itemlist的内容,等价于list[low:high] = itemlist。itemlist可以为NULL,表示赋空列表,即删除该切片(del list[low:high])。成功返回 0,失败返回 -1,同样不支持负索引。底层list_ass_slice(listobject.c)负责处理长度差带来的扩容/缩容与元素引用释放。

切片操作的边界规范可总结为:

操作Python 等价负索引越界行为
PyList_GetSlicelist[low:high]不支持未钳制,需调用方保证合法
PyList_SetSlicelist[low:high] = itemlist不支持未钳制,需调用方保证合法
itemlist == NULLdel list[low:high]—同上

七、排序、反转与转元组

int PyList_Sort(PyObject *list) // 0 成功,-1 失败,等价于 list.sort() int PyList_Reverse(PyObject *list) // 0 成功,-1 失败,等价于 list.reverse() PyObject* PyList_AsTuple(PyObject *list) // 等价于 tuple(list)
  • PyList_Sort原地排序,实现(listobject.c)先校验类型再委托list_sort_impl,即 Python 层list.sort()的同一实现(Timsort),支持key与reverse参数路径;参数不是 list 时设置BadInternalCall并返回-1。
  • PyList_Reverse原地反转,长度大于 1 时调用reverse_slice交换元素(listobject.c),元素引用计数不变。
  • PyList_AsTuple返回包含list全部内容的新 tuple(浅拷贝:新 tuple 对每个元素Py_INCREF,见 listobject.c)。若 list 后续会频繁修改而调用方需要稳定快照,用它比保留 list 更安全。

八、内存优化:free list 与 PyList_ClearFreeList

int PyList_ClearFreeList() // 清空空闲链表,返回释放的元素总数(3.3 版本新增)

CPython 为 list 维护了一个空闲对象池(free list):list 对象被销毁时,若池未满且是精确 list 类型(PyList_CheckExact),会把对象挂入池中而不是直接归还内存(list_dealloc,见 listobject.c);下次PyList_New时优先复用池中对象,显著减少高频创建/销毁小列表时的 malloc/free 开销。

池的容量与结构(listobject.c):

#ifndef PyList_MAXFREELIST #define PyList_MAXFREELIST 80 #endif static PyListObject *free_list[PyList_MAXFREELIST]; static int numfree = 0;

PyList_ClearFreeList遍历池并PyObject_GC_Del释放全部缓存对象,返回被释放数量。因为该 API 自 Python 3.3 引入(文档标注versionadded:: 3.3),本仓库内置的 3.7.3 解释器完全可用。它主要供嵌入式宿主(如 KBEngine)在需要释放解释器内部缓存、统计内存或配合gc模块时调用;KBEngine 在批量创建脚本对象的高频路径上正是受益于这一池化机制。

九、引用计数与安全使用速查

综合文档与 listobject.c 的实现,把最容易出错的地方汇总如下:

场景正确做法依据
PyList_New(len)后必须用PyList_SetItem/PyList_SET_ITEM填满所有槽位,才能暴露给 Python文档 note:槽位初始为NULL
PyList_SetItem/PyList_SET_ITEM窃取item引用,调用方此后不再拥有文档 note
PyList_SET_ITEM改写旧槽会泄漏被替换元素的引用,仅限填充新列表文档 note
PyList_Insert/PyList_Append不窃取引用,调用方需自行释放自己的那份文档与ins1/app1实现(Py_INCREF(v))
PyList_GetItem/PyList_GET_ITEM返回借用引用,不可Py_DECREF文档语义
索引访问只用正索引;越界会得到IndexError(GetItem/SetItem)文档与 listobject.c
切片GetSlice/SetSlice不支持负索引,边界需自行保证文档 note
类型判断需要精确 list 用PyList_CheckExact,宽容判断用PyList_Check文档语义
宏 vs 函数宏(GET_SIZE/GET_ITEM/SET_ITEM)不做错误检查,仅限类型与索引已确认文档语义

十、KBEngine 源码中的典型调用模式

引擎内部对PyList_*的调用可分为三类典型模式,值得作为扩展开发模板:

模式一:新建 + 填充(配合窃取引用)。vector2.cpp 的向量转 list;fixedarray.cpp 的__py_reduce_ex__序列化定长数组,先用PyList_New(len)建空槽,再对每个元素Py_INCREF后PyList_SET_ITEM填入——这正是文档要求的标准填充流程。

模式二:序列运算的中间结果。sequence.cpp 中:

  • seq_concat(PyList_New(seqSize1 + seqSize2))先复制自身元素,再用PySequence_GetItem取对方元素直接PyList_SET_ITEM填入(该函数返回新引用,被窃取后无需再释放);
  • seq_repeat对n <= 0直接返回PyList_New(0),并通过PyList_GET_ITEM+PyList_SET_ITEM复制已填内容完成循环展开;
  • seq_slice用PyList_New(length)+PyList_SET_ITEM构造切片结果。

模式三:读取外部返回值。py_entitydef.cpp 用PyList_Size+PyList_GetItem遍历 Pythoninspect模块返回的参数名 list,属于只读借用场景。

十一、接口总览速查表

以下表格完整覆盖本仓库官方文档 list.rst 所列全部接口:

接口签名返回值Python 等价 / 说明
PyListObject——list 对象的 C 结构类型,PyObject子类型
PyList_TypePyTypeObject—list 类型对象,即 Python 层list
PyList_Checkint (PyObject *p)真/假p是 list 或其子类型实例
PyList_CheckExactint (PyObject *p)真/假p是 list 且非子类型实例
PyList_NewPyObject* (Py_ssize_t len)list 或NULL新列表,len>0时槽位为NULL
PyList_SizePy_ssize_t (PyObject *list)长度或-1len(list)
PyList_GET_SIZEPy_ssize_t (PyObject *list)长度宏,无错误检查
PyList_GetItemPyObject* (PyObject *list, Py_ssize_t index)元素或NULL仅正索引,越界置IndexError
PyList_GET_ITEMPyObject* (PyObject *list, Py_ssize_t i)元素宏,无错误检查
PyList_SetItemint (PyObject *list, Py_ssize_t index, PyObject *item)0 或-1窃取引用,释放被替换旧引用
PyList_SET_ITEMvoid (PyObject *list, Py_ssize_t i, PyObject *o)—宏,窃取引用但会泄漏被替换引用
PyList_Insertint (PyObject *list, Py_ssize_t index, PyObject *item)0 或-1list.insert(index, item)
PyList_Appendint (PyObject *list, PyObject *item)0 或-1list.append(item)
PyList_GetSlicePyObject* (PyObject *list, Py_ssize_t low, Py_ssize_t high)新 list 或NULLlist[low:high],不支持负索引
PyList_SetSliceint (PyObject *list, Py_ssize_t low, Py_ssize_t high, PyObject *itemlist)0 或-1list[low:high] = itemlist;NULL表示删除切片
PyList_Sortint (PyObject *list)0 或-1list.sort()
PyList_Reverseint (PyObject *list)0 或-1list.reverse()
PyList_AsTuplePyObject* (PyObject *list)新 tuple 或NULLtuple(list),浅拷贝
PyList_ClearFreeListint ()释放数量清空 list 空闲池(3.3+)

结语

PyList_*接口看似简单,但“槽位初始为 NULL”“引用窃取”“借用引用”“宏不做检查”这四类语义决定了它比普通 C 数组更容易踩坑。本文以 KBEngine 内置 Python 3.7.3 的官方文档 list.rst 为主线、以 listobject.c 的实现为证据,逐项覆盖了类型检查、创建填充、读写、插入追加、切片、排序反转、转元组与内存池机制,并给出了引擎自身在 pyscript、entitydef 模块中的真实用法。后续无论是为 KBEngine 编写新的 C++ 扩展模块,还是阅读引擎底层代码,都可以把本文的速查表作为直接参考。

  • 游戏开发
  • 后端

【免费下载链接】kbengine

A MMOG engine of server.

项目地址:https://gitcode.com/gh_mirrors/kb/kbengine
点击查看免费下载

相关推荐

上一篇:TradingAgents-CN终极指南:三分钟掌握AI驱动的智能股票投资分析系统
下一篇:SkyReels-V2终极指南:无限视频生成的完整解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 2:15:14

在 Olares 上为 OpenClaw 启用网页搜索:接入 SearXNG 实时联网能力

云原生后端运维 【免费下载链接】Terminus Open-Source Personal Cloud OS for Always-On Agents 项目地址&#xff1a; https://gitcode.com/gh_mirrors/terminus2/Terminus 点击查看 免费下载 OpenClaw 默认仅依赖训练数据作答&#xff0c;无法感知时事新闻与实时网页内容。…

作者头像 李华
网站建设 2026/10/16 2:14:52

贝叶斯网络学习指南:从联合分布拆解到推断与参数学习实践

简介&#xff1a;《贝叶斯网络学习指南》是一部系统讲解贝叶斯网络理论与应用的经典著作&#xff0c;适合机器学习、数据挖掘及概率图模型初学者和进阶者阅读。全书以概率论为起点&#xff0c;依次涵盖贝叶斯定理、随机变量与联合概率分布、贝叶斯推断、因果边构建网络等基础内…

作者头像 李华
网站建设 2026/10/16 2:14:19

微信小程序调用上一页的方法(主包,分包)

微信小程序调用上一页的方法&#xff08;主包&#xff0c;分包&#xff09;一、介绍二、代码1、const pages getCurrentPages();2、const prevPage pages[pages.length - 2];3、const prevVm prevPage.$vm || prevPage;4、if (prevVm) { prevVm.getList.call(prevVm); }一、…

作者头像 李华
网站建设 2026/10/16 2:14:08

AI Agent 面试题 097:LLM-as-Judge模式的原理和应用场景有哪些?

&#x1f525; AI Agent 面试题 097&#xff1a;LLM-as-Judge模式的原理和应用场景有哪些&#xff1f;摘要&#xff1a;本文深入解析了「LLM-as-Judge模式的原理和应用场景有哪些&#xff1f;」这一 AI Agent 领域的核心面试题。文章从 LLM-as-Judge 的基本概念出发&#xff0c…

作者头像 李华
网站建设 2026/10/16 2:12:56

C语言判断二叉排序树与平衡二叉树:递归实现与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华