为什么在 Python 中关闭文件很重要?

753 阅读14分钟

Why Is It Important to Close Files in Python?

为什么在Python中关闭文件很重要?

目录

在你的 Python 编码之旅的某个阶段,你学会了应该使用上下文管理器来打开文件。Python 上下文管理器使得在你完成了对文件的处理后,可以很容易地关闭它们。

with open("hello.txt", mode="w") as file:
    file.write("Hello, World!")

with 语句启动了一个上下文管理器。在这个例子中,只要上下文处于活动状态,上下文管理器就会打开文件hello.txt管理文件资源。一般来说,缩进块中的所有代码都依赖于正在打开的文件对象。一旦缩进块结束或引发一个异常,那么文件就会关闭。

如果你没有使用上下文管理器,或者你在用不同的语言工作,那么你可以用try...finally 的方法明确地关闭文件。

try:
    file = open("hello.txt", mode="w")
    file.write("Hello, World!")
finally:
    file.close()

关闭文件的finally 块无条件地运行,无论try 块是否成功。虽然这种语法有效地关闭了文件,但 Python 上下文管理器提供了更少的言语和更直观的语法。此外,它比简单地用try...finally 来包装你的代码更灵活一些。

你可能已经使用上下文管理器来管理文件了,但是你有没有想过,为什么大多数教程和五分之四的牙医都建议这样做?简而言之,为什么在 Python 中关闭文件是很重要的?

在本教程中,你将深入研究这个问题。首先,你将了解到文件句柄是一种有限的资源。然后,你将试验一下不关闭文件的后果

免费下载。 获取CPython内部的一个样本章节。你的Python 3 解释器指南》中的一章,展示了如何解开Python语言的内部工作,从源代码中编译Python解释器,并参与CPython的开发。

简而言之:文件是受操作系统限制的资源

Python 将文件操作委托给 操作系统.作业系统是诸如Python之类的程序与所有系统资源之间的中介。 进程诸如Python,和所有的系统资源,如硬盘、内存和CPU时间。

当你用open() 打开一个文件时,你向操作系统发出了一个 系统调用给操作系统,让它在硬盘上找到该文件,并为读写做准备。然后操作系统将返回一个无符号的整数,在Windows上称为文件句柄,在类似UNIX的系统上称为文件描述符,包括Linux和macOS。

Illustration of Python making a system call for a file handle

一个Python进程进行系统调用,得到整数10作为文件句柄

一旦有了与文件相关的编号,就可以进行读或写操作了。每当 Python 想读、写或关闭文件时,它就会进行另一个系统调用,提供文件句柄号。Python 文件对象有一个.fileno() 方法,你可以用它来寻找文件柄。

>>>

>>> with open("test_file.txt", mode="w") as file:
...     file.fileno()
...
4

在打开的文件对象上的.fileno() 方法将返回操作系统作为文件描述符使用的整数。就像你可能使用一个 ID 字段从数据库中获得一条记录一样,Python 在每次从文件中读或写时都向操作系统提供这个数字。

操作系统限制了任何一个进程可以拥有的开放文件的数量。这个数字通常是几千。操作系统设置这个限制是因为如果一个进程试图打开成千上万的文件描述符,那么这个进程可能出了问题。尽管数以千计的文件看起来很多,但仍有可能碰到这个限制。

除了跑进极限的风险外,保持文件的开放使你容易丢失数据。一般来说,Python和操作系统努力工作,保护你不丢失数据。但是如果你的程序或计算机崩溃了,通常的程序可能不会发生,而打开的文件可能被破坏。

注意:有些库有特定的方法和函数,似乎可以在没有上下文管理器的情况下打开文件。例如,pathlib库有 .write_text(),而pandas有 read_csv().

不过它们确实在引擎盖下正确地管理资源,所以在这些情况下你不需要使用上下文管理器。最好是参考你所使用的库的文档,看看你是否需要一个上下文管理器。

简而言之,让上下文管理器管理你的文件是一种防御性的技术,很容易实践,而且能使你的代码变得更好--所以你不妨这样做。这就像系上安全带。你可能不需要它,但不这样做的代价可能很高。

在本教程的其余部分,你将深入了解不关闭文件的限制、后果和危险。在下一节中,你将探索Too many open files 错误。

当你打开太多的文件时,会发生什么?

在本节中,你将探索当你遇到文件限制时会发生什么。你将通过尝试一个代码段来完成,该代码段将创建一个打开文件的负载并引发一个OSError

注意:正如OSError 中的OS 所示,这个限制是由操作系统而不是由 Python 强制执行的。然而,从理论上讲,操作系统可以处理更多的文件描述符。稍后,你会了解到更多关于操作系统限制文件句柄的原因

