Python fdopen 的关闭责任:文件对象已经关闭,为什么原描述符还能继续写
一个辅助函数接收已经打开的文件描述符,为了方便写入,把它包装成文件对象。离开with以后,调用者有时还能写,有时却收到Bad file descriptor。关键不在with是否执行,而在包装对象是否同时拥有关闭底层描述符的责任。
本文在Linux、CPython 3.12.14实跑。保存为demo.py,运行python3 demo.py。程序只在自动清理的私有临时目录创建一个小文件,不接触现有业务文件。第一轮用closefd=False借用描述符;第二轮省略参数,观察默认所有权;最后检查文件名入口。
AI模型生成概念插图:一侧外层套筒离开后通道仍开着,另一侧套筒与通道一起封闭,用来说明关闭责任;不是系统句柄截图。
完整程序与本地结果
完整可运行程序
import errno
import os
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory(prefix="fd-owner-") as directory:
path = Path(directory) / "sample.bin"
fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "wb", closefd=False) as borrowed:
borrowed.write(b"A")
assert borrowed.closed
assert os.write(fd, b"B") == 1
assert path.read_bytes() == b"AB"
print("borrowed wrapper closed:", borrowed.closed)
print("original descriptor still writes:", path.read_bytes())
finally:
os.close(fd)
owned_fd = os.open(path, os.O_RDONLY)
with os.fdopen(owned_fd, "rb") as owned:
assert owned.read() == b"AB"
try:
os.fstat(owned_fd)
except OSError as exc:
assert exc.errno == errno.EBADF
print("default ownership:", "EBADF")
else:
os.close(owned_fd)
raise AssertionError("owned descriptor should be closed")
try:
open(path, "rb", closefd=False)
except ValueError:
print("filename with closefd=False:", "ValueError")
else:
raise AssertionError("closefd=False requires a descriptor")本次实际输出(以下为结果,不是程序)
borrowed wrapper closed: True original descriptor still writes: b'AB' default ownership: EBADF filename with closefd=False: ValueError
关闭包装对象,不一定关闭底层通道
borrowed.closed显示True,但os.write仍能写入B,文件最后是b'AB'。closefd=False明确让底层描述符在文件对象关闭后继续有效。with已经正常结束,关闭动作也发生了,只是它没有接管调用方持有的描述符。
文件对象关闭时会处理自己的缓冲,因此A先被写出,随后底层写入B。这个顺序不能反推任意混用都安全:如果包装对象还开着且有未刷新的缓冲,直接对描述符读写可能让位置与内容变得难以预测。示例刻意先结束包装层,再使用原描述符。
默认参数会把关闭责任交给包装层
第二轮os.fdopen没有指定closefd,默认值为True。退出with后,os.fstat立即得到EBADF,说明owned_fd对应的描述符已经关闭。调用方此后不应再盲目调用os.close尝试重复清理;资源所有权应在包装前就约定清楚。
代码在检测关闭的期间没有另外打开文件,避免旧整数恰好被系统分配给新描述符而混淆结果。描述符只是进程里的整数编号,不是永远指向同一文件的身份标识。真实程序也不应保存已关闭编号,之后再根据数值相同推断资源仍然原样存在。
文件名入口不能借用一个尚不存在的描述符
最后open接收的是路径,却同时指定closefd=False,因此抛ValueError。这个选项的借用语义要求传入已有描述符;通过路径打开的新描述符需要由创建出来的文件对象负责关闭,不能用同一参数让它悄悄失去清理责任。
设计函数时可以明确区分“接收并借用现有描述符”和“接收路径并创建资源”两个入口。前者在文档中写清调用者负责最终关闭,后者用上下文管理器收尾。关闭状态、内容正确与数据持久化是不同问题;本例验证前两项,不把成功关闭当作断电后一定保存的证明。
参考资料
资料核验日期:2026年10月2日。以上输出对应文中固定输入和明确运行版本,本地执行退出码为0。


