[Python-ideas] use context managers for new-style "for" statement

Bruce Frederiksen dangyogi at gmail.com
Sat Feb 21 04:30:21 CET 2009


Raymond Hettinger wrote:
> ## untested recipe
>
> def closeme(iterable):
> it = iter(iterable)
> try:
>  for i in it:
>   yield i
> finally:
>   it.close()
>
>
> # doesn't this do this same thing without any interpreter magic?
> for i in closeme(gen(x)):
>   ...
>
> for i in chain.from_iterable(map(closeme, [it1, it2, it3, it4])):
>   ...
No, this only works on CPython because of the referencing counting 
collector and the fact that PEP 342 specifies that __del__ calls close 
on generators.  This will not work reliably on Jython, IronPython or 
Pypy because none of these have reference counting collectors.

The closeme generator really adds nothing here, because it is just 
another generator that relies on either running off the end of the 
generator, or its close or throw methods to be called to activate the 
finally clause.  This is identical to the generators that it is being 
mapped over.  *Nothing* in python is defined to call the close or throw 
methods on generators, except for the generator __del__ method -- and 
that is *only* called reliably in CPython, and not in any of the other 
python implementations which may never garbage collect the generator if 
it's allocated near the end of the program run!

I had generators with try/finally, and these fail on Jython and 
IronPython.  What I ended up doing to get my program working on Jython 
was to convert all of my generators to return context managers.  That 
way I could not accidentally forget to use a with statement with them.  
Thus:

def gen(x):
    return itertools.chain.from_iterable(...)

for i in gen(x):
    ...

becomes the following hack:

class chain_context(object):
    def __init__(self, outer_it):
        self.outer_it = outer_iterable(outer_it)
    def __enter__(self):
        return itertools.chain.from_iterable(self.outer_it)
    def __exit__(self, type, value, tb): self.outer_it.close()

class outer_iterable(object):
    def __init__(self, outer_it):
        self.outer_it = iter(outer_it)
        self.inner_it = None
    def __iter__(self): return self
    def close(self):
        if hasattr(self.inner_it, '__exit__'):
            self.inner_it.__exit__(None, None, None)
        elif hasattr(self.inner_it, 'close'): self.inner_it.close()
        if hasattr(self.outer_it, 'close'): self.outer_it.close()
    def next(self):
        ans = self.outer_it.next()
        if hasattr(ans, '__enter__'):
            self.inner_it = ans
            return ans.__enter__()
        ans = iter(ans)
        self.inner_it = ans
        return ans

def gen(x):
    return chain_context(...)

with gen(x) as it:
    for i in it:
        ...

Most of my generators used chain.  Those that didn't went from:

def gen(x):
    ...

to:

def gen(x):
    def gen2(x):
        ...
    return contextlib.closing(gen2(x))

This got the program working on Jython in a way that future maintenance 
on the program can't screw up, but it sure doesn't feel "pythonic"...

-bruce frederiksen



More information about the Python-ideas mailing list