Python文件对象、关闭和析构函数

6

tempfile.NamedTemporaryFile()的描述如下:

如果delete为true(默认值),则文件在关闭时立即被删除。

在某些情况下,这意味着文件在Python解释器结束后仍未被删除。例如,在py.test下运行以下测试时,临时文件仍然存在:

from __future__ import division, print_function, absolute_import
import tempfile
import unittest2 as unittest
class cache_tests(unittest.TestCase):
    def setUp(self):
        self.dbfile = tempfile.NamedTemporaryFile()
    def test_get(self):
        self.assertEqual('foo', 'foo')

在某种程度上这是有道理的,因为这个程序从来没有显式地关闭文件对象。对象关闭的另一种方式可能在 __del__ 析构函数中,但是在这里,语言参考文献指出:“不能保证解释器退出时仍存在的对象会调用__del__()方法。”所以到目前为止所有内容都符合文档要求。
然而,我对此的影响感到困惑。如果不能保证文件对象在解释器退出时被关闭,那么即使程序正常退出,因为数据仍在文件对象的缓冲区中,并且文件对象永远没有被关闭,那么一些成功写入的数据是否会丢失呢?
在我看来,这似乎非常不可能和不符合 Python 的编码规范,而且 open() 文档也没有包含任何这样的警告。因此我 (暂时) 得出结论,文件对象是被保证关闭的。
但是这个魔法是如何发生的,为什么 NamedTemporaryFile() 不能使用相同的方法来确保文件被删除呢?
请注意,我不是在谈论操作系统缓冲并在程序退出时由操作系统关闭的文件描述符,而是关于实现自己的缓冲区的 Python 文件对象。

这篇帖子似乎包含了很多假设和争论,而实际问题很少。 - Lennart Regebro
3个回答

15
在Windows上,NamedTemporaryFile使用一个Windows特定的扩展(os.O_TEMPORARY)来确保在关闭文件时删除文件。这也可能适用于进程以任何方式终止的情况。然而,在POSIX上没有明显的等效物,很可能是因为在POSIX上,您可以直接删除仍在使用的文件。它只会删除名称,文件的内容只有在关闭后(以任何方式)才会被删除。但是如果我们想要文件名一直存在直到文件被关闭,就像NamedTemporaryFile一样,那么我们需要“魔法”。
我们不能使用与刷新缓冲文件相同的魔法。那里发生的是C库处理它(在Python 2中):文件是C中的FILE对象,C保证它们在正常程序退出时被刷新(但如果进程被杀死,则不会)。在Python 3的情况下,有自定义的C代码实现了相同的效果。但它是特定于此用例的,不能直接重用。
这就是为什么NamedTemporaryFile使用自定义的__del__的原因。事实上,当解释器退出时,__del__不能保证被调用。(我们可以证明它,即创建一个全局循环引用,该引用还引用了一个NamedTemporaryFile实例;或者运行PyPy而不是CPython。)
作为副说明,NamedTemporaryFile可以更加稳健地实现,例如通过使用atexit向其自身注册来确保在那时删除文件名。但是您也可以自己调用它:如果您的进程不使用无限数量的NamedTemporaryFiles,那么它就是简单的 atexit.register(my_named_temporary_file.close)。

对于Python 3的情况:在3.0中,io.BufferedWriter是用纯Python实现的,在Python 3.1及更高版本中,仍然可以使用纯Python实现_pyio。这些模块如何使用“自定义C代码”? - Nikratio
我不太了解Python 3的细节,但我知道_io模块至少在当前的Python 3.x中是用C编写的(Python 3.0现在已经过时)。如果以前有一个纯Python版本,那么它可能使用atexit;如果它使用__del__,那么它将遭受与此问题相同的问题。 - Armin Rigo
@ArminRigo: 你在谈论的是 io,它是一个 C 模块。_pyioio 的功能等效、速度较慢的纯 Python 版本,也存在于 Python 3.3 中。而且它也不使用 atexit。嗯,所以也许 _pyio 的缓冲区实际上可能会丢失... - Nikratio
@ArminRigo:你有 bug 编号或测试脚本吗?你的 bpaste 链接现在是 404。 - Nikratio

1
在任何版本的*nix上,当进程结束时所有文件描述符都会关闭,这是由操作系统处理的。Windows 在这方面可能完全相同。如果不深入源代码,我不能百分之百地确定实际发生了什么,但很可能发生以下情况:
- 如果 `delete` 是 `False`,则调用 `unlink()`(或其他操作系统上类似的函数)。这意味着当进程退出且没有更多打开的文件描述符时,文件将自动被删除。在进程运行时,文件仍然存在。 - 如果 `delete` 是 `True`,很可能使用 C 函数 `remove()`。这将在进程退出之前强制删除文件。

2
你在这里非常接近正确。处理匿名临时文件的正常方法是打开文件,立即取消链接,然后继续使用文件描述符。正常的Unix文件系统只有在链接计数为零且没有保留指向其的打开文件描述符时才会回收inode。 - Jim Dennis
@JimDennis 没错,谢谢,这是更准确的描述发生的事情。 - Yuushi
@Yuushi:恐怕这里有几个错误。1)如果delete为false,则文件不会在程序退出后自动删除,而是保留下来。2)C的remove函数只调用unlink。没有所谓的“强制删除”。3)我正在谈论Python文件对象,而不是文件描述符。虽然文件对象通常具有底层描述符,但我担心在数据写入描述符之前文件对象内部发生缓冲。 - Nikratio

-1
文件缓冲由操作系统处理。如果您在打开文件后没有关闭它,那是因为您假定操作系统会在所有者退出后刷新缓冲区并关闭文件。这不是Python的魔法,而是您的操作系统在执行其任务。 __del__() 方法与Python相关,并需要显式调用。

那不正确。有几层缓冲,操作系统层只是其中之一。除非我误解了http://docs.python.org/3.4/library/io.html#io.BufferedIOBase,文件对象也可以在Python层面上进行缓冲。 - Nikratio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接