如何确保发电机正确关闭?

时间:2019-11-06 19:40:30

标签: python generator

考虑具有以下签名的库函数:

from typing import Iterator

def get_numbers() -> Iterator[int]:
    ...

让我们看一些使用它的简单代码:

for i in get_numbers():
    print(i)

到目前为止,没有什么有趣的。但是,假设我们不在乎偶数。只有奇数,例如我们:

for i in get_numbers():
    if i & 1 == 0:
        raise ValueError("Ew, an even number!")
    print(i)

现在让我们尝试实现get_numbers

def get_numbers() -> Iterator[int]:
    yield 1
    yield 2
    yield 3

这里没有什么很有趣的。运行我们的小for的结果几乎是我们期望的:

>>> for i in get_numbers():
  2     if i & 1 == 0:
  3         raise ValueError("Ew, an even number!")
  4     print(i)
1
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
ValueError: Ew, an even number!

Ew, an even number!
>>>

如果get_numbers具有更简单的实现,我们将获得完全相同的结果:

def get_numbers() -> Iterator[int]:
    return iter([1, 2, 3])

但是让我们假设get_numbers因为它管理一些资源而需要保留一个生成器。

def get_numbers() -> Iterator[int]:
    acquire_some_resource()
    try:
        yield 1
        yield 2
        yield 3
    finally:
        release_some_resource()

出于我们的目的,我们将管理的资源只是在屏幕上打印的文本:

def acquire_some_resource() -> None:
    print("generating some numbers")

def release_some_resource() -> None:
    print("done generating numbers")

我们的输出仍然可以预测:

>>> for i in get_numbers():
  2     if i & 1 == 0:
  3         raise ValueError("Ew, an even number!")
  4     print(i)
generating some numbers
1
done generating numbers
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
ValueError: Ew, an even number!

Ew, an even number!
>>>

但是如果我们不能使用简单的for循环怎么办?例如,如果我们想忽略第一个数字怎么办? (让我们假装itertools.islice不是问题。)

>>> it = get_numbers()
  2 next(it, None)
  3 for i in it:
  4     if i & 1 == 0:
  5         raise ValueError("Ew, an even number!")
  6     print(i)
generating some numbers
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
ValueError: Ew, an even number!

Ew, an even number!
>>>

有事吗?我们已经获得了资源,正如“生成一些数字”这样的文字所证明的那样,但我们从未发布过它。

正确的做法是确保关闭生成器:

>>> it = get_numbers()
  2 try:
  3     next(it, None)
  4     for i in it:
  5         if i & 1 == 0:
  6             raise ValueError("Ew, an even number!")
  7         print(i)
  8 finally:
  9     it.close()
generating some numbers
done generating numbers
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
ValueError: Ew, an even number!

Ew, an even number!
>>>

此方法的问题在于,它假设get_numbers()返回一个生成器,因此具有close方法。但是它的签名并不能保证这一点。如果它的实现是我之前给出的更简单的实现该怎么办?

>>> def get_numbers() -> Iterator[int]:
  2     return iter([1, 2, 3])
  3 
  4 it = get_numbers()
  5 try:
  6     next(it, None)
  7     for i in it:
  8         if i & 1 == 0:
  9             raise ValueError("Ew, an even number!")
 10         print(i)
 11 finally:
 12     it.close()
Traceback (most recent call last):
  File "<stdin>", line 12, in <module>
AttributeError: 'list_iterator' object has no attribute 'close'

'list_iterator' object has no attribute 'close'
>>>

所以在这里正确要做的事情很繁琐:

it = get_numbers()
try:
    next(it, None)
    for i in it: 
        if i & 1 == 0: 
            raise ValueError("Ew, an even number!") 
        print(i) 
finally: 
    if hasattr(it, "close"): 
        it.close()

我可以将其包装在上下文管理器中以使其更简单,但是感觉就像我在做某种语言应该为我做的事情,或者至少是被调用方应该与自身相关,而不是与调用方相关。

有没有更简单的方法来解决这个问题?

2 个答案:

答案 0 :(得分:3)

正如我的评论所提到的那样,一种正确构造此结构的方法是使用contextlib.contextmanager装饰生成器:

from typing import Iterator
import contextlib

@contextlib.contextmanager
def get_numbers() -> Iterator[int]:
    acquire_some_resource()
    try:
        yield iter([1, 2, 3])
    finally:
        release_some_resource()

然后在使用生成器时:

with get_numbers() as et:
    for i in et:
        if i % 2 == 0:
            raise ValueError()
        else:
            print(i)

结果:

generating some numbers
1
done generating numbers
Traceback (most recent call last):
  File "<pyshell#64>", line 4, in <module>
    raise ValueError()
ValueError

这使contextmanager装饰器可以为您管理资源,而不必担心处理发行版。如果您有勇气,甚至可以使用带有__enter____exit__函数的build your own context manager类来处理资源。

我认为这里的要点是,由于应该由生成器来管理资源,因此您应该使用with语句,或者之后始终关闭它,就像f = open(...)应该始终紧随其后f.close()

答案 1 :(得分:0)

一种选择是实际使用Generator type,以正确表示您正在返回Generator