你可以通过尝试一下子打开成千上万的文件来测试操作系统上每个进程的文件限制。你将把文件对象存储在一个列表中,这样它们就不会被自动清理掉。但首先,你要做一些内务管理,以确保你不会在你不需要的地方创建大量的文件。

$ mkdir file_experiment
$ cd file_experiment

创建一个可以转储文件的文件夹,然后导航到那个文件夹就足够了。然后,你可以打开一个Python REPL并尝试制作成千上万的文件。

>>>

>>> files = [open(f"file-{n}.txt", mode="w") for n in range(10_000)]
Traceback (most recent call last):
    ...
OSError: [Errno 24] Too many open files: 'file-1021.txt'

这个片段试图打开一万个文件并将它们保存在一个列表中。操作系统开始创建文件,但一旦达到极限就会推倒重来。如果你列出你新创建的目录中的文件,你会注意到,即使列表理解最终失败,操作系统还是创建了许多文件--只是没有达到你要求的一万个。

你所遇到的限制在不同的操作系统中会有所不同,在Windows中似乎默认较大。根据操作系统的不同,有一些方法可以提高这个每进程文件的限制。然而,你应该问问自己,你是否真的需要这样做。选择这种解决方案的合法用例只有少数。

一个合法的情况是用于服务器。服务器使用套接字工作,它的处理方式很像文件。操作系统在文件表中使用文件柄跟踪套接字。一个服务器可能需要为他们连接的每个客户打开许多套接字。此外,一个服务器可能会与几个客户进行通信。这种情况可能会导致需要成千上万的文件句柄。

有趣的是,尽管某些应用程序可能要求提高操作系统对开放文件的限制,但通常正是这些应用程序需要特别勤奋地关闭文件

也许你认为你没有任何直接的危险,不会碰到限制。即便如此,请继续阅读,因为在下一节中,你将仔细看看不小心闯入该限制的一些后果。

闯入文件限制的现实生活中的后果是什么?

如果你在 Python 中打开文件并且从不关闭它们,你可能不会注意到任何区别,特别是当你在做一个文件的脚本或小项目时。然而,随着你工作的项目越来越复杂,你会越来越多地接触到有问题的情况。

想象一下,你在一个大的团队中工作,从事一个庞大的代码库。然后,有一天你达到了开放文件的极限。最重要的是,限制的错误信息不会告诉你问题出在哪里。它将是一个通用的 OSError你之前看到的那个通用的错误信息,它只告诉你Too many open files

在你的代码库中,你可能有成千上万个打开文件的地方。想象一下,寻找代码中不能正确处理文件的地方。想象一下,代码在函数之间传递文件对象,而你不能立即知道任何给定的文件对象最终是否被关闭。那就不好玩了。

如果你有兴趣,有一些方法可以探索你的系统的开放文件柄。展开下面的块来探索。

探索文件句柄的实用程序显示/隐藏

安装process hacker

PS> choco install processhacker

打开应用程序,点击查找句柄或DLLs按钮。勾选regex复选框,然后输入.* ,就可以看到所有的文件句柄和附带的信息。

微软官方版本的进程黑客是Sysinternals工具的一部分,即进程监控器进程浏览器

你可能需要安装 lsof,它是一个列出 开放 文件的Linux工具。通过这个工具,你可以获得信息并计算有多少个打开的文件。

$ lsof | head
$ lsof | wc -l

lsof 命令为每个打开的文件打印一个新行,并附有该文件的基本信息。用管道将其输入到head 命令中,将显示输出的开始,包括列名。

lsof 的输出可以通过管道进入wc ,即字数计算命令。-l 开关意味着它将只计算新行。这个数字可能是几十万。

你可以用管道将lsof 的输出输入到grep ,以找到包含一个字符串的行,如python 。你还可以传入一个进程ID,如果你想寻找文件描述符,这可能会有帮助。

$ lsof | grep python

这个命令将过滤掉所有不包含grep 后面的术语的行,在这里是python

如果你对系统中文件的理论极限感到好奇,你可以在基于UNIX的系统中通过研究一个特殊文件的内容来探索这个问题。

$ cat /proc/sys/fs/file-max

这个数字与平台有关,但它很可能是巨大的。在达到这个极限之前,系统几乎肯定会耗尽其他资源。

你可能想知道为什么操作系统会限制文件。据推测,它能处理的文件句柄比它所允许的要多得多,对吗?在下一节中,你会发现为什么操作系统会关心这个问题。

为什么操作系统会限制文件处理量?

