Search code examples
pythongeneratorcontextmanager

Taking a generator out of a context manager


I just saw the following code:

from __future__ import print_function
from future_builtins import map # generator

with open('test.txt', 'r') as f:
    linegen = map(str.strip, f)

# file handle should be closed here

for line in linegen:
    # using the generator now
    print(line)

What happens in this case? Is the context manager smart enough to know that linegen still has a reference to the file handle, such that it's not closed when the context is left? Or is this potentially unsafe?


Solution

  • This is one of those breaking changes in Python 3.

    Your question title (Taking a generator ...) implies you are reading it as Python 3 code.

    But the statement

    from __future__ import print_function
    

    implies it was written for Python 2

    In Python 2, map returns an actual list - and thus this code is both perfectly safe and extrmely sensible (i.e open a file, read all lines,s tripping them as you go, then close the file)

    In [2]: with open('README.md','r') as f:
       ...:     lines = map(str.strip, f)
       ...:     
    In [3]: lines
    Out[3]: 
    ['ASM',
     '============',
     '',
    

    In Python 3, the same code throws an exception

    In [1]: with open('README.md','r') as f:
        lines = map(str.strip, f)
       ...:     
    In [2]: lines
    Out[2]: <map at 0x7f4d393c3ac8>
    In [3]: list(lines)
    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    <ipython-input-3-2666a44c63b5> in <module>()
    ----> 1 list(lines)
    
    ValueError: I/O operation on closed file.
    

    If you want a version-safe implementation of this you need to either convert the generator to a list

    lines = list(map(str.strip, f))
    

    or just use a list comprehension

    lines = [l.strip() for l in f]