从Python中的“with”块中获取是否安全(以及为什么)?

时间:2009-03-26 09:24:57

标签: python resources coroutine

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

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

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

5 个答案:

答案 0 :(得分:22)

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

有一件事我不知道我在回答你的问题时学到了它对生成器有一个新的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łFowgird在说“你必须要知道发电机就像任何其他拥有资源的物体一样。”我不认为with语句在这里真的那么重要,因为这个函数会遇到同样的死锁问题:

def coroutine():
    lock.acquire()
    yield 'spam'
    yield 'eggs'
    lock.release()

generator = coroutine()
generator.next()
lock.acquire() # whoops!

答案 1 :(得分:9)

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

with coroutine() as cr:
  doSomething(cr)

但必须:

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

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

答案 2 :(得分:1)

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

然而,

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

答案 3 :(得分:0)

这就是我期望事情发挥作用的方式。是的,该块在完成之前不会释放其资源,因此从这个意义上讲,资源已经逃脱了它的词法嵌套。但是,这与尝试在with块中使用相同资源的函数调用没有什么不同 - 在块但是已终止的情况下没有任何帮助,对于无论什么< / em>原因。这对发电机来说并不是特定的。

可能值得担心的一件事是,如果生成器从不恢复,则会出现这种情况。我希望with块的行为类似于finally块,并在终止时调用__exit__部分,但情况似乎并非如此。

答案 4 :(得分:0)

对于TLDR,请以这种方式查看:

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

Context完成pass后结束(即没有),passyield完成后执行(即执行恢复)。因此,在with恢复控件之后yield结束

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

实际上只有两条相关的规则:

  1. with什么时候发布资源?

    一旦 块完成后直接。前者意味着它不会在 a yield期间释放,因为这可能会发生几次。后面的意思是它在 yield完成后发布

  2. yield什么时候完成?

    yield视为反向调用:控制权传递给调用者,而不是调用者。类似地,yield在控制权传递给它时完成,就像调用返回控制时一样。

  3. 请注意,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