有必要关闭没有被引用的文件吗?

54

作为一个完全的编程新手,我正在尝试理解打开和关闭文件的基本概念。我正在做一个练习,创建一个脚本,使我能够将一个文件的内容复制到另一个文件中。

in_file = open(from_file)
indata = in_file.read()

out_file = open(to_file, 'w')
out_file.write(indata)

out_file.close()
in_file.close()

我尝试缩短这段代码,并得出了这个结果:

indata = open(from_file).read()
open(to_file, 'w').write(indata)

这种方法看起来对我更有效率。然而,我也感到困惑。我认为我忘了引用已打开的文件;in_file和out_file变量是不必要的。但是,这会让我拥有两个已经打开但没有被引用的文件吗?我该如何关闭它们,还是说不需要关闭?

如果您能提供一些关于此主题的帮助,我将非常感激。


3
附注:您正在进行的操作可能会导致大型输入文件的内存问题;我建议您查看 shutil.copyfileobj 和相关方法来执行此操作(这些方法明确地按块复制,因此峰值内存消耗是固定的,不取决于输入文件的大小)。 - ShadowRanger
3
这个问题在https://dev59.com/bWs05IYBdhLWcg3wLe8j有很好的讨论。 - snakecharmerb
10
“看起来更加高效”是什么意思?如果你的意思是源代码更短,那肯定是真的。但在Python中,通常不会将让代码尽可能变短视为首要任务,更重要的是可读性。如果你的意思是代码执行更高效,我认为这可能不会有足够大的差异来影响程序,不过确认这一点,你始终应该进行测量。为什么不会有太大的差别呢?因为创建和分配变量很便宜,对于Python而言,所有赋值都相当于设置一个指针。 - John Y
@JohnY - 这是一个非常好的问题。我认为我有一个假设,即“更短”或“涉及较少的中间步骤”或“较少的变量”是好的。你提出了一个很好的观点,实际上可读性更重要(特别是在比我的简单练习更多代码的脚本中)。当你说分配变量很便宜时,我认为这意味着它不会占用太多内存? - Roy Crielaard
2
@RoyCrielaard确实 - "变量"只是对已经存在于内存中的数据的引用,它的大小等同于内存指针。赋值在大小和速度方面都很便宜。请注意,即使在您的第一个示例中,您也可能会遇到文件句柄泄漏问题 - 如果写入崩溃,例如,将不会关闭任何内容。 - Boris the Spider
当代码块被执行数百万次时,或者“步骤”非常多(比如读取整个文件),“中间步骤较少”变得非常重要。单个赋值是琐碎的;事实上,通过将非局部函数或引用分配给局部变量,你经常可以使程序运行速度更快。可读性确实总是更重要的。 - alexis
6个回答

59

处理这种情况的Pythonic方式是使用with上下文管理器

with open(from_file) as in_file, open(to_file, 'w') as out_file:
    indata = in_file.read()
    out_file.write(indata)

使用with与这样的文件一起使用,即使read()write()发生错误,它也会确保为您完成所有必要的清理工作。


