CPython C API 文件对象详解:PyFile_* 接口的完整用法与源码剖析
2026/9/7 8:49:08 网站建设 项目流程

CPython C API 文件对象详解:PyFile_* 接口的完整用法与源码剖析

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

本文围绕 CPython 的Doc/c-api/file.rst文档,系统讲解 Python C API 中文件对象(File objects)一族的PyFile_*接口。读完之后,你将掌握:如何从文件描述符创建 Python 文件对象(PyFile_FromFd)、如何从任意对象提取文件描述符(PyObject_AsFileDescriptor)、如何在 C 层读取/写入“行”(PyFile_GetLinePyFile_WriteObjectPyFile_WriteString)、如何定制解释器加载代码文件的打开方式(PyFile_SetOpenCodeHookPyFile_OpenCode系列),以及了解 CPython 3.15 中软弃用的PyFile_NewStdPrinter/PyStdPrinter_Type的来龙去脉。

定位:Python 2 文件 C API 的“最小复刻”

Doc/c-api/file.rst开篇就明确了这组 API 的身份:

These APIs are a minimal emulation of the Python 2 C API for built-in file objects... In Python 3, files and streams use the newiomodule... The functions described below are convenience C wrappers over these new APIs, and meant mostly for internal error reporting in the interpreter; third-party code is advised to access theioAPIs instead.

也就是说,在 Python 2 时代,内置文件对象依赖 C 标准库的FILE*缓冲 I/O;进入 Python 3 后,文件与流全部由 :mod:io模块提供,它建立在操作系统无缓冲 I/O 之上、分为多层(FileIOBufferedReader/BufferedWriterTextIOWrapper)。本文档描述的PyFile_*函数只是这些新 API 之上的便捷 C 封装,主要服务于解释器内部的错误报告场景(如打印回溯、警告、线程异常)。第三方 C 扩展应优先直接使用io的 Python API,而不是依赖这些函数。

对应的公共头文件是 Include/fileobject.h,其中声明了本族的核心函数;内部实现集中在 Objects/fileobject.c。

PyFile_FromFd:从文件描述符创建 Python 文件对象

