使用pytest测试文件读取功能时出错

问题描述 投票:2回答:1

我有一个简单的功能:

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'>

为什么这个测试没有通过?

python python-3.x pytest
1个回答
3
投票

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

>>> 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()

这个问题是如果在returnopen之间发生某些事情(例如,例外或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函数)

© www.soinside.com 2019 - 2024. All rights reserved.