Python Strategy Pattern

The strategy pattern can be a nice way to improve flexibility when accessing external resources. For example an application might have images referenced in Flickr and a relational database. You want to be able to search both places, but you also want your API to be the same.

There are lots of ways to do this, since in Python functions can be passed around. This StackOverflow page has some details.

I’ll show two approaches here that I think are the easiest to read. The first is a simple interface / abstract class (loosely same thing in Python due to duck typing) with concrete classes that do the work. The second is a more complex setup that allows for a default strategy and the ability to maintain state on the top level strategy object. The code is here for download: Example 1, Example 2.

Basic Method – Simple Inheritance:

This should look very familiar to Java/C# programmers:

class ImageFinder:
    """ Inteface / Abstract Class concept for readability. """

    def find(self, image):
        # explicitly set it up so this can't be called directly
        raise NotImplementedError('Exception raised, ImageFinder is supposed to be an interface / abstract class!')

class ImageFinderFlickr(ImageFinder):
    ''' Locates images in flickr'''

    def find(self, image):
        # in reality, query Flickr API for image path
        return "Found image in Flickr: " + image


class ImageFinderDatabase(ImageFinder):
    ''' Locates images in database. '''
    def find(self, image):
        #in reality, query database for image path
        return "Found image in database: " + image
    
    
    
if __name__ == "__main__" :

    finderBase = ImageFinder()
    finderFlickr = ImageFinderFlickr()
    finderDatabase = ImageFinderDatabase()

    try:
        #this is going to blow up!
        print finderBase.find('chickens')
    except NotImplementedError as e:
        print "The following exception was expected:"
        print e
        

    print finderFlickr.find('chickens')
    print finderFlickr.find('rabbits')
    print finderDatabase.find('dogs')
    print finderDatabase.find('cats')  

Outputs:

$ python strategy_ex1.py
The following exception was expected:
Exception raised, ImageFinder is supposed to be an interface / abstract class!
Found image in Flickr: chickens
Found image in Flickr: rabbits
Found image in database: dogs
Found image in database: cats

Secondary Method – Default Strategy with Stateful Object:

This approach allows us to track the number of times the method has been called. Granted this is a trivial example but it demonstrates one way to go beyond the simple example above.

class ImageFinder(object):
    """ 
    In this example the base object ImageFinder keeps a copy
    of the concrete class (strategy).  You may also set
    a default strategy to use which might be convienient.
    In this case it is set to None which forces the caller
    to supply a concrete class.
        
    The concrete find method is supplied with an instance of
    this object so its state can be tracked.
    """
    
    def __init__(self, strategy=None):
        self.action = None
        self.count = 0
        if strategy:
            #get a handle to the object
            self.action = strategy()
    
    def find(self, image):
        if(self.action):
            self.count += 1
            return self.action.find(image, self)
        else: 
            raise UnboundLocalError('Exception raised, no strategyClass supplied to ImageFinder!')

class ImageFinderFlickr(object):
    ''' Locates images in Flickr. '''

    def find(self, image, instance):
        # in reality, query Flickr API for image path
        return "Found image in Flickr: " + image + ", search #" + str(instance.count)


class ImageFinderDatabase(object):
    ''' Locates images in database. '''
    def find(self, image, instance):
        #in reality, query database for image path
        return "Found image in database: " + image + ", search #" + str(instance.count)
    
    
if __name__ == "__main__" :

    finderBase = ImageFinder()
    #these next two look a little convuluted don't they?
    #useage is a little more verbose in example 2 vs example 1
    #however, benefits in include a default strategy type, and ability to track state
    finderFlickr = ImageFinder(strategy=ImageFinderFlickr)
    finderDatabase = ImageFinder(strategy=ImageFinderDatabase)

    try:
        #this is going to blow up!
        print finderBase.find('chickens')
    except Exception as e:
        print "The following exception was expected:"
        print e
        

    print finderFlickr.find('chickens')
    print finderFlickr.find('bugs bunny')
    print finderFlickr.find('tweety')
    print finderDatabase.find('dogs')
    print finderDatabase.find('cats')
    print finderDatabase.find('rabbits')

Outputs:

$ python strategy_ex2.py
The following exception was expected:
Exception raised, no strategyClass supplied to ImageFinder!
Found image in Flickr: chickens, search #1
Found image in Flickr: bugs bunny, search #2
Found image in Flickr: tweety, search #3
Found image in database: dogs, search #1
Found image in database: cats, search #2
Found image in database: rabbits, search #3

To make the Flickr strategy default:

#change: def __init__(self, strategy=None):
#to
def __init__(self, strategy=ImageFinderFlickr):

Other Thoughts:

Even if you know there will only be one implementation for the foreseeable future, it can be helpful to create a ‘Fake’ implementation that returns hard coded test data. This also gets you thinking in terms of interfaces, which is a good thing. The fake implementation is only wired up temporarily. This is useful if different team members are working on different aspects of the system. Once the interface and return type are agreed upon team members may work on the back end and front end without interfering with each other. A fake implementation can also be used to show the customer something quickly e.g. – “This is just test data, but here is how it will work.”

If you would like to download the examples in this article: Example 1, Example 2.

This entry was posted in Code and tagged , . Bookmark the permalink.

Comments are closed.