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

标签 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 方法。但它的签名并没有 promise 这一点。如果它的实现比我之前给出的更简单怎么办?

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

我可以将其包装在上下文管理器中以使其更简单,但感觉我正在做语言应该为我做的事情,或者至少,被调用者应该关心自己,而不是调用者.

有没有更简单的方法来处理这个问题?

最佳答案

正如我的评论所提到的,正确构建此结构的一种方法是使用 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 装饰器为您管理资源,而无需担心处理发布。如果您有勇气,您甚至可以build your own context manager具有 __enter____exit__ 函数的类来处理您的资源。

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

关于python - 如何确保发电机正确关闭?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58737402/

相关文章:

python - 使用 Azure AD 保护 Python Rest API

python - 将字典(数组)导出到 json

python - 类型错误 : can't pickle generator objects

python - 在 scikit-learn 中使用 python 生成器

python - 与 itertools.dropwhile 相反(如何在 N 次迭代后停止生成器)

python - Python 生成器表达式中的变量范围

c++ - 从 1-6 中选择 random() 数字

python - 将整数从列表解析为软件命令行

python - 将 pyBarcode 输出转换为 PIL 图像文件

python - 什么文件模式在不存在时创建新数据并在存在时追加新数据