Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it good practice to yield from within a context manager?

I recently wrote a method which returned a sequence of open files; in other words, something like this:

# this is very much simplified, of course
# the actual code returns file-like objects, not necessarily files
def _iterdir(self, *path):
    dr = os.path.join(*path)
    paths = imap(lambda fn: os.path.join(dr, fn), os.listdir(dr))

    return imap(open, paths)

Syntactically, I do not expect to have to close the resulting objects if I do something like:

for f in _iterdir('/', 'usr'):
    make_unicorns_from(f)
    # ! f.close()

As a result, I decided to wrap _iterdir in a context manager:

def iterdir(self, *path):
    it = self._iterdir(*path)

    while 1:
        with it.next() as f:
            yield f

This appears to be working correctly.

What I'm interested in is whether doing this is good practice. Will I run into any issues following this pattern (perhaps if exceptions are thrown)?

like image 285
sapi Avatar asked Jul 11 '14 06:07

sapi


People also ask

What does yield do in context manager?

The yield expression pauses the generator, control goes back to the decorator.

When would you use a context manager?

A context manager usually takes care of setting up some resource, e.g. opening a connection, and automatically handles the clean up when we are done with it. Probably, the most common use case is opening a file. The code above will open the file and will keep it open until we are out of the with statement.

Can context managers be used outside the with statement?

Yes, the context manager will be available outside the with statement and that is not implementation or version dependent. with statements do not create a new execution scope.

What is required to make a class A context manager?

Creating a Context Manager: When creating context managers using classes, user need to ensure that the class has the methods: __enter__() and __exit__(). The __enter__() returns the resource that needs to be managed and the __exit__() does not return anything but performs the cleanup operations.


1 Answers

There are two problems I see. One is that if you try to use more than one file at a time, things break:

list(iterdir('/', 'usr')) # Doesn't work; they're all closed.

The second is unlikely to happen in CPython, but if you have a reference cycle, or if your code is ever run on a different Python implementation, the problem can manifest.

If an exception happens in make_unicorns_from(f):

for f in iterdir('/', 'usr'):
    make_unicorns_from(f) # Uh oh, not enough biomass.

The file you were using won't be closed until the generator is garbage-collected. At that point, the generator's close method will be called, throwing a GeneratorExit exception at the point of the last yield, and the exception will cause the context manager to close the file.

With CPython's reference counting, this usually happens immediately. However, on a non-reference-counted implementation or in the presence of a reference cycle, the generator might not be collected until a cycle-detecting GC pass is run. This could take a while.


My gut says to leave closing the files to the caller. You can do

for f in _iterdir('/', 'usr'):
    with f:
        make_unicorns_from(f)

and they'll all be closed promptly, even without a with in the generator, and even if an exception is thrown. I don't know whether or not this is actually a better idea than having the generator take charge of closing the files.

like image 165
user2357112 supports Monica Avatar answered Oct 23 '22 01:10

user2357112 supports Monica