'with' and code blocks

» In which yours truly finally takes a look at Python's new with statement, and compares the use of code blocks in three languages, of which Python is probably the least dynamic. (Gasp!)

Python 2.5 has been out for a while now, but to be honest, I have hardly looked at it. I didn't keep tabs on its development, and didn't even notice when it was released until days later.

There are many reasons for this, which shall be discussed some other time. For now, let's focus on the fact that I did take a look at it today, and at the new with statement in particular.

After reading the relevant section in What's new in Python 2.5, I wondered if this idea could not be expressed in existing (pre-2.5) Python. After some tinkering, I came up with the following:

class With(object):
    def __init__(self, *args):
        self.args = args
        self.count = 0
        self.obj = self.__enter__()
    def __enter__(self):
        # return something that will become self.obj
        pass
    def __exit__(self):
        # do something with self.obj
        pass
    def next(self):
        self.count += 1
        if self.count <= 1:
            return self.obj
        else:
            try:
                self.__exit__()
            finally:
                raise StopIteration
    def __iter__(self):
        return self

class WithFile(With):
    def __enter__(self):
        print "Opening file..."
        return open(self.args[0], 'r')
    def __exit__(self):
        print "Closing file..."
        self.obj.close()

for f in WithFile("test1.txt", "r"):
    for line in f:
        print "-", f

This is of course a gross abuse of the for statement, but never mind that. ;-) At first glance it seems to work. The __enter__ method is called before the code block is executed, and __exit__ afterwards.

What's more interesting is that it has a problem that might not be immediately obvious: if an error occurs in the code block, __exit__ is never called.

After some googling, I found that Ryan Tomayko already pointed this out a year and a half ago. Like I said, I'm late to the party. ;-)

Anyway, by doing this little exercise, I learned something valuable about the real with statement, which solves this problem.

But that's not all. Ruby doesn't have this problem either, because its code blocks are first-class citizens. Thus, Ruby's equivalent of "with" would call the code block and be able to intercept any exceptions, and handle them appropriately. By contrast, the Python code listed earlier has no notion about "its" code block. With nor WithFile is able to access it, because there simply is no such concept as passing a code block in Python. In order to do that, one must wrap it in a function.

This is still the same when you use 2.5's with -- there's still no way to access the code block. You cannot, for example, run the code multiple times, or choose to skip it, or pass it to a function, or whatever. In fact, __enter__ and __exit__ work around it. The "context manager" object may affect code executed before and after the code block, but not the code block itself.

Ruby is not the only language that solves this problem. Io is another, and it does it in such a way that makes Ruby's code blocks seem almost clumsy. I will write more about this later (when I've finished downloading the latest version of Io over this !@#$% satellite connection). For now, suffice to say that in an Io method, lines of code can be passed as regular parameters, and the method may decide what to do with it. This way, constructs such as if, while and for can be written in pure Io, to name a few things. Here's a snippet:

# while(condition, message)
a := 1
while(a <= 10,
    a print
    a = a + 1
)

An Io method that takes code blocks can do many powerful and dynamic things with them... of course it can execute them at will (or choose not to, in the case of conditionals), but it may also pass them to other methods, inspect them, change the context/namespace in which they are executed, etc.

More about Io later... this is not the time for an Io primer, for now I merely wanted to point out the differences between the handling of code blocks in Python, Ruby and Io. But if you want to know more, check out the Io website or my (incomplete and possibly out-of-date) notes.