我有一个简单的功能:
def read_file(fp):
with open(fp) as fr:
for line in fr.readlines():
yield line
当我在一个不存在的文件上运行此函数时,我得到:
FileNotFoundError: [Errno 2] No such file or directory: 'idontexist.txt'
在另一个文件中,我试图使用pytest
测试此函数:
import pytest
from utils import read_file
def test_file_not_exist():
filepath = 'idontexist.txt'
with pytest.raises(FileNotFoundError):
read_file(filepath)
但是,运行pytest
,我收到消息:
E Failed: DID NOT RAISE <class 'FileNotFoundError'>
为什么这个测试没有通过?
您正在创建生成器函数。调用生成器函数返回一个生成器对象:
>>> def read_file(fp):
... with open(filepath) as fr:
... for line in fr.readlines():
... yield line
...
>>> read_file('asd')
<generator object read_file at 0x10554ee08>
在循环生成器之前,不会调用open调用(应该引发FileNotFoundError)。然后你会看到
>>> g = read_file('asd')
>>> for x in g:
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in read_file
NameError: name 'filepath' is not defined
我猜你在发布之前将fp
改为filepath
。您可以解决这个问题,但无论哪种方式,您都不会看到来自read_file
的任何错误,除非您通过迭代生成返回的生成器。
编辑:我不建议在像这样的生成器中使用with
语句。原因是无法保证文件将被关闭。
要理解这一点,请考虑with
的目的是什么。在上下文管理器之前,您将打开一个像
f = open(filename)
f.read(4)
# etc
f.close()
这个问题是如果在return
和open
之间发生某些事情(例如,例外或close
等),文件可能不会被关闭。你可以用try/finally
解决这个问题,即
f = open(filename)
try:
f.read(4)
# etc
finally:
f.close()
这很麻烦所以我们有with
声明缩短它
with open(filename) as f:
f.read(4)
# etc
这很好,因为它减少了混乱,没有文件被关闭就无法离开with
语句。然而,当你在像read_file
这样的生成器上执行它时,有人可能会调用生成器
for line in read_file(filename):
if line.startswith('#'):
break
现在在break
之后,生成器被暂停在yield
,它无法知道它将不再被重复,所以它在那里等待。 yield
块中的with
允许您离开上下文管理器而不关闭文件。 (当使用try/finally
时也会出现同样的问题,但在这种情况下可能更明显。)即使你知道你不会break
循环体的异常会产生相同的效果。
在这种情况下会发生的事情是,由于CPython中的ref计数GC,文件可能会被关闭:当收集生成器时,它将被关闭,在终止with
块并因此关闭文件时抛出异常。这并不比允许GC直接收集文件对象f
(它也通过file.__del__
关闭文件)好多了。
简单的规则是:
不要在
yield
声明中加入with
这意味着你通常应该在生成器之外使用with
语句。所以你做的事情就像
def read_file(f):
for line in f.readlines():
yield line
# Control resource at top level
with open(filename) as fin:
for line in read_file(fin) # pass the resource to generator
# do something with line
另一点:迭代器的重点在于它们允许我们不必执行诸如将整个文件读入内存之类的操作。因此,不是调用将while文件读入内存的readlines()
,而是应该直接迭代文件,一次只能读取一行。通过这两个更改,您的功能如下所示:
def read_file(f):
for line in f:
yield line
甚至:
def read_file(f):
yield from f
就迭代器而言,这只是身份函数,因此它是冗余的并且可以被删除。因此,无论您使用read_lines
功能,只需使用即可
with open(filename) as fin:
for line in fin:
# do stuff
(即代码中不再有read_lines
函数)