【问题标题】:Is it safe to yield from within a "with" block in Python (and why)?从 Python 中的“with”块中产生是否安全(以及为什么)?
【发布时间】:2010-10-15 15:55:42
【问题描述】:

协同程序和资源获取的结合似乎会产生一些意想不到的(或不直观的)后果。

基本问题是这样的事情是否有效:

def coroutine():
    with open(path, 'r') as fh:
        for line in fh:
            yield line

它的作用。 (你可以测试一下!)

更深层次的担忧是with 应该是finally 的替代品,您可以确保在块的末尾释放资源。协程可以在 with 块内暂停和恢复执行,那么冲突如何解决?

例如,如果您在协程内部和外部都以读/写方式打开文件,而协程尚未返回:

def coroutine():
    with open('test.txt', 'rw+') as fh:
        for line in fh:
            yield line

a = coroutine()
assert a.next() # Open the filehandle inside the coroutine first.
with open('test.txt', 'rw+') as fh: # Then open it outside.
    for line in fh:
        print 'Outside coroutine: %r' % repr(line)
assert a.next() # Can we still use it?

更新

在前面的示例中,我打算进行写锁定文件句柄争用,但由于大多数操作系统为每个进程分配文件句柄,因此不会出现争用。 (感谢@Miles 指出该示例并没有多大意义。)这是我修改后的示例,它显示了一个真正的死锁情况:

import threading

lock = threading.Lock()

def coroutine():
    with lock:
        yield 'spam'
        yield 'eggs'

generator = coroutine()
assert generator.next()
with lock: # Deadlock!
    print 'Outside the coroutine got the lock'
assert generator.next()

【问题讨论】:

  • @Miles 指出该示例格式有些错误。我想要一个写锁定的文件句柄,但由于操作系统可能会为每个进程分配文件句柄,这会很好。
  • TL;DR yieldreturn 是安全的(因为它们最终会释放资源)。但是return 可能表现不佳。考虑with os.scandir() as entries: return entries。它根本行不通!请改用with os.scandir() as entries: yield from entries 或简单的return os.scandir()。如果没有用尽,第二种解决方案将要求您在 ScandirIterator 实例上调用 .close()。这只是一个示例,但它说明了从 with 语句返回临时资源时会发生什么。

标签: python resources coroutine


【解决方案1】:

对于 TLDR,这样看:

with Context():
    yield 1
    pass  # explicitly do nothing *after* yield
# exit context after explicitly doing nothing

Contextpass 完成后结束(即什么都没有),passyield 完成后执行(即恢复执行)。因此,with 结束 控制在yield 处恢复。

TLDR:当yield 释放控制权时,with 上下文仍然保持不变。


这里实际上只有两条规则:

  1. with 什么时候释放它的资源?

    它这样做一次并且直接它的块完成之后。前者意味着它不会在yield 期间发布,因为这可能会发生多次。后者意味着它确实在yield 完成后释放

  2. yield 什么时候完成?

    yield 视为反向调用:控制权向上传递给调用者,而不是向下传递给被调用者。同样,yield 在将控制权传回给它时完成,就像调用返回控制权一样。

请注意,withyield 都在这里按预期工作! with lock 的目的是保护资源,并在 yield 期间保持受保护状态。您始终可以显式释放此保护:

def safe_generator():
  while True:
    with lock():
      # keep lock for critical operation
      result = protected_operation()
    # release lock before releasing control
    yield result

