English 中文(简体)
在Python中,在“with”块内放弃是安全的吗(为什么)? 这是中文翻译。
原标题:
  • 时间:2009-03-26 09:24:56
  •  标签:

协程和资源获取的组合似乎可能具有一些无意的(或不直观的)后果。

基本问题是这样的东西是否有效:

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?

Update

我之前在谈论写锁定的文件句柄争用问题,但是由于大多数操作系统都是按进程分配文件句柄的,所以在那里不会有争用。 (感谢 @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()
最佳回答

我并不真正明白你在问什么类型的冲突,也不知道这个例子有什么问题:对于同一个文件拥有两个共存且独立的句柄是可以的。

我不知道的一件事,在回答你的问题时学到的是,生成器上有一个新的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 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!
问题回答

我不认为有真正的冲突。你只需要意识到,发生器就像任何其他保存资源的对象一样,因此创建者有责任确保其正确地完成(并避免与对象持有的资源发生冲突/死锁)。我在此仅看到一个(轻微的)问题是,生成器不实现上下文管理协议(至少在Python 2.5之前),因此您不能仅:

with coroutine() as cr:
  doSomething(cr)

但是必须要:

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

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

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

然而,生成器始终(几乎总是)“关闭”,可以通过显式的close()调用或通过被垃圾回收来实现。关闭生成器将在生成器内抛出GeneratorExit异常,因此运行finally子句、语句清理等。您可以捕获异常,但必须抛出或退出函数(即抛出StopIteration异常),而不是使用yield。在您编写的情况下,依赖垃圾收集器关闭生成器可能是不好的做法,因为这可能会比您想要的时间晚,并且如果有人调用sys._exit(),那么您的清理可能根本不会发生。

简而言之,可以这样看待:

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

Contextpass 完成后(即什么也不做),yield 完成后执行 pass(即执行继续)。因此,在 yield 恢复控制后,with 结束。

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

那就是我期望事情運作的方式。是的,這個區塊在完成之前不會釋放其資源,因此在這個意義上,該資源已經逃離了其詞法巢穴。然而,這與在with區塊中嘗試使用相同資源的函數呼叫沒有什麼不同 - 在區塊尚未終止的情況下,不論是什麼原因,沒有任何幫助。這與生成器沒有什麼特別的關係。

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





相关问题
热门标签