Python 标准库中文件对象未正确关闭后如何清理(异常后)
How to clean up after an improperly closed file object in the Python Standard Library (after an exception)
TL;DR:标准库在引发异常时无法关闭文件。我正在寻找处理这种情况的最佳方法。请随意阅读以 "Upon closer inspection of CPython's source code". 开头的段落,也可以向下滚动到问题的末尾以获取在 Windows.[=46 上重现此问题的独立脚本=]
我正在编写一个 Python 包,其中我使用 STL 的 ConfigParser
(2.x) 或 configparser
(3.x) 来解析用户配置文件(我将两者都称为 ConfigParser
,因为问题主要在于 2.x 实现)。从现在开始,我在 GitHub 上的相关代码行将在适当的时候被链接起来。 ConfigParser.ConfigParser.read(filenames)
(在我的代码中使用 here) raises a ConfigParser.Error
exception when the config file is malformed. I have some code in my test suite targeting this situation, using unittest.TestCase.assertRaises(ConfigParser.Error)
. The malformed config file is properly generated with tempfile.mkstemp
(the returned fd is closed with os.close
before anything) and I attempt to remove the temp file 和 os.remove
。
os.remove
是麻烦开始的地方。我的测试在 Windows 上失败(同时在 OS X 和 Ubuntu 上工作),Python 2.7(参见 this AppVeyor build):
Traceback (most recent call last):
File "C:\projects\storyboard\tests\test_util.py", line 235, in test_option_reader
os.remove(malformed_conf_file)
WindowsError: [Error 32] The process cannot access the file because it is being used by another process: 'c:\users\appveyor\appdata\local\temp\1\storyboard-test-3clysg.conf'
注意,正如我上面所说,malformed_conf_file
是用tempfile.mkstemp
生成的,并立即用os.close
关闭,所以它唯一一次打开是在我调用[=29=时] here inside the unittest.TestCase.assertRaises(ConfigParser.Error)
context。所以罪魁祸首似乎是 STL 而不是我自己的代码。
在仔细检查 CPython 的源代码后,我发现 ConfigParser.ConfigPaser.read
在引发异常时确实没有正确关闭文件. 2.7 (here on CPython's Mercurial) 中的 read
方法具有以下几行:
for filename in filenames:
try:
fp = open(filename)
except IOError:
continue
self._read(fp, filename)
fp.close()
read_ok.append(filename)
self._read(fp, filename)
引发异常(如果有的话),但如您所见,如果 self._read
引发,则 fp
不会关闭,因为 fp.close()
仅在 self._read
returns.
之后调用
同时,来自 3.4 (here) 的 read
方法不会遇到同样的问题,因为这次他们在上下文中正确地嵌入了文件处理:
for filename in filenames:
try:
with open(filename, encoding=encoding) as fp:
self._read(fp, filename)
except OSError:
continue
read_ok.append(filename)
所以我认为问题很明显是 2.7 的 STL 中的一个缺陷。 处理这种情况的最佳方法是什么? 具体来说:
- 我可以做些什么来关闭该文件吗?
- 是否值得向bugs.python.org报告?
现在我想我会在 os.remove
中添加一个 try .. except OSError ..
(有什么建议吗?)。
更新:可用于在 Windows 重现此问题的独立脚本:
#!/usr/bin/env python2.7
import ConfigParser
import os
import tempfile
def main():
fd, path = tempfile.mkstemp()
os.close(fd)
with open(path, 'w') as f:
f.write("malformed\n")
config = ConfigParser.ConfigParser()
try:
config.read(path)
except ConfigParser.Error:
pass
os.remove(path)
if __name__ == '__main__':
main()
当我 运行 它与 Python 2.7 解释器时:
Traceback (most recent call last):
File ".\example.py", line 19, in <module>
main()
File ".\example.py", line 16, in main
os.remove(path)
WindowsError: [Error 32] The process cannot access the file because it is being used by another process: 'c:\users\redacted\appdata\local\temp\tmp07yq2v'
这是一个有趣的问题。正如 Lukas Graf 在评论中指出的那样,问题似乎是异常回溯对象持有对引发异常的调用框架的引用。这个调用帧包括当时存在的局部变量,其中之一是对打开文件的引用。因此该文件对象仍然有对它的引用并且没有正确关闭。
对于您的独立示例,只需删除 try/except ConfigParser.Error
"works":有关格式错误的配置文件的异常未被捕获并停止程序。但是,在您的实际应用程序中,assertRaises
正在捕获异常以检查它是否是您要测试的异常。我不是 100% 确定为什么即使在 with assertRaises
块之后回溯仍然存在,但显然它确实存在。
对于您的示例,另一个更有希望的解决方法是将 except
子句中的 pass
更改为 sys.exc_clear()
:
try:
config.read(path)
except ConfigParser.Error:
sys.exc_clear()
这将摆脱讨厌的回溯对象并允许关闭文件。
然而,在您的实际应用程序中具体如何做到这一点并不清楚,因为有问题的 except
子句在 unittest
中。我认为最简单的事情可能是不直接使用 assertRaises
。相反,编写一个辅助函数来执行您的测试,检查您想要的异常,使用 sys.exc_clear()
技巧进行清理,然后引发另一个自定义异常。然后在 assertRaises
中包装对该辅助方法的调用。这样您就可以控制 ConfigParser 引发的有问题的异常并可以正确清理它(unittest
没有这样做)。
这是我的意思的草图:
# in your test method
assertRaises(CleanedUpConfigError, helperMethod, conf_file, malformed_conf_file)
# helper method that you add to your test case class
def helperMethod(self, conf_file, malformed_conf_file):
gotRightError = False
try:
or6 = OptionReader(
config_files=[conf_file, malformed_conf_file],
section='sec',
)
except ConfigParser.Error:
gotRightError = True
sys.exc_clear()
if gotRightError:
raise CleanedUpConfigError("Config error was raised and cleaned up!")
当然,我还没有实际测试过这个,因为我没有用你的代码设置整个单元测试。你可能需要稍微调整一下。 (想想看,如果你这样做,你甚至可能不需要 exc_clear()
,因为由于异常处理程序现在在一个单独的函数中,回溯应该在 helperMethod
退出时被正确清除。)但是,我认为这个想法可能会让你到达某个地方。基本上你需要确保捕获这个特定 ConfigParser.Error
的 except
子句是由你编写的,以便在你尝试删除你的测试文件之前可以清理它。
附录:似乎如果上下文管理器处理异常,回溯实际上会被存储到包含 with
块的函数结束,如本例所示:
class CM(object):
def __init__(self):
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
return True
def foo():
with CM():
raise ValueError
print(sys.exc_info())
即使 with
块在 print
发生时已经结束,所以异常处理应该完成,sys.exc_info
仍然 returns 异常信息就好像有一个活跃的例外。这也是您的代码中发生的情况:with assertRaises
块导致回溯一直持续到该函数的末尾,从而干扰您的 os.remove
。这似乎是错误的行为,我注意到它在 Python 3 中不再以这种方式工作(print
打印 (None, None None)
),所以我想这是一个用 [ 修复的疣=61=] 3.
基于此,我怀疑在 os.remove
之前(在 with assertRaises
块之后)插入一个 sys.exc_clear()
可能就足够了。
TL;DR:标准库在引发异常时无法关闭文件。我正在寻找处理这种情况的最佳方法。请随意阅读以 "Upon closer inspection of CPython's source code". 开头的段落,也可以向下滚动到问题的末尾以获取在 Windows.[=46 上重现此问题的独立脚本=]
我正在编写一个 Python 包,其中我使用 STL 的 ConfigParser
(2.x) 或 configparser
(3.x) 来解析用户配置文件(我将两者都称为 ConfigParser
,因为问题主要在于 2.x 实现)。从现在开始,我在 GitHub 上的相关代码行将在适当的时候被链接起来。 ConfigParser.ConfigParser.read(filenames)
(在我的代码中使用 here) raises a ConfigParser.Error
exception when the config file is malformed. I have some code in my test suite targeting this situation, using unittest.TestCase.assertRaises(ConfigParser.Error)
. The malformed config file is properly generated with tempfile.mkstemp
(the returned fd is closed with os.close
before anything) and I attempt to remove the temp file 和 os.remove
。
os.remove
是麻烦开始的地方。我的测试在 Windows 上失败(同时在 OS X 和 Ubuntu 上工作),Python 2.7(参见 this AppVeyor build):
Traceback (most recent call last):
File "C:\projects\storyboard\tests\test_util.py", line 235, in test_option_reader
os.remove(malformed_conf_file)
WindowsError: [Error 32] The process cannot access the file because it is being used by another process: 'c:\users\appveyor\appdata\local\temp\1\storyboard-test-3clysg.conf'
注意,正如我上面所说,malformed_conf_file
是用tempfile.mkstemp
生成的,并立即用os.close
关闭,所以它唯一一次打开是在我调用[=29=时] here inside the unittest.TestCase.assertRaises(ConfigParser.Error)
context。所以罪魁祸首似乎是 STL 而不是我自己的代码。
在仔细检查 CPython 的源代码后,我发现 ConfigParser.ConfigPaser.read
在引发异常时确实没有正确关闭文件. 2.7 (here on CPython's Mercurial) 中的 read
方法具有以下几行:
for filename in filenames:
try:
fp = open(filename)
except IOError:
continue
self._read(fp, filename)
fp.close()
read_ok.append(filename)
self._read(fp, filename)
引发异常(如果有的话),但如您所见,如果 self._read
引发,则 fp
不会关闭,因为 fp.close()
仅在 self._read
returns.
同时,来自 3.4 (here) 的 read
方法不会遇到同样的问题,因为这次他们在上下文中正确地嵌入了文件处理:
for filename in filenames:
try:
with open(filename, encoding=encoding) as fp:
self._read(fp, filename)
except OSError:
continue
read_ok.append(filename)
所以我认为问题很明显是 2.7 的 STL 中的一个缺陷。 处理这种情况的最佳方法是什么? 具体来说:
- 我可以做些什么来关闭该文件吗?
- 是否值得向bugs.python.org报告?
现在我想我会在 os.remove
中添加一个 try .. except OSError ..
(有什么建议吗?)。
更新:可用于在 Windows 重现此问题的独立脚本:
#!/usr/bin/env python2.7
import ConfigParser
import os
import tempfile
def main():
fd, path = tempfile.mkstemp()
os.close(fd)
with open(path, 'w') as f:
f.write("malformed\n")
config = ConfigParser.ConfigParser()
try:
config.read(path)
except ConfigParser.Error:
pass
os.remove(path)
if __name__ == '__main__':
main()
当我 运行 它与 Python 2.7 解释器时:
Traceback (most recent call last):
File ".\example.py", line 19, in <module>
main()
File ".\example.py", line 16, in main
os.remove(path)
WindowsError: [Error 32] The process cannot access the file because it is being used by another process: 'c:\users\redacted\appdata\local\temp\tmp07yq2v'
这是一个有趣的问题。正如 Lukas Graf 在评论中指出的那样,问题似乎是异常回溯对象持有对引发异常的调用框架的引用。这个调用帧包括当时存在的局部变量,其中之一是对打开文件的引用。因此该文件对象仍然有对它的引用并且没有正确关闭。
对于您的独立示例,只需删除 try/except ConfigParser.Error
"works":有关格式错误的配置文件的异常未被捕获并停止程序。但是,在您的实际应用程序中,assertRaises
正在捕获异常以检查它是否是您要测试的异常。我不是 100% 确定为什么即使在 with assertRaises
块之后回溯仍然存在,但显然它确实存在。
对于您的示例,另一个更有希望的解决方法是将 except
子句中的 pass
更改为 sys.exc_clear()
:
try:
config.read(path)
except ConfigParser.Error:
sys.exc_clear()
这将摆脱讨厌的回溯对象并允许关闭文件。
然而,在您的实际应用程序中具体如何做到这一点并不清楚,因为有问题的 except
子句在 unittest
中。我认为最简单的事情可能是不直接使用 assertRaises
。相反,编写一个辅助函数来执行您的测试,检查您想要的异常,使用 sys.exc_clear()
技巧进行清理,然后引发另一个自定义异常。然后在 assertRaises
中包装对该辅助方法的调用。这样您就可以控制 ConfigParser 引发的有问题的异常并可以正确清理它(unittest
没有这样做)。
这是我的意思的草图:
# in your test method
assertRaises(CleanedUpConfigError, helperMethod, conf_file, malformed_conf_file)
# helper method that you add to your test case class
def helperMethod(self, conf_file, malformed_conf_file):
gotRightError = False
try:
or6 = OptionReader(
config_files=[conf_file, malformed_conf_file],
section='sec',
)
except ConfigParser.Error:
gotRightError = True
sys.exc_clear()
if gotRightError:
raise CleanedUpConfigError("Config error was raised and cleaned up!")
当然,我还没有实际测试过这个,因为我没有用你的代码设置整个单元测试。你可能需要稍微调整一下。 (想想看,如果你这样做,你甚至可能不需要 exc_clear()
,因为由于异常处理程序现在在一个单独的函数中,回溯应该在 helperMethod
退出时被正确清除。)但是,我认为这个想法可能会让你到达某个地方。基本上你需要确保捕获这个特定 ConfigParser.Error
的 except
子句是由你编写的,以便在你尝试删除你的测试文件之前可以清理它。
附录:似乎如果上下文管理器处理异常,回溯实际上会被存储到包含 with
块的函数结束,如本例所示:
class CM(object):
def __init__(self):
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
return True
def foo():
with CM():
raise ValueError
print(sys.exc_info())
即使 with
块在 print
发生时已经结束,所以异常处理应该完成,sys.exc_info
仍然 returns 异常信息就好像有一个活跃的例外。这也是您的代码中发生的情况:with assertRaises
块导致回溯一直持续到该函数的末尾,从而干扰您的 os.remove
。这似乎是错误的行为,我注意到它在 Python 3 中不再以这种方式工作(print
打印 (None, None None)
),所以我想这是一个用 [ 修复的疣=61=] 3.
基于此,我怀疑在 os.remove
之前(在 with assertRaises
块之后)插入一个 sys.exc_clear()
可能就足够了。