如何在Python标准库中错误关闭文件对象后进行清理(发生异常后)
TL;DR:引发异常时,标准库无法关闭文件。我在寻找处理这种情况的最佳方法。请随意阅读以“仔细检查CPython的源代码”开头的段落。也可以向下滚动到问题的末尾,获取一个在Windows上复制此问题的自包含脚本 我正在编写一个Python包,其中我使用STL的如何在Python标准库中错误关闭文件对象后进行清理(发生异常后),python,python-2.7,exception-handling,Python,Python 2.7,Exception Handling,TL;DR:引发异常时,标准库无法关闭文件。我在寻找处理这种情况的最佳方法。请随意阅读以“仔细检查CPython的源代码”开头的段落。也可以向下滚动到问题的末尾,获取一个在Windows上复制此问题的自包含脚本 我正在编写一个Python包,其中我使用STL的ConfigParser(2.x)或ConfigParser(3.x)来解析用户配置文件(我将两者都称为ConfigParser,因为问题主要在于2.x实现)。从现在起,我在GitHub上的相关代码行将在适当的时候进行链接ConfigPar
ConfigParser
(2.x)或ConfigParser
(3.x)来解析用户配置文件(我将两者都称为ConfigParser
,因为问题主要在于2.x实现)。从现在起,我在GitHub上的相关代码行将在适当的时候进行链接ConfigParser.ConfigParser.read(文件名)
(在我的代码中使用)在配置文件格式错误时引发ConfigParser.Error
异常。我在测试套件中使用unittest.TestCase.assertRaises(ConfigParser.Error)
针对这种情况。格式错误的配置文件使用tempfile.mkstemp
(返回的fd在任何操作之前使用os.close
关闭),我尝试使用os.remove
os.remove
是故障开始的地方。我的测试在使用Python 2.7的Windows(同时使用OS X和Ubuntu)上失败(请参阅):
请注意,正如我上面所说的,格式错误的配置文件
是使用tempfile.mkstemp
生成的,并立即使用os.close
关闭,因此它打开的唯一时间是在。因此,罪魁祸首似乎是STL,而不是我自己的代码
仔细检查CPython的源代码后,我发现当引发异常时,ConfigParser.ConfigPaser.read
确实无法正确关闭文件。2.7()中的read
方法有以下几行:
对于文件名中的文件名:
尝试:
fp=打开(文件名)
除IOError外:
持续
自读(fp,文件名)
fp.close()
read_ok.append(文件名)
异常(如果有)是由self.\u read(fp,filename)
引发的,但正如您所看到的,如果self.\u read
引发,则不会关闭fp
,因为fp.close()
仅在self.\u read
返回后调用
同时,3.4()中的read
方法不会遇到同样的问题,因为这一次它们在上下文中正确地嵌入了文件处理:
对于文件名中的文件名:
尝试:
将open(filename,encoding=encoding)作为fp:
自读(fp,文件名)
除操作错误外:
持续
read_ok.append(文件名)
所以我认为问题很明显是2.7的STL中的一个缺陷。处理这种情况的最佳方法是什么?具体来说:
- 我这边能做些什么来关闭那个文件吗
- 值得向bugs.python.org报告吗
试试。。除了操作系统错误。
到该操作系统。删除()(有什么建议吗?)
更新:一个自包含脚本,可用于在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'
回溯(最近一次呼叫最后一次):
文件“\example.py”,第19行,在
main()
文件“\example.py”,第16行,主
删除操作系统(路径)
WindowsError:[错误32]进程无法访问该文件,因为另一个进程正在使用它:“c:\\users\\Redact\\appdata\\local\\temp\\tmp07yq2v”
这是一个有趣的问题。正如Lukas Graf在一篇评论中指出的,问题似乎在于异常回溯对象持有对引发异常的调用帧的引用。此调用框架包括当时存在的局部变量,其中一个是对打开文件的引用。因此该文件对象仍有对它的引用,并且未正确关闭
对于您的自包含示例,只需删除try/except ConfigParser.Error
“works”:有关格式错误的配置文件的异常未被捕获并停止程序。但是,在实际应用程序中,assertRaises
正在捕获异常,以便检查它是否是您正在测试的异常。我不能100%确定为什么即使在使用assertRaises执行块之后,回溯仍然存在,但显然它确实存在
例如,另一个更有希望的修复方法是将except
子句中的pass
更改为sys.exc_clear()
:
这将消除讨厌的回溯对象,并允许关闭文件
然而,在实际的应用程序中如何做到这一点还不清楚,因为在unittest
中有一个令人不快的except
子句。我认为最简单的事情可能是不要直接使用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
退出时,应该正确地清除回溯。)然而,我认为这个想法可能会让您有所收获。
try:
config.read(path)
except ConfigParser.Error:
sys.exc_clear()
# 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!")
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())