原型(见 Include/fileobject.h#L11-L13):

PyObject *PyFile_FromFd(int fd, const char *name, const char *mode, int buffering, const char *encoding, const char *errors, const char *newline, int closefd);

它从已打开文件描述符fd创建一个 Python 文件对象。各参数与io.open的含义一一对应(文档原文即建议参照io.open的说明):

参数说明默认值
fd已打开的文件描述符必填
name文件名,自 Python 3.2 起被忽略,仅为向后兼容而保留NULL合法
mode打开模式(如"r""rb""w"必填
buffering缓冲策略,语义同io.open-1表示使用默认
encoding文本编码NULL表示默认
errors编码错误处理策略NULL表示默认
newline换行处理NULL表示默认
closefd关闭流时是否同时关闭底层 fd必填(0/非 0)

失败时返回NULL并设置异常。

源码实现:只是_io.open的薄封装

Objects/fileobject.c#L33-L51 的实现非常简洁——导入_io模块的open,然后以"isisssO"格式转发全部参数:

PyObject * PyFile_FromFd(int fd, const char *name, const char *mode, int buffering, const char *encoding, const char *errors, const char *newline, int closefd) { PyObject *open, *stream; /* import _io in case we are being used to open io.py */ open = PyImport_ImportModuleAttrString("_io", "open"); if (open == NULL) return NULL; stream = PyObject_CallFunction(open, "isisssO", fd, mode, buffering, encoding, errors, newline, closefd ? Py_True : Py_False); Py_DECREF(open); ... return stream; }

源码注释还解释了name为何被忽略:_BufferedIOMixinTextIOWrappername属性是只读的,构造后无法改写。返回对象的类型取决于modebuffering的组合:二进制无缓冲(buffering=0)得到_io.FileIO,二进制缓冲(如1024)得到_io.BufferedReader,文本模式得到_io.TextIOWrapper——这一点在 Lib/test/test_capi/test_file.py#L20-L55 的test_pyfile_fromfd中被逐一断言验证。

警告(文档原文):由于 Python 流自带缓冲层,把它们与操作系统级的文件描述符混用会产生各种问题(例如数据出现意外的先后顺序)。

PyObject_AsFileDescriptor:从对象提取文件描述符

原型(Include/fileobject.h#L17):

int PyObject_AsFileDescriptor(PyObject *p);

文档给出的规则是:若对象是整数,直接返回其值;否则调用对象的fileno()方法(如果存在),方法必须返回整数,即作为 fd 返回;失败时设置异常并返回-1

Objects/fileobject.c#L167-L217 的实现展示了完整的错误分支,值得逐条对照:

  1. 整数分支PyLong_Check(o)成立时取整数值。特殊地,若传入的是bool,会发出RuntimeWarning: bool is used as a file descriptor(测试见 test_file.py#L144-L147);
  2. fileno 分支:用PyObject_GetOptionalAttrfileno属性并调用。返回值若不是整数,抛TypeError: %T.fileno() must return an int, not %T
  3. 无 fileno 分支:抛TypeError: argument must be an int, or have a fileno() method.
  4. 校验分支:最终 fd 为负时抛ValueError: file descriptor cannot be a negative integer

这些行为在 Lib/test/test_capi/test_file.py#L131-L171 的test_pyobject_asfiledescriptor中有完整的正向/异常覆盖。

PyFile_GetLine:C 层读取“一行”

原型(Include/fileobject.h#L14):

PyObject *PyFile_GetLine(PyObject *p, int n);

文档说明它等价于p.readline([n])p可以是文件对象,也可以是任何具有readline方法的对象。n的三种取值语义:

n行为
0恰好读取一行,无论该行多长;若立即到达文件尾,返回空字符串
> 0最多读取n个字节,可能返回不完整的行;立即到文件尾时返回空字符串
< 0无论长度读取一行,但若立即到达文件尾则抛出EOFError

Objects/fileobject.c#L54-L102 的实现还揭示了两个文档未展开的细节:

  • 结果必须是strbytes,否则抛TypeError: %T.readline() must return a str, not %T
  • n < 0时,函数会剥离行尾的\n(bytes 与 unicode 各有一条处理路径);若读取结果为空,则置PyExc_EOFError: EOF when reading a line

对照测试 test_file.py#L57-L84:文本模式下getline(fp, -1)的结果是不带\n的首行,getline(fp, 0)\ngetline(fp, 6)是前 6 个字符——与上表语义完全吻合。该函数也是交互解释器的内部工具:Python/bltinmodule.craw_input的实现即调用PyFile_GetLine(fin, -1)

打开“代码文件”:PyFile_OpenCode 家族与 SetOpenCodeHook

这一组函数(均自 Python 3.8 加入)服务于解释器加载待执行代码的路径,与io.open_code对应:

PyFile_OpenCodeObject 与 PyFile_OpenCode

PyObject *PyFile_OpenCodeObject(PyObject *path); /* path 必须是 str */ PyObject *PyFile_OpenCode(const char *utf8path); /* UTF-8 编码的 C 字符串 */

文档说明:PyFile_OpenCodeObject'rb'模式打开pathpath必须是 Pythonstr对象),其行为可被PyFile_SetOpenCodeHook覆盖,用于对文本做预处理;成功返回文件对象的强引用,失败返回NULL并置异常。PyFile_OpenCode只是它的 UTF-8 C 字符串版包装。

实现印证了这一点,见 Objects/fileobject.c#L512-L547:PyFile_OpenCodeObject先校验path类型,随后优先检查_PyRuntime.open_code_hook——有 hook 就走 hook,没有则导入_io.open并以"Os", path, "rb"调用;PyFile_OpenCode则先用PyUnicode_FromString把 UTF-8 字节串转成str再委托前者。

PyFile_SetOpenCodeHook:给解释器加“代码打开钩子”

头文件中的真实签名(Include/cpython/fileobject.h#L12-L16)带有文档正文里描述的userData参数:

typedef PyObject * (*Py_OpenCodeHookFunction)(PyObject *path, void *userData); int PyFile_SetOpenCodeHook(Py_OpenCodeHookFunction hook, void *userData); PyObject *PyFile_OpenCode(const char *utf8path); PyObject *PyFile_OpenCodeObject(PyObject *path);

文档对该 hook 的约束可归纳为四条,全部可以在 Objects/fileobject.c#L491-L509 的实现中逐条验证:

  1. path 保证是PyUnicodeObject:hook 收到的第一个参数恒为str
  2. userData的传递:调用 hook 时原样回传(实现中即_PyRuntime.open_code_userdata)。由于 hook 可能从不同的 runtime 被调用,文档特别提醒该指针不应直接指向 Python 状态
  3. 执行期间的导入限制:该 hook 恰在 import 过程中被调用,除非目标模块确定是 frozen 的或已在sys.modules中,否则应避免在 hook 里 import 新模块;
  4. 一次性安装:hook 一经设置便无法移除或替换,再次调用PyFile_SetOpenCodeHook返回-1;若解释器已初始化,还会设置SystemError(源码中为"failed to change existing open_code hook")。此外,Py_Initialize之前调用是安全的(此时若已有 hook,只返回失败而不设异常);解释器已初始化时安装 hook 会触发审计事件setopencodehookPySys_Audit("setopencodehook", NULL))。

io.open_code的 Python 侧文档(Doc/library/io.rst)也与该 hook 交叉引用,说明这是嵌入者定制“代码来源”(如加密字节码、沙箱路径重定向)的官方入口。

PyFile_WriteObject 与 PyFile_WriteString:内部错误报告的主力

这两个函数是解释器向文件对象“打字”的底层工具,文档定义如下:

int PyFile_WriteObject(PyObject *obj, PyObject *p, int flags); int PyFile_WriteString(const char *s, PyObject *p);
  • PyFile_WriteObject:把对象obj写入文件对象p。唯一支持的 flag 是Py_PRINT_RAW——给定它写str(obj),否则写repr(obj);若objNULL,写入字面字符串"<NULL>"。成功返回0,失败返回-1并设置相应异常。
  • PyFile_WriteString:把 C 字符串s写入文件对象p,返回值语义相同。

Objects/fileobject.c#L107-L157 的实现值得细看:PyFile_WriteObject通过PyObject_GetAttr(f, &_Py_ID(write))获取write方法,按 flag 选择PyObject_Str/PyObject_Repr后调用writePyFile_WriteString则把 C 串转成 unicode 后复用PyFile_WriteObject(v, f, Py_PRINT_RAW)。对f == NULL的处理也各有差异:WriteObjectTypeError: writeobject with NULL fileWriteString在无既有异常时抛SystemError: null file for PyFile_WriteString,若已存在异常则直接返回-1(避免覆盖原始错误)。

它们在整个解释器中是被高频复用的内部设施,典型的调用点包括:

  • Python/errors.c#L1471-L1590:打印 “Exception ignored in: ...” 的 GC/终结器异常报告,其中文件名用Py_PRINT_RAW、对象本体用reprflags=0);
  • Python/_warnings.c#L666-L703:向sys.stderr拼接“文件:行号: 警告名: 文本”;
  • Modules/_threadmodule.c#L2253-L2289:输出 “Exception in thread ” 及 traceback;
  • Python/bltinmodule.c:内置printPyFile_WriteString写分隔符、PyFile_WriteObject(..., Py_PRINT_RAW)写各元素;raw_input则用PyFile_GetLine(fin, -1)读行。

测试侧的test_pyfile_writeobject/test_pyfile_writestring(test_file.py#L86-L129)验证了:Py_PRINT_RAW下写str、flag 为 0 时写repr(断言输出raw<NULL>'repr'<NULL>)、NULL对象写<NULL>、非法文件对象抛AttributeErrorNULL文件抛TypeError;字符串版本则验证了 UTF-8 编解码路径与非法字节抛UnicodeDecodeError

软弃用 API:PyFile_NewStdPrinter 与 PyStdPrinter_Type

Doc/c-api/file.rst末尾单列了一节 “Soft-deprecated API”,标注.. soft-deprecated:: 3.15:这些 API 是“被错误地”纳入 Python C API 的,文档仅出于完整性而保留,建议使用其他PyFile*API 替代。

PyFile_NewStdPrinter(int fd)

文档给出的替代方案是:改用PyFile_FromFd加默认参数,即PyFile_FromFd(fd, NULL, "w", -1, NULL, NULL, NULL, 0)

从源码看它的历史使命很清晰:Objects/fileobject.c#L290-L316 的注释说明 stdprinter 用于引导阶段(bootstrapping)作为sys.stderr的临时文件对象——那时_io尚未就绪,无法创建真正的文件对象。实现中有两处值得注意:

  • 只接受fileno(stdout)fileno(stderr),否则返回NULL
  • write方法(#L318-L369)直接调用_Py_write(self->fd, ...)写裸 fd:优先用PyUnicode_AsUTF8AndSize编码,遇到 surrogate 时用backslashreplace兜底;fd 无效(如 Windows 上 stderr 失效)时静默返回None而不是抛异常,以避免错误报告路径上的无限递归。

它的类型PyStdPrinter_Type(#L442-L483)暴露的方法只有closeflush(均为 no-op)、filenoisattywrite,属性closed恒为Falsemode恒为"w"encodingNone;类型带有Py_TPFLAGS_DISALLOW_INSTANTIATION,禁止从 Python 侧实例化。测试 test_file.py#L173-L220 完整覆盖了这些行为,包括通过os.dup2把临时文件接到 fd 1 上验证write的实际落盘与 surrogate 编码("[\udc80]"写入 8 字节)。

当前代码中它仍在启动路径被真实使用:Python/sysmodule.c#L4229 在创建sys模块时以PyFile_NewStdPrinter(fileno(stderr))先期填充sys.stderr,随后io基础设施就绪后才会替换为真正的文件对象。

顺带一提:头文件中的弃用全局变量

Include/fileobject.h#L22-L30 还声明了一组标记Py_DEPRECATED(3.12)的全局数据:Py_FileSystemDefaultEncodingPy_FileSystemDefaultEncodeErrorsPy_HasFileSystemDefaultEncoding,以及在非Py_LIMITED_API(或 ≥ 3.6)下可见的Py_UTF8Mode。它们曾是文件对象 C API 的一部分,如今同样被弃用,新扩展不应再引用——这与“第三方代码改用ioAPI”的总体方向一致。

小结:选型建议

需求推荐接口备注
C 扩展中创建文件/流对象优先走 PythonioAPI文档明确的官方建议
从裸 fd 快速拿一个流(内部/引导场景)PyFile_FromFd注意与 OS fd 混用的缓冲顺序风险
取对象对应的 fdPyObject_AsFileDescriptor注意 bool 告警、负值ValueError
C 层读一行(如交互输入)PyFile_GetLine(p, n)n<0时抛EOFError且去尾\n
C 层写日志/错误文本PyFile_WriteObject/PyFile_WriteStringPy_PRINT_RAW决定str还是repr
定制代码文件的打开方式(嵌入器)PyFile_SetOpenCodeHook+PyFile_OpenCode(Object)一次性安装;3.8+;审计事件setopencodehook
PyFile_NewStdPrinter/PyStdPrinter_Type避免在新代码中使用3.15 起软弃用,仅供了解启动引导机制

相关实现与测试入口:Objects/fileobject.c、Include/fileobject.h、Include/cpython/fileobject.h、Modules/_testlimitedcapi/file.c、Modules/_testcapi/file.c、Lib/test/test_capi/test_file.py。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询