Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to yield from within a "with" block in Python (and why)?

The combination of coroutines and resource acquisition seems like it could have some unintended (or unintuitive) consequences.

The basic question is whether or not something like this works:

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

Which it does. (You can test it!)

The deeper concern is that with is supposed to be something an alternative to finally, where you ensure that a resource is released at the end of the block. Coroutines can suspend and resume execution from within the with block, so how is the conflict resolved?

For example, if you open a file with read/write both inside and outside a coroutine while the coroutine hasn't yet returned:

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

I was going for write-locked file handle contention in the previous example, but since most OSes allocate filehandles per-process there will be no contention there. (Kudos to @Miles for pointing out the example didn't make too much sense.) Here's my revised example, which shows a real deadlock condition:

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() 
like image 350
cdleary Avatar asked Mar 26 '09 09:03

cdleary


People also ask

What does yield from do in Python?

Yield is a keyword in Python that is used to return from a function without destroying the states of its local variable and when the function is called, the execution starts from the last yield statement. Any function that contains a yield keyword is termed a generator. Hence, yield is what makes a generator.

Should I use yield in Python?

We should use yield when we want to iterate over a sequence, but don't want to store the entire sequence in memory. Yield are used in Python generators. A generator function is defined like a normal function, but whenever it needs to generate a value, it does so with the yield keyword rather than return.

Can I use yield with the for loop Python?

yield in Python can be used like the return statement in a function. When done so, the function instead of returning the output, it returns a generator that can be iterated upon. You can then iterate through the generator to extract items. Iterating is done using a for loop or simply using the next() function.

How do you return a yield in Python?

The Yield keyword in Python is similar to a return statement used for returning values or objects in Python. However, there is a slight difference. The yield statement returns a generator object to the one who calls the function which contains yield, instead of simply returning a value.


1 Answers

I don't really understand what conflict you're asking about, nor the problem with the example: it's fine to have two coexisting, independent handles to the same file.

One thing I didn't know that I learned in response to your question it that there is a new close() method on generators:

close() raises a new GeneratorExit exception inside the generator to terminate the iteration. On receiving this exception, the generator’s code must either raise GeneratorExit or StopIteration.

close() is called when a generator is garbage-collected, so this means the generator’s code gets one last chance to run before the generator is destroyed. This last chance means that try...finally statements in generators can now be guaranteed to work; the finally clause will now always get a chance to run. This seems like a minor bit of language trivia, but using generators and try...finally is actually necessary in order to implement the with statement described by PEP 343.

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

So that handles the situation where a with statement is used in a generator, but it yields in the middle but never returns—the context manager's __exit__ method will be called when the generator is garbage-collected.


Edit:

With regards to the file handle issue: I sometimes forget that there exist platforms that aren't POSIX-like. :)

As far as locks go, I think Rafał Dowgird hits the head on the nail when he says "You just have to be aware that the generator is just like any other object that holds resources." I don't think the with statement is really that relevant here, since this function suffers from the same deadlock issues:

def coroutine():     lock.acquire()     yield 'spam'     yield 'eggs'     lock.release()  generator = coroutine() generator.next() lock.acquire() # whoops! 
like image 74
Miles Avatar answered Oct 09 '22 02:10

Miles