Python 使用pytest测试文件读取函数时出错

Python 使用pytest测试文件读取函数时出错,python,python-3.x,pytest,Python,Python 3.x,Pytest,我有一个简单的功能: 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 pyte

我有一个简单的功能:

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'>
E失败:未引发

为什么这次考试不及格

您正在创建一个生成器函数。调用generator函数将返回一个generator对象:

>>> 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>
问题在于,如果在
打开
关闭
之间发生某种情况(例如异常或
返回
等),文件可能无法关闭。您可以使用
try/finally
解决此问题,即

f = open(filename)
try:
    f.read(4)
    # etc
finally:
    f.close()
这很麻烦,因此我们使用
with
语句将其缩短为

with open(filename) as f:
    f.read(4)
    # etc
这很好,因为它减少了混乱,并且在文件关闭之前,无法使用语句离开
。但是,当您在上面的
read\u文件
这样的生成器中执行此操作时,可能会有人用

for line in read_file(filename):
    if line.startswith('#'):
        break
现在,在
中断后
生成器在
产量处暂停
它无法知道它不会再次迭代,因此它在那里等待。
with
块中的
yield
允许您在不关闭文件的情况下离开上下文管理器。(使用
try/finally
时也会出现同样的问题,但在这种情况下可能更为明显。)即使您知道不会
break
循环体的异常也会产生同样的效果

在这种情况下,由于CPython中的ref counting GC,文件可能会被关闭:当收集生成器时,它将被关闭,在终止
with
块时抛出异常,从而关闭文件。这并不比允许GC直接收集文件对象
f
(也通过
file.\uu del\uu
关闭文件)好多少

简单的规则是:

不要在带有
语句的
中产生

这意味着您通常应该在生成器外部使用
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

(即代码中不再有
读取行
功能)

这是有意义的。谢谢相关问题:
open
是否为生成器流中的每个元素调用一次?No open仅在请求第一个元素时调用。生成器函数的规则意味着当您请求第一个元素时,它将继续执行,直到第一个
产生
。另外,我不建议在这样的生成器函数中使用
,我会更新答案来解释原因。谢谢,解释得很好!
def read_file(f):
    yield from f
with open(filename) as fin:
    for line in fin:
        # do stuff