【讨论】:

    【解决方案2】:

    我真的不明白你问的是什么冲突,也不明白这个例子的问题:同一个文件有两个共存的独立句柄很好。

    在回答您的问题时我不知道我了解到,生成器上有一个新的 close() 方法:

    close() 在生成器内部引发一个新的GeneratorExit 异常以终止迭代。收到此异常后,生成器的代码必须引发 GeneratorExitStopIteration

    close() 在生成器被垃圾回收时调用,因此这意味着生成器的代码在生成器被销毁之前有最后一次运行机会。这最后一次机会意味着生成器中的try...finally 语句现在可以保证工作; finally 子句现在总是有机会运行。这似乎是一些语言琐事,但实际上需要使用生成器和 try...finally 才能实现 PEP 343 描述的 with 语句。

    http://docs.python.org/whatsnew/2.5.html#pep-342-new-generator-features

    这样可以处理在生成器中使用 with 语句但它在中间产生但从不返回的情况 - 上下文管理器的 __exit__ 方法将在生成器被垃圾收集时调用。


    编辑

    关于文件句柄问题:我有时会忘记存在不类似于 POSIX 的平台。 :)

    就锁而言,我认为 Rafał Dowgird 说的“你只需要注意,生成器就像任何其他拥有资源的对象一样”是一针见血。我不认为with 语句在这里真的那么相关,因为这个函数也有同样的死锁问题:

    def coroutine():
        lock.acquire()
        yield 'spam'
        yield 'eggs'
        lock.release()
    
    generator = coroutine()
    generator.next()
    lock.acquire() # whoops!
    

    【讨论】:

    • 根据您对示例的困惑进行了更新。它实际上有助于形成一个答案——拥有多个读/写文件句柄上下文管理器是可以的,(因为大多数操作系统为每个进程分配它们)但大多数其他资源(即非递归锁)并非如此。跨度>
    • +1:什么冲突?当 yield 停止时,with 语句关闭资源。什么冲突?
    • 只是为了澄清,“当产量停止时”=> 当生成器退出 with 块时。这可能是在多次收益之后。
    • 出色的分析。 with 语句基本上只是诸如 lock.acquire/lock.release 和文件打开和关闭之类的语法糖。你仍然需要避免死锁,但它会让你的代码更干净。
    【解决方案3】:

    因为yield 可以执行任意代码,所以我会非常警惕对yield 语句持有锁。不过,您可以通过许多其他方式获得类似的效果,包括调用可能已被覆盖或以其他方式修改的方法或函数。

    然而,生成器总是(几乎总是)“关闭”,要么通过明确的close() 调用,要么只是通过垃圾收集。关闭生成器会在生成器内引发GeneratorExit 异常,因此运行 finally 子句、语句清理等。您可以捕获异常,但必须抛出或退出函数(即抛出StopIteration 异常),而不是屈服。在您编写的情况下,依靠垃圾收集器关闭生成器可能是一种不好的做法,因为这可能会比您想要的更晚发生,并且如果有人调用 sys._exit(),那么您的清理可能根本不会发生.

    【讨论】:

      【解决方案4】:

      我认为没有真正的冲突。您只需要知道生成器就像任何其他拥有资源的对象一样,因此创建者有责任确保它被正确完成(并避免与对象所拥有的资源发生冲突/死锁)。我在这里看到的唯一(次要)问题是生成器没有实现上下文管理协议(至少从 Python 2.5 开始),所以你不能只是:

      with coroutine() as cr:
        doSomething(cr)
      

      但必须:

      cr = coroutine()
      try:
        doSomething(cr)
      finally:
        cr.close()
      

      垃圾收集器无论如何都会执行close(),但依赖它来释放资源是不好的做法。

      【讨论】:

      • with contextlib.closing(coroutine()) as cr:
      【解决方案5】:

      这就是我所期望的工作方式。是的,块在完成之前不会释放它的资源,所以从这个意义上说,资源已经逃脱了它的词法嵌套。但是,这与尝试在 with 块中使用相同资源的函数调用没有什么不同 - 在块 not 尚未终止的情况下,对于 whatever 原因。它并不是特定于生成器的任何东西。

      可能值得担心的一件事是如果生成器从不恢复时的行为。我原以为with 块会像finally 块一样工作,并在终止时调用__exit__ 部分,但情况似乎并非如此。

      【讨论】:

        猜你喜欢
        • 2019-08-12
        • 2017-06-12
        • 2016-09-06
        • 2022-11-05
        • 2011-01-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多