从Python中的“with”块(以及为什么)产生安全吗?

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

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

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

它做的。 (你可以testing它!)

更深层次的问题是,应该是finally的替代scheme,在那里确保资源在块的末尾被释放。 协程可以暂停和恢复执行,那么冲突是如何解决的呢?

例如,如果在协程尚未返回的情况下,在协同程序内部和外部使用读/写打开文件:

 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? 

更新

在前面的例子中,我想写locking的文件句柄争用,但是由于大多数操作系统每个进程都分配文件句柄,所以没有争用。 (荣誉@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() 

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

有一件事我不知道,我在回答你的问题时已经知道,在generator中有一个新的close()方法:

close()会在GeneratorExit引发一个新的GeneratorExitexception来终止迭代。 在接收到这个exception时,生成器的代码必须提高GeneratorExitStopIteration

当一个生成器被垃圾收集时, close()被调用,所以这意味着生成器的代码在生成器被销毁之前得到最后一次运行的机会。 这最后一个机会意味着, try...finally发电机中的try...finally陈述现在可以保证工作; finally条款现在总是有机会运行。 这似乎只是一些小小的语言琐事,但是使用生成器并try...finally实际上是实现PEP 343所描述的声明所必需的。

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

因此,处理在生成器中使用with语句的情况,但是它在中间产生,但从不返回 – 当生成器被垃圾收集时,上下文pipe理器的__exit__方法将被调用。


编辑

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

就锁具而言,我认为RafałDowgird在说到“你必须意识到发电机就像任何其他持有资源的物体一样”。 我不认为这里的with语句真的是相关的,因为这个函数会遇到同样的死锁问题:

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

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

 with coroutine() as cr: doSomething(cr) 

而是必须:

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

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

因为yield可以执行任意代码,所以我会非常小心地对yield语句进行locking。 不过,您可以通过其他方式获得类似的效果,包括调用可能已被覆盖或以其他方式修改的方法。

然而,生成器总是(几乎总是)“closures”,不pipe是用close()函数还是垃圾回收。 closures一个生成器会在GeneratorExit器中抛出一个GeneratorExitexception,从而运行finally子句,语句清理等等。你可以捕获exception,但是你必须抛出或者退出这个函数(即抛出一个StopIterationexception),而不是yield。 在你写的情况下依靠垃圾回收器来closures生成器可能是一个很糟糕的做法,因为这可能比你想要的要晚,如果有人调用sys._exit(),那么你的清理可能根本不会发生。

那将是我期待的事情。 是的,这个块在它完成之前不会释放它的资源,所以从这个意义上来说资源已经逃脱了它的词汇嵌套。 但是,这与尝试在一个块中使用相同资源的函数调用没有什么不同,因为无论出于何种原因,在块尚未终止的情况下都没有什么帮助。 这实际上并不是什么特定的发电机。

有一件事值得担心的是,如果发电机永远不能恢复的话。 我会期望with块像finally块一样工作,并在终止时调用__exit__部分,但似乎并不是这样。