25
注意:在CPython参考解释器中,打开并立即读/写返回的文件对象(没有存储打开的文件对象的引用)是可预测且安全的,但大多数其他解释器不是这样,并且这不是语言的合同保证(只是CPython引用计数的副作用)。使用“with”更具可扩展性、可移植性和可预测性,因此始终使用它,即使似乎在没有使用它的情况下也能正常工作。 - ShadowRanger
你是说在其他实现中open(foo).read()不可靠吗?这太可怕了,哪些实现会这样? - Russell Borogove
8
@RussellBorogove:大概所有的主要实现都是这样的;我不知道有没有非CPython实现会进行引用计数。快速搜索显示官方PyPy文档警告说open("filename", "w").write("stuff")只有在GC运行收集后才会写入文件,这是一个非常相似的情况。(如果你所说的“不可靠”是指“可能崩溃”或“可能给出错误结果”,而不是“可能泄漏文件句柄”,那么open(foo).read()并不比with open(foo) as f: contents = f.read()更容易出现这种情况。) - user2357112
3
在执行read()时,open(foo).read()应该是“安全的”,因为文件对象在执行read()期间仍将被引用。但是,当您有多个文件操作时,没有保证关闭将在何时(或以何种顺序)发生,只是它将在“文件对象不再被引用之后的某个时间”发生。这意味着写入可能会被任意延迟(或者在程序退出时仍有对象被引用且它们的终结器未运行时可能会丢失,即使CPython也不能完全保证:https://docs.python.org/2/reference/datamodel.html#object.__del__)。 - Ben
1
@jared 在2.5中,你只需要使用__future__导入。2.6具有单个上下文的with语句,2.7从版本3.1中回溯了多个上下文,如上所示。在2.5之前,你就没办法了。 - RoadieRich
显示剩余5条评论

42

默认的Python解释器CPython使用引用计数。这意味着一旦没有对一个对象的引用,它就会被垃圾回收,即清理。

在您的情况下,执行以下操作:

open(to_file, 'w').write(indata)

将创建一个文件对象以供使用to_file,但不会给它命名 - 这意味着没有对它的引用。此行之后,您无法操纵该对象。

CPython将检测到这一点,并在使用后清理对象。对于文件,这意味着自动关闭它。原则上,这是可以的,您的程序不会泄漏内存。

“问题”在于,这种机制是CPython解释器的实现细节。语言标准明确表示不提供对此的任何保证!如果您使用像pypy这样的替代解释器,则文件的自动关闭可能会被无限期地延迟。无限期地包括其他隐式操作,如在关闭时刷新写入。

此问题也适用于其他资源,例如网络套接字。最佳做法是始终明确处理此类外部资源。自Python 2.6以来,with语句使此过程变得简洁:

with open(to_file, 'w') as out_file:
    out_file.write(in_data)

简而言之:它可以工作,但请不要这样做。


6
不仅是“关闭”——还要“写入”!只有在文件关闭之前缓冲区才会被刷新... - Boris the Spider
谢谢,我已经把它包含在答案中了。 - MisterMiyagi
除非文件使用了O_DIRECT标志打开 - 这种情况比较少见。 - edmz
@black:O_DIRECT 不提供任何保证,请参见 open(2)。您需要使用 O_SYNC/O_DSYNC - Kevin

38

你询问了关于"基本概念"的问题,那么让我们从头开始:当您打开文件时,程序可以访问系统资源,也就是程序自身内存空间之外的一些东西。这基本上是操作系统提供的一种魔法(在Unix术语中称为系统调用)。文件对象中隐藏着一个对"文件描述符"的引用,它是与打开文件相关联的实际操作系统资源。关闭文件会告诉系统释放这个资源。

作为操作系统资源,进程可以打开的文件数是有限制的:很久以前Unix每个进程的限制大约是20个。现在我的OS X电脑强制限制为256个打开的文件(虽然这是一个强制限制,但可以增加)。其他系统可能会设置几千个或者在这种情况下达到数万个的限制(每个用户限制,而不是每个进程限制)。当程序结束时,所有资源都会自动释放。所以如果您的程序只打开了一些文件,对它们进行了一些操作并退出,那么您可以随便处理,您将不会感受到任何不同。但是,如果您的程序将打开数千个文件,则最好释放打开的文件以避免超过操作系统的限制。

关闭进程退出前的文件还有另一个好处:如果您打开了一个写入文件,关闭文件将首先"刷新其输出缓冲区"。这意味着i/o库通过收集("缓冲")写出的内容,批量保存到磁盘中来优化磁盘的使用。如果您写入文本文件并尝试立即重新打开并读取它,而没有先关闭输出句柄,那么您会发现并没有把所有东西都写出来。此外,如果您的程序被过于突然地关闭(使用信号,或者甚至通过正常退出),输出可能永远不会被刷新。

已经有很多其他关于如何释放文件的答案了,因此这里只列出一些方法的简要列表:

  1. 使用close()显式关闭。(对于Python新手的注意事项:别忘了括号!我的学生们喜欢写in_file.close,但这不起作用。)

  2. 推荐:使用with语句打开文件隐式关闭。当with块的末尾被调用时,甚至在异常情况下(从异常中),close()方法也会被调用。

with open("data.txt") as in_file:
    data = in_file.read()
  • 如果你的Python引擎实现了,可以通过引用管理器或垃圾回收器隐式关闭。这不是推荐的方法,因为它不太可移植;详情请参见其他答案。这就是为什么在Python中添加了with语句的原因。

  • 当程序结束时,会自动隐式关闭。如果有文件以输出方式打开,则可能存在程序在所有内容都写入磁盘之前退出的风险。


  • 一个进程可以打开的文件数量是有限制的(很久以前Unix的限制大约是20个)。但是在任何现代的类Unix系统上,这个限制已经非常高了,在达到限制之前,系统会先耗尽内存或CPU时间,所以我几乎不认为这是一个问题。 - cat
    谢谢... 我快速查找了一下,但没有找到任何确切数字,所以我只能从记忆中入手。现在做了一些研究。(虽然“硬”限制可能是天文数字或无限的,但通常数字会受到 ulimit 等因素的限制。) - alexis
    Linux在过去几十年中的默认最大文件描述符数量,以及今天大多数Linux发行版的默认值,是1024。您可以使用ulimit -n命令来检查它。这个值是可以更改的。 - nh2

    7

    在处理文件对象时,使用with关键字是一个好的习惯。这样做的好处是,即使在执行过程中出现异常,文件也会在其代码块完成后被正确关闭。同时,相比于编写等效的try-finally代码块,使用with要更加简洁。

    >>> with open('workfile', 'r') as f:
    ...     read_data = f.read()
    >>> f.closed
    True
    

    7
    到目前为止,回答都是绝对正确的,特别是在使用Python时。你应该使用“with open()”上下文管理器。这是一个很棒的内置功能,有助于快捷地完成常见的编程任务(打开和关闭文件)。
    然而,由于你是初学者,在整个职业生涯中可能无法访问上下文管理器自动引用计数,我将从一般编程角度回答这个问题。
    你的代码的第一个版本完全没问题。你打开一个文件,保存引用,从文件读取,然后关闭它。当语言没有提供任务的快捷方式时,这就是很多代码的写法。唯一需要改进的是将“close()”移到打开和读取文件的位置。一旦你打开并读取了文件,就将内容保存在内存中,不再需要打开文件。
    in_file = open(from_file)
    indata = in_file.read()
    out_file.close() 
    
    out_file = open(to_file, 'w')
    out_file.write(indata)
    in_file.close()
    

    5

    一种安全的打开文件的方法,而无需担心您没有关闭它们,就像这样:

    with open(from_file, 'r') as in_file:
        in_data = in_file.read()
    
    with open(to_file, 'w') as out_file:
        outfile.write(in_data)
    

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