Codementor Events

How to make a Sandwich using Python Context Manager

Published Aug 16, 2016Last updated Jan 18, 2017
How to make a Sandwich using Python Context Manager

Most people shy away from the kitchen, not because cooking isn't fun, but because cleaning up after is tedious.

What if I told you that you could get cooking without having to explicitly cleanup? Hopefully, that should get you hooked!

For the purpose of this tutorial, we're going to be making a sandwich. Said sandwich will comprise of the following ingredients:

  • Bread
  • Bacon
  • Mayonnaise
  • Lettuce

For brevity, we're going to treat printing the action as an equivalent to actually doing it. We can define our tasks like so:

# the boilerplate tasks

def fetch_ingredient(name):
   print "Fetching {0}...".format(name)
   return

def shelve_ingredient(name):
   print "Shelving {0}...\n".format(name) # ignore the \n
   return
# don't pay too much attention to this function
def pluralize_token(token, quantity):
   if quantity > 1:
       token += 's'
   return token

# the core task at hand
def add_ingredient(name, quantity, token='piece'):
   token = pluralize_token(token, quantity)
   print "Adding {0} {1} of {2}...\n".format(
       quantity, token, name
   )
   return

Pretty self-explanatory right? Let's move on.

Now, if you had to make that sandwich you'd probably end up doing these tasks in order:

  • fetch_ingredient
  • add_ingredient
  • shelve_ingredient

Basically, it would look something like this:

def make_me_a_sandwich():
   fetch_ingredient('bread')
   add_ingredient('bread', 2, 'slice')

   fetch_ingredient('bacon')
   add_ingredient('bacon', 2, 'strip')

   fetch_ingredient('mayo')
   add_ingredient('mayo', 2, 'dab')

   fetch_ingredient('lettuce')
   add_ingredient('lettuce', 2)

   # can't forget to clean the kitchen
   shelve_ingredient('lettuce')
   shelve_ingredient('mayo')
   shelve_ingredient('bacon')
   shelve_ingredient('bread')

Phew! That seems like so much boilerplate to do something so simple.

We're basically looking to do away with the boring tasks and focus on our one fundamental task - add_ingredient.

Enter, context_managers. They contain the magic sauce that abstracts resource management, which aids you in writing clean code.

Python Context Manager

Python context manager

One of the most common context managers (which I'm sure you must've already dealt with) would be open.

with open('sample.txt', 'r') as source:
    # observe that we move into a nested block to work with source
    print source.read() 

# now that we're outside the block if we tried some kind of file i/o
# we'd get an exception, because the resource was already closed.

Surely there's some black magic at hand here. How do I even begin to grasp something like that?

Using with, we can call anything that returns a context manager (like the built-in open() function). We assign it to a variable using ... as <variable_name>. Crucially, the variable only exists within the indented block below the with statement. Think of with as creating a mini-function: we can use the variable freely in the indented portion, but once that block ends, the variable goes out of scope. When the variable goes out of scope, it automatically calls a special method that contains the code to clean up the resource.

So basically, using context managers, you can execute code on entering and exiting from the block.

There are a number of ways to create your own context manager. The most common way would be to define a class that implements these two methods:

__enter__ : In this, you place the code you would use in order to retrieve/open a resource and basically make it ready for consumption/utilisation. Note: Make sure that this function returns the resource in question!

__exit__: Here you would write the logic in order to restore/close/cleanup after utilising the resource.

Which would look something like this:

class Ingredient(object):

    def __init__(self, name, quantity=1, token=None):
        self.name = name
        self.quantity = quantity
        self.token = token or 'piece'
    
    def fetch_ingredient(self):
        print "Fetching {0.name}...".format(self)
        return

    def add_ingredient(self):
        token = self.pluralize_token()
        print "Adding {0.quantity} {1} of {0.name}...\n".format(self, token)
        return

    def pluralize_token(self):
        token = self.token
        if self.quantity > 1:
            token += 's'
        return token

    def shelve_ingredient(self):
        print "Shelving {0.name}...\n".format(self)
        return

    def __enter__(self):
        self.fetch_ingredient()
        return self

    def __exit__(self, *args):
        self.shelve_ingredient()
        return

As you can see, we are able to achieve encapsulation by defining the methods on the resource, and mandating the cleanup on __exit__.

This time, we'll make the sandwich using Ingredient.

def make_me_another_sandwich():
    with Ingredient(name="bread", quantity=2, token="slice") as bread:
        bread.add_ingredient()
        with Ingredient(name="bacon", quantity=2, token="strip") as bacon:
            bacon.add_ingredient()
            with Ingredient(name="mayo", quantity=2, token="dab") as tomato:
                tomato.add_ingredient()
                with Ingredient(name="lettuce", quantity=1) as lettuce:
                    lettuce.add_ingredient()
                    print '-' * 28
                    print "That's one helluva sandwich!"
                    print '-' * 28
                    print
    print "...Woah! Kitchen's all clean."

Pretty cool eh?

There's another convenient way of making context managers, using a more functional approach. In fact, there's a whole standard library module just for that. We just decorate a function using @contextmanager and voila!

To use it, decorate a generator function that calls yield exactly once. Everything before the call to yield is considered the code for enter(). Everything after is the code for exit()

Let's redefine Ingredient using this.

from contextlib import contextmanager

@contextmanager
def ingredient(name):
    fetch_ingredient(name)
    yield # return control to inner block where add_ingredient is called
    shelve_ingredient(name)

# usage
with ingredient('bread'):
    add_ingredient('bread', 2, 'slice')

Hopefully, by now you should have a good understanding of what context managers are and how they work. What I've shown you was a contrived yet simple illustration. As far as real-world use-cases go, context managers are used to remove the bloat(in general - make other developers' lives easy) & enforce cleanup when handling resources(think file descriptors, socket connections, etc). If you'd like to learn more, you definitely should give the contextlib module a peek.

Thanks for reading!

Discover and read more posts from Vishal Gowda
get started
post commentsBe the first to share your opinion
fkrv
8 years ago

Amazing tutorial…Thanks

Karthik Srivatsa
8 years ago

You lamb!

Vassiliy Taranov
8 years ago

“Flat is better than nested”

Vishal Gowda
8 years ago

You propose a fair point. You could always do this in those cases:

from contextlib import nested

with nested(A(), B(), C()) as (X, Y, Z):
do_something()

I didn’t want to include this there because it would deviate from the topic.
(EDIT: Which is why, I’ve included the link to the contextlib module at the bottom :) )

MaT
8 years ago

The solution with contextlib.nested is deprecated. From Python 3.1 (or 2.7), you can use this syntax:

with A() as a, B() as b, C() as c:
doSomething(a,b,c)

And from Python 3.3 there is also contextlib.ExitStack, which you can use for a list of context managers (for example if you don’t know the number beforehand):

with ExitStack() as stack:
for mgr in context_managers:
stack.enter_context(mgr)

PS: Disqus forum removes indentation, so you have to add it - one level for each line following the previous one.

Show more replies