Search code examples
pythoncoding-styleindentationwith-statementzen-of-python

zen of Python vs with statement - philosophical pondering


I don't intend to simply waste your time, but: has it occurred to you too, while using Python's with statement that it really is contrary to the 5th line of "The Zen of Python" that goes "Flat is better than nested"? Can any enlightened Python guru share me some of their insights on this?

(I always find that one more level of indentation pops up in my code every time I use with instead of f.close()... and it's not like I'm not gonna use try: ... finally: ... anyways and thus the benefits of with still elude me, even as I grow to like and understand Python more and more...)


@glglgl (sorry, I can't find a way to write code in comments): yes, but if you go the with way, your code becomes:

try:
    with file(...) as f:
        ...
except IOError:
    ...

...and using just with without the try is what people end up doing in the type of hacky "one use" code where they use f.close() instead of with anyways (which is bad because the file may not be closed if an exception is thrown before their f.close()), so for "hacky" code people just don't use with because, I don't know, I guess they just find it too "fancy" and for well structured code it doesn't bring any benefits anyways, so it seems to me there's no real world use case left for it... that was my pondering about really.


Solution

  • Yes, The Zen of Python states "Flat is better than nested", however it is not the only characteristic we care about; it also states "Simple is better than complex". The beauty of with is that it actually adheres to both of those principles as I will explain below.

    Any time you find yourself in philosophical pondering about a feature in Python it's probably worth looking up the Python Enhancement Proposals (PEPs) to read about the motivation behind the feature. In this case PEP 343 -- The "with" Statement says it up front in the abstract:

    This PEP adds a new statement "with" to the Python language to make it possible to factor out standard uses of try/finally statements.

    Factoring out try/finally statements makes the code simpler and more readable.

    PEP 343 goes deeper than providing some simplistic syntactic sugar, however. It establishes a context manager protocol:

    The expression immediately following the with keyword in the statement is a "context expression" as that expression provides the main clue as to the runtime environment the context manager establishes for the duration of the statement body.

    Using the context manager protocol, API writers can help hide complexity and ensure correct acquisition/release of resources in a multi-threaded context.

    But the real beauty of the with statement is shown in Example 12 of PEP 343 which explains that:

    A "nested" context manager that automatically nests the supplied contexts from left-to-right to avoid excessive indentation.

    Using the nested() context manager you can take code that looks like this:

    with a as x:
        with b as y:
            with c as z:
                # Perform operation
    

    and turn it into this:

    with nested(a, b, c) as (x, y, z):
                 # Perform operation
    

    Note that nested() was introduced in Python 2.5, but as of version 2.7 it is deprecated in favor of this multiple context manager syntactic form:

    with a as x, b as y, c as z:
                 # Perform operation
    

    Clearly not only is this simpler and more readable, but it is much more flat than nested. Thus, using with is following the path of 無爲 :)

    UPDATE: In response to comments on Simeon Visser's answer here is an example of when you might use multiple context managers to open more than one file at once, when you want to zip the contents of two (or more) files together such that if opening one of the files fails it will make the whole thing fail and properly close each file that was opened:

    from itertools import izip
    with open("/etc/passwd") as a, open("/etc/group") as b, open("/etc/shadow") as c:
        for lines in izip(a,b,c):
            print map(lambda x: x.split(':')[0], lines)
    

    Run this example twice; once as a root and once as normal user. Presuming you save this file as ziptogether.py first try invoking it as root with sudo python ziptogether.py and it will succeed, but invoking it as a normal user with python ziptogether.py will fail because you don't have permissions to read /etc/shadow. When it fails the context manager will ensure that the files that were successfully opened before the failure are properly closed when execution moves outside the scope of the with statement.