Home » python » Decorators in Python and Syncing Objects

Decorators in Python and Syncing Objects

1. Functions returning functions

Today we’re going to learn about decorators, and do an interesting example showing how objects in Python can communicate with each other. I first heard about decorators from a very interesting post on dynamic programming, where you give functions a ‘memory’. so that they remember the computations they do given particular arguments, and so avoid repeated calculuations -very useful for improving the efficiency of recursive functions!

The first thing to note is that functions are objects too, in fact, practically everything is an object in Python. So we can pass a function as an argument to a function, do some stuff involving that function, and then return a new function. Something you might want to do, for debugging purposes say, is have the console print out the name of a function every time it is executed, which we do like this:

def announce_function(input_function):
    def new_function():
        print 'Executing function: %s' %input_function.func_name
        input_function()
    return new_function

Notice how we are returning a function, not simply calling it. Now let’s think of a function to announce. How about telling the time? It’s pretty easy to do using the time module, as follows:

import time
def tell_time():
    print "The time is: ", time.asctime(time.localtime(time.time()))

tell_time()

The time is:  Mon Aug 19 19:18:01 2013

the ‘time.asctime’ part just formats the time tuple nicely.

Now let’s try out our announcer:

announce_function(tell_time)()

Executing function: tell_time
The time is:  Mon Aug 19 19:18:01 2013

so it works! That’s fine if you only want to announce specific call of a function, but what if you want to announce every call of the function? Or of every function in your code? It seems like a lot of typing! But Python has a handy syntax doing it automagically:

@announce_function
def tell_time():
    print "The time is: ", time.asctime(time.localtime(time.time()))

tell_time()

Executing function: tell_time
The time is:  Mon Aug 19 19:18:01 2013

Neat! Now you may be thinking: hey that’s nice and all, but what about the arguments? We really want our decorator to be seperate from the function it decorates, and so it shouldn’t need to know anything about what kinds and numbers of arguments they expect, it should work for any function.

2. Args and Kwargs

This allows you to grab the arguments from a function and pass them to a new one. Let’s see how this works by example.

def argument_show(*args, **kwargs):
    print 'Printing args', args
    print 'Printing kwargs ', kwargs

argument_show(1,2, 'cats', holiday='Christmas')

Printing args (1, 2, 'cats')
Printing kwargs  {'holiday': 'Christmas'}

You see that args refers to arguments that are passed by position to the function, and kwargs refers to keyword arguments that are named as they are passed.

Now let’s put this to work in a decorator that will work with any function, and create a new functon to showcase it.

def adding_list(list_to_add):
    print reduce(lambda x,y: x+y, list_to_add)

adding_list([1,2,3])

6

def announce_function(input_function):
    def new_function(*args,**kwargs):
        print 'Executing function: %s' %input_function.func_name
        input_function(*args,**kwargs)
    return new_function

@announce_function
def adding_list(list_to_add):
    print reduce(lambda x,y: x+y, list_to_add)

adding_list([1,2,3])

Executing function: adding_list
6

3. Communication between objects with decorators

Now we’re going to put this to work in a simple example that nonetheless demonstrates some very interesting ideas. Next time we’re going to start implementing graphs as objects, and one of the things I wanted to implement is the notion of a subgraph as an object. The nontrivial part of this is getting the two objects to communicate, for instance, when we delete an edge in a subgraph, naturally the edge should be deleted from the supergraph, and when we delete an edge in the supergraph that is also in the subgraph, the subgraph should also be informed of this. However when we create edges or vertices, the changes should travel up (to supergraphs), but not down (to subgraphs).

I thought this behaviour was worth looking at on its own, and so we abstract the essence of the problem to a toy example.

The specification is we want a car object with attribute colour. We also want car shops that have cars and buy and sell them. Furthermore we want to be able to create `subcarshops’ that have a subset of the cars with the following behaviours: when the subshop buys or sells, the supershop is updated, and when the supershop sells, but not buys, the subshop is updated.

This can be accomplished by using decorators that tell class methods how to update up and down the lattice of shops. They use a nifty function called `getattr’ that allows us to access class methods on the fly, by name. Let’s make these first.

def update_up(class_method):
    def inner(self, *args, **kwargs):
        method_name = class_method.func_name
        class_method(self, *args, **kwargs)
        for super_shop in self.super_shops:
            getattr(super_shop, method_name)(*args, **kwargs)
    return inner

