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

P D*_*ddy 7 python generator

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

from typing import Iterator

def get_numbers() -> Iterator[int]:
    ...
Run Code Online (Sandbox Code Playgroud)

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

for i in get_numbers():
    print(i)
Run Code Online (Sandbox Code Playgroud)

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

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

现在让我们尝试以下实现get_numbers

def get_numbers() -> Iterator[int]:
    yield 1
    yield 2
    yield 3
Run Code Online (Sandbox Code Playgroud)

这里没什么有趣的。运行我们的小程序的结果几乎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!
>>>
Run Code Online (Sandbox Code Playgroud)

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

>>> 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!
>>>
Run Code Online (Sandbox Code Playgroud)

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

def get_numbers() -> Iterator[int]:
    return iter([1, 2, 3])
Run Code Online (Sandbox Code Playgroud)

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

def get_numbers() -> Iterator[int]:
    acquire_some_resource()
    try:
        yield 1
        yield 2
        yield 3
    finally:
        release_some_resource()
Run Code Online (Sandbox Code Playgroud)

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

>>> 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!
>>>
Run Code Online (Sandbox Code Playgroud)

但是,如果我们不能使用简单的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!
>>>
Run Code Online (Sandbox Code Playgroud)

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

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

>>> 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!
>>>
Run Code Online (Sandbox Code Playgroud)

这种方法的问题在于,它假定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'
>>>
Run Code Online (Sandbox Code Playgroud)

因此,在这里要做的正确的事情很繁琐:

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

def release_some_resource() -> None:
    print("done generating numbers")
Run Code Online (Sandbox Code Playgroud)

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

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

r.o*_*ook 5

正如我的评论所提到的,正确构建此结构的一种方法是使用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()
Run Code Online (Sandbox Code Playgroud)

然后当你使用生成器时:

with get_numbers() as et:
    for i in et:
        if i % 2 == 0:
            raise ValueError()
        else:
            print(i)
Run Code Online (Sandbox Code Playgroud)

结果:

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

这允许contextmanager装饰器为您管理资源,而不必担心处理发布。如果您有勇气,您甚至可以构建自己的上下文管理器类,__enter__并使用__exit__它的函数来处理您的资源。

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

  • +1我喜欢这一点的是,被调用者本质上声明它需要清理,有效地将部分责任转移到它所属的被调用者身上。我不喜欢的是它仍然需要被调用者和调用者之间的合作。因此,举例来说,如果这是 ABC 的实现,则实现者无法选择是否需要清理。这是一个无法概括的细节。但这只是目前语言的限制,我认为你的答案是正确的方法。 (2认同)