我有一个包含两个嵌套的with个块的Python应用程序,其中一个块发生在对象的__iter__方法中.我观察到,将可迭代对象包装在生成器表达式中会改变引发异常时完成这with个块的顺序,我对此感到非常惊讶.

我使用的是python3.11.5,但在3.8.17中观察到了相同的行为.

这是一个演示,它再现了观察到的行为和预期的行为:

from contextlib import closing, contextmanager


class Reader:
    def close(self):
        print(f"CLOSE: {self.__class__.__name__}")

    @contextmanager
    def get_resource(self):
        try:
            yield 42
        finally:
            print(f"CLOSE: {self.__class__.__name__} releases resource")


class Container:
    def __init__(self, reader: Reader):
        self._reader = reader

    def __iter__(self):
        with self._reader.get_resource() as resource:
            for x in range(10):
                yield x


def my_code():
    with closing(Reader()) as reader:
        container = Container(reader)
        container_gen = (x for x in container)
        for thing in container_gen:
            assert False


def expected():
    with closing(Reader()) as reader:
        container = Container(reader)
        for thing in container:
            assert False


print(">>> What my code does:")
try:
    my_code()
except AssertionError:
    print("handle exception")

print()

print(">>> The context handling I expected:")
try:
    expected()
except AssertionError:
    print("handle exception")

这是我从下面的代码中得到的输出:

>>> What my code does:
CLOSE: Reader
handle exception
CLOSE: Reader releases resource

>>> The context handling I expected:
CLOSE: Reader releases resource
CLOSE: Reader
handle exception

似乎将Container包装在生成器表达式中会导致在异常处理完成后清理其__iter__方法中的with块,尽管它嵌套在引发异常时被清理的另一个with块中.

为什么将Iterable包装在生成器表达式中会改变其__iter__方法内的with块在异常处理期间被终结的方式?

编辑:

感谢@user2357112的回答,我看到省略绑定到生成器表达式的局部变量可以得到我所期望的上下文处理:

def my_code_without_local():
    with closing(Reader()) as reader:
        container = Container(reader)
        for thing in (x for x in container):
            assert False

这将产生以下输出:

CLOSE: Reader releases resource
CLOSE: Reader
handle exception

这种行为真的让我感到惊讶,因为我预计命名生成器表达式container_gen中的活动with块会像my_code中的活动with块一样被清理,而不是它会挂起,只有在本地名称container_gen超出范围时才会被清理.

推荐答案

你依赖于发电机中的with,这是很尴尬的,因为清理很容易像这样被推迟.具体来说,我指的是您编写的__iter__方法,它是作为生成器编写的.不是genexp--我稍后再谈.

如果您没有完全循环您的__iter__生成器,with块清理仅在生成器为closed时运行.像大多数人一样,你从来没有明确关闭过你的发电机.你可能从来没有想过这一点.它唯一被关闭的时间是在生成器的__del__方法中,当Python确定生成器不可访问时会调用该方法.

对于expected,对__iter__生成器的唯一引用是for循环对其迭代器的内部引用.该引用在with closing(Reader()) as reader:块退出之前死亡,由于CPython有引用计数,解释器立即检测到生成器不可访问,因此它调用__del__,触发预期的清理.(在非REFCOUNT实现上,如PyPy,这种清理不会那么迅速.)

对于my_code,您的__iter__生成器可以从genexp创建的生成器访问,该生成器可以通过container_gen局部变量访问.该局部变量可以通过异常回溯来访问,因此直到except块完成并且异常对象终止时才会清除该局部变量.(这是正确的,即使您的except块没有as子句-在except块期间,异常及其回溯在sys.exc_info()期间仍然可用.)

你的发电机最终生命周期 比你预期的要长得多.因为您期望的清理只由生成器的__del__方法触发,所以清理被延迟了同样长的时间.

Python相关问答推荐

是什么导致对Python脚本的jQuery Ajax调用引发500错误?

无法使用equals_html从网址获取全文

如何在Python中将returns.context. DeliverresContext与Deliverc函数一起使用?

类型错误:输入类型不支持ufuncisnan-在执行Mann-Whitney U测试时[SOLVED]

什么相当于pytorch中的numpy累积ufunc

将pandas Dataframe转换为3D numpy矩阵

Python键入协议默认值

所有列的滚动标准差,忽略NaN

Pandas Loc Select 到NaN和值列表

什么是最好的方法来切割一个相框到一个面具的第一个实例?

使用groupby方法移除公共子字符串

Django—cte给出:QuerySet对象没有属性with_cte''''

在www.example.com中使用`package_data`包含不包含__init__. py的非Python文件

判断solve_ivp中的事件

如何在达到end_time时自动将状态字段从1更改为0

polars:有效的方法来应用函数过滤列的字符串

在二维NumPy数组中,如何 Select 内部数组的第一个和第二个元素?这可以通过索引来实现吗?

为什么t sns.barplot图例不显示所有值?'

GPT python SDK引入了大量开销/错误超时

极点替换值大于组内另一个极点数据帧的最大值