一个操作系统可以同时打开的文件数量的实际限制是巨大的。你说的是数以百万计的文件。但实际上达到这个限制并给它加上一个固定的数字并不明确。通常情况下,一个系统会在文件处理量耗尽之前耗尽其他资源。

从操作系统的角度来看,这个限制是保守的,但从大多数程序的角度来看是充足的。从操作系统的角度来看,任何达到极限的进程都可能是在泄露文件句柄和其他资源。

资源的泄漏可能是由于不良的编程实践或试图攻击系统的恶意程序造成的。这就是为什么操作系统规定了限制,以保证你的安全,不受他人和自己的影响

另外,对于大多数应用程序来说,打开这么多文件是没有意义的。一个硬盘上不能同时发生超过一个读或写的操作,所以如果你只是处理文件,并不会使事情变得更快。

好了,你知道打开大量的文件是有问题的,但是在Python中不关闭文件还有其他的坏处,即使你只打开了少数几个。

如果你不关闭文件,Python 崩溃了会发生什么?

在这一节中,你将尝试模拟崩溃,看看它对打开的文件有什么影响。你可以使用os 模块中的一个特殊函数,它将在不执行任何 Python 通常做的清理工作的情况下退出,但首先,你将看到事情通常是如何清理的。

为每个命令执行写操作可能很昂贵。出于这个原因,Python 的默认做法是使用一个收集写操作的缓冲区。当缓冲区满了,或者文件被明确关闭时,缓冲区被刷新,写操作就完成了。

Python 努力地清理自己的工作。在大多数情况下,它自己会主动地刷新和关闭文件。

# write_hello.py

file = open("hello.txt", mode="w")
file.write("Hello, world!")

当运行这段代码时,操作系统会创建文件。尽管你在代码中从未真正冲洗或关闭过文件,但操作系统也会写下内容。这种冲刷和关闭是由Python在执行结束时执行的清理程序来处理的。

但有时退出并不是那么好控制的,崩溃最终会绕过这个清理程序。

# crash_hello.py

import os

file = open("crash.txt", mode="w")
file.write("Hello, world!")
os._exit(1)

在运行上面的片段之后,你可以使用cat 来检查你刚刚创建的文件的内容。

$ cat crash.txt
$ # No output!

你会看到,尽管操作系统已经创建了这个文件,但它并没有任何内容。没有输出的原因是 os._exit()绕过了通常的 Python 退出程序,模拟了崩溃。也就是说,即使是这种类型的模拟也是相对可控的,因为它假设的是 Python,而不是你的操作系统崩溃了。

在幕后,一旦Python完成了,操作系统也会进行自己的清理,关闭进程所打开的所有文件描述符。崩溃可能发生在很多层面,并干扰操作系统的清理工作,使文件句柄悬空。

例如,在Windows上,悬空的文件句柄可能是有问题的,因为任何打开一个文件的进程都会锁定它。另一个进程不能打开该文件,直到它被关闭。Windows用户可能熟悉那些不让你打开或删除文件的流氓进程。

有什么比被锁在文件外面更糟糕的呢?泄漏的文件柄会带来安全风险,因为与文件相关的权限有时会被混淆。

注意:Python 最常见的实现,CPython,在清理悬空的文件柄方面比你想象的更进一步。它使用引用计数进行垃圾回收,所以一旦文件不再被引用就会被关闭。也就是说,其他的实现,比如PyPy,使用不同的策略,在清理未使用的文件句柄方面可能没有那么积极。

一些实现可能不会像CPython那样有效地清理文件句柄,这也是始终使用上下文管理程序的另一个论据。

文件句柄被泄露和内容在缓冲区中丢失已经很糟糕了,但中断文件操作的崩溃也可能导致文件损坏。这大大增加了数据丢失的可能性。同样,这些都是不太可能发生的情况,但它们可能是昂贵的。

你不可能完全避免崩溃,但你可以通过使用上下文管理器来减少你的风险。上下文管理器的语法将自然而然地引导你以一种只在需要时保持文件打开的方式进行编码。

总结

你已经学会了为什么在 Python 中关闭文件是很重要的。因为文件是由操作系统管理的有限资源,确保文件在使用后被关闭,可以防止出现难以调试的问题,如文件句柄耗尽或数据被破坏。最好的防御措施是用上下文管理器打开文件。

从表面上看,你已经看到了当你打开太多的文件时会发生什么,你已经激起了崩溃,导致文件的内容丢失。要了解更多关于打开文件的信息,请看在 Python 中读和写文件。关于上下文管理的深入指导,请看上下文管理和 Python 的with 声明