Managing contexts - Python vs hy

| categories: python, hylang | tags:

A common pattern we have in running molecular simulations is to temporarily change to a new directory, do some stuff, and then change back to the directory, even if something goes wrong and an exception is raised. Here we examine several approaches to handling this in Python.

1 a try/except/finally approach

A way to handle this is with a try/except/finally block in Python. Here we illustrate the idea. Nothing fancy happens for the exception here, other than we do get back to the original directory before the program ends. There is nothing wrong with this, but it is not that reusable, and has a lot of places to make sure the right thing happens.

import os
print(os.getcwd())
try:
    cwd = os.getcwd()
    os.chdir('/tmp')
    f = open('some-file', 'w')
    f.write('5')
    print(os.getcwd())
    1 / 0
except:
    pass
finally:
    f.close()
    os.chdir(cwd)

print(os.getcwd())

print(open('/tmp/some-file').read())
/Users/jkitchin/Dropbox/python/hyve
/private/tmp
/Users/jkitchin/Dropbox/python/hyve
5

2 A Python context manager

A more sophisticated way to handle this in Python is a context manager. We create a context manager here called cd that does the same thing. The context manager is longer, but we would presumably put this in module and import it. This allows us to to the same thing in a lot less code afterwards, and to reuse this pattern. We also use the built in context manager for opening a file. This is for the most part a syntactical sugar for the code above.

import contextlib

@contextlib.contextmanager
def cd(wd):
    import os
    cwd = os.getcwd()
    print('Started in {}'.format(os.getcwd()))
    os.chdir(wd)
    print('Entered {}'.format(os.getcwd()))
    try:
        yield
    except:
        pass
    finally:
        os.chdir(cwd)
        print('Entered {}'.format(os.getcwd()))

##################################################################
with cd('/tmp'):
    with open('some-other-file', 'w') as f:
        f.write('5')
    1 / 0

print(open('/tmp/some-other-file').read())
Started in /Users/jkitchin/Dropbox/python/hyve
Entered /private/tmp
Entered /Users/jkitchin/Dropbox/python/hyve
5

3 A python decorator

Here is an example of doing something like this with a decorator. I don't do this too often, but this does more or less the same thing. It does eliminate a with statement and provide some context to do work in. The overall indentation is identical to the context manager we looked at previously because we have to wrap our code in a function to delay its execution, which we have to ask for with f(). A downside of this is f is always decorated now. I am not sure you can undecorate it.

import os

def cd(wd):
    def outer(func):
        def inner(*args):
            cwd = os.getcwd()
            print('Started in {}'.format(os.getcwd()))
            os.chdir(wd)
            print('entered {}'.format(os.getcwd()))
            try:
                return func(*args)
            except:
                pass
            finally:
                os.chdir(cwd)
                print('entered {}'.format(os.getcwd()))
        return inner
    return outer
##################################################################

@cd('/tmp')
def f():
    with open('decorated-file', 'w') as f:
        f.write("5")
    1 / 0

f()
print(open("/tmp/decorated-file").read())
Started in /Users/jkitchin/Dropbox/python/hyve
entered /private/tmp
entered /Users/jkitchin/Dropbox/python/hyve
5

4 A hy macro approach

hy gives us yet another option: a macro. We can use a macro to construct the context for us by building up the try/except/finally code we used above. The indentation used here is just for readability.

(defmacro cd [wd &rest body]
  `(do
    (import os)
    (print "started in " (os.getcwd))
    (let [cwd (os.getcwd)]
      (try
       (do (os.chdir ~wd)
           (print "entered " (os.getcwd))
           ~@body)
       (except [e Exception] nil)
       (finally
        (os.chdir cwd)
        (print "entered " (os.getcwd)))))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(cd "/tmp"
    (with [f (open "some-hy-file" "w")]
          (.write f "5")
          (/ 1 0)))

(print (.read (open "/tmp/some-hy-file")))
started in  /Users/jkitchin/Dropbox/python/hyve
entered  /private/tmp
entered  /Users/jkitchin/Dropbox/python/hyve
5

The results are the same, even down to the reduced number of lines! But the mechanism that achieves that is different. In this example, we subtly changed the syntax that was possible, eliminating the need for one of the "with" statements. This is only possible with this kind of macro construction as far as I know. It still is not a game changer of programming, but does illustrate some new ways to think about writing these programs. It is not necessary to wrap the code into a function just to delay it from being executed.

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

Discuss on Twitter