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_GetLine、PyFile_WriteObject、PyFile_WriteString)、如何定制解释器加载代码文件的打开方式(PyFile_SetOpenCodeHook与PyFile_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 new
iomodule... 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 之上、分为多层(FileIO→BufferedReader/BufferedWriter→TextIOWrapper)。本文档描述的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为何被忽略:_BufferedIOMixin与TextIOWrapper的name属性是只读的,构造后无法改写。返回对象的类型取决于mode与buffering的组合:二进制无缓冲(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 的实现展示了完整的错误分支,值得逐条对照:
- 整数分支:
PyLong_Check(o)成立时取整数值。特殊地,若传入的是bool,会发出RuntimeWarning: bool is used as a file descriptor(测试见 test_file.py#L144-L147); - fileno 分支:用
PyObject_GetOptionalAttr取fileno属性并调用。返回值若不是整数,抛TypeError: %T.fileno() must return an int, not %T; - 无 fileno 分支:抛
TypeError: argument must be an int, or have a fileno() method.; - 校验分支:最终 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 的实现还揭示了两个文档未展开的细节:
- 结果必须是
str或bytes,否则抛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)带\n,getline(fp, 6)是前 6 个字符——与上表语义完全吻合。该函数也是交互解释器的内部工具:Python/bltinmodule.c中raw_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'模式打开path(path必须是 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 的实现中逐条验证:
- path 保证是
PyUnicodeObject:hook 收到的第一个参数恒为str; userData的传递:调用 hook 时原样回传(实现中即_PyRuntime.open_code_userdata)。由于 hook 可能从不同的 runtime 被调用,文档特别提醒该指针不应直接指向 Python 状态;- 执行期间的导入限制:该 hook 恰在 import 过程中被调用,除非目标模块确定是 frozen 的或已在
sys.modules中,否则应避免在 hook 里 import 新模块; - 一次性安装:hook 一经设置便无法移除或替换,再次调用
PyFile_SetOpenCodeHook返回-1;若解释器已初始化,还会设置SystemError(源码中为"failed to change existing open_code hook")。此外,在Py_Initialize之前调用是安全的(此时若已有 hook,只返回失败而不设异常);解释器已初始化时安装 hook 会触发审计事件setopencodehook(PySys_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);若obj为NULL,写入字面字符串"<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后调用write;PyFile_WriteString则把 C 串转成 unicode 后复用PyFile_WriteObject(v, f, Py_PRINT_RAW)。对f == NULL的处理也各有差异:WriteObject抛TypeError: writeobject with NULL file;WriteString在无既有异常时抛SystemError: null file for PyFile_WriteString,若已存在异常则直接返回-1(避免覆盖原始错误)。
它们在整个解释器中是被高频复用的内部设施,典型的调用点包括:
- Python/errors.c#L1471-L1590:打印 “Exception ignored in: ...” 的 GC/终结器异常报告,其中文件名用
Py_PRINT_RAW、对象本体用repr(flags=0); - Python/_warnings.c#L666-L703:向
sys.stderr拼接“文件:行号: 警告名: 文本”; - Modules/_threadmodule.c#L2253-L2289:输出 “Exception in thread ” 及 traceback;
- Python/bltinmodule.c:内置
print用PyFile_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>、非法文件对象抛AttributeError、NULL文件抛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)暴露的方法只有close、flush(均为 no-op)、fileno、isatty、write,属性closed恒为False、mode恒为"w"、encoding为None;类型带有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_FileSystemDefaultEncoding、Py_FileSystemDefaultEncodeErrors、Py_HasFileSystemDefaultEncoding,以及在非Py_LIMITED_API(或 ≥ 3.6)下可见的Py_UTF8Mode。它们曾是文件对象 C API 的一部分,如今同样被弃用,新扩展不应再引用——这与“第三方代码改用ioAPI”的总体方向一致。
小结:选型建议
| 需求 | 推荐接口 | 备注 |
|---|---|---|
| C 扩展中创建文件/流对象 | 优先走 PythonioAPI | 文档明确的官方建议 |
| 从裸 fd 快速拿一个流(内部/引导场景) | PyFile_FromFd | 注意与 OS fd 混用的缓冲顺序风险 |
| 取对象对应的 fd | PyObject_AsFileDescriptor | 注意 bool 告警、负值ValueError |
| C 层读一行(如交互输入) | PyFile_GetLine(p, n) | n<0时抛EOFError且去尾\n |
| C 层写日志/错误文本 | PyFile_WriteObject/PyFile_WriteString | Py_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),仅供参考