def update_up_down(class_method):
    def inner(self, *args, **kwargs):
        method_name = class_method.func_name
        if class_method(self, *args, **kwargs):
            for super_shop in self.super_shops:
                getattr(super_shop, method_name)(*args, **kwargs)
            for sub_shop in self.sub_shops:
                getattr(sub_shop, method_name)(*args, **kwargs)
    return inner

These ping updates up and down until everybody in the whole lattice of shops knows about the changes. Just like in recursive functions, we have to give stopping conditions for the update_up_down so we don’t go on forever. This will all make sense when you see it in action:

class Car():
    def __init__(self, color):
        self.color = color

class CarsForSale(object):
    def __init__(self, list_cars=None, sub_shops=None, super_shops=None):
        self.available_cars = list_cars

        if sub_shops is None:
            self.sub_shops = []
        else:
            self.sub_shops=sub_shops

        if super_shops is None:
            self.super_shops = []
        else:
            self.super_shops=super_shops

        if len(self.super_shops)!= 0:
            for Super in self.super_shops:
                Super.add_sub_shop(self)

        if len(self.sub_shops)!= 0:
            for Sub in self.sub_shops:
                Sub.add_super_shop(self)

    @update_up_down
    def sell_car(self,car):
        if car not in self.available_cars:
            return False
        self.available_cars.remove(car)
        return True

    @update_up
    def buy_car(self,car):
        if car in self.available_cars:
            return
        self.available_cars.append(car)
        return

    def add_sub_shop(self, sub_shop):
        self.sub_shops.append(sub_shop)

    def add_super_shop(self, super_shop):
        self.super_shops.append(super_shop)

Now better put the code through its paces by testing the behaviours we were seeking.
Starting with making some red and blue cars, a car shop with these cars, and a subshop with all the red cars.

cars =[]
testcar = Car('blue')
for i in range(5):
    newcar = Car('red')
    cars.append(newcar)
    newcar = Car('blue')
    cars.append(newcar)
carShop = CarsForSale(list_cars=cars)
redCars = filter(lambda x: x.color=='red',carShop.available_cars)

subShop = CarsForSale(super_shops=[carShop], list_cars=redCars)
print 'Initial State:'
print [x.color for x in carShop.available_cars]
print [x.color for x in subShop.available_cars]

Initial State:
['red', 'blue', 'red', 'blue', 'red', 'blue', 'red', 'blue', 'red', 'blue']
['red', 'red', 'red', 'red', 'red']

print 'Selling a car in subshop:'
newcar = subShop.available_cars[0]
subShop.sell_car(newcar)
print [x.color for x in carShop.available_cars]
print [x.color for x in subShop.available_cars]

Selling a car in subshop:
['blue', 'red', 'blue', 'red', 'blue', 'red', 'blue', 'red', 'blue']
['red', 'red', 'red', 'red']

print 'Buying a car in subShop:'
redcar = Car('red')
subShop.buy_car(redcar)
print [x.color for x in carShop.available_cars]
print [x.color for x in subShop.available_cars]

Buying a car in subShop:
['blue', 'red', 'blue', 'red', 'blue', 'red', 'blue', 'red', 'blue', 'red']
['red', 'red', 'red', 'red', 'red']

print'Selling a Car in carShop, which is in the subshop:'
car = subShop.available_cars[0]
carShop.sell_car(car)
print [x.color for x in carShop.available_cars]
print [x.color for x in subShop.available_cars]

Selling a Car in carShop, which is in the subshop:
['blue', 'blue', 'red', 'blue', 'red', 'blue', 'red', 'blue', 'red']
['red', 'red', 'red', 'red']

Hurray it works!

Please speak up if you have any questions, comments, improvements, criticisms, profanities etc…


Advertisements

5 Comments

  1. […] time we are going to combine the lessons learned about objects and decorators in Python, and about graph theory, to represent graphs as […]

  2. […] in python programming, you can read about object orientated programming here, decorators in python here, and how to represent graphs in python here and here. Note that to produce plots of the graphs, you […]

  3. so it works! That’s fine if you only want to announce specific call of a function, but what if you want to announce every call of the function? Or of every function in your code? It seems like a lot of typing! But Python has a handy syntax doing it automagically:

    Sorry for being a grammar Nazi but should not it be automatically in place of automagically.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: