Monday, October 1, 2012

Lazy Initialization, Memoization, and Fun with Python Magic Methods

In this blog, we have some fun creating a class that implements the mapping protocol. The class also provides a simple example of the techniques of lazy initialization and memoization.

Let's say we have a configuration file, planets.conf that lists standard attributes for each planet in the solar system.

planets.conf


[Mercury]
orbit = 57910000
diameter = 4880
mass = 3.30e23

[Venus]
orbit = 108200000
diameter = 12103.6
mass = 4.869e24

[Earth]
orbit = 149600000
diameter = 12756.3
mass = 5.972e24
satellites = Moon

[Mars]
orbit = 227940000
diameter = 6794
mass = 6.4219e23
satellites = Phobos Deimos
.
.
.

Let's now create a structure-like class, called Planet, that encapsulates the attributes of planet name, orbit, diameter, mass, and satellites.

class Planet:
    def __init__(self, name, orbit, diameter, mass, satellites = []):
        self._name = name
        self._orbit = orbit
        self._diameter = diameter
        self._mass = mass
        self._satellites = satellites

    def __str__(self):
        return '%s%s\torbit: %.2f%s\tdiameter: %.2f%s\tmass: %g%s' +
            '\tsatellites: %s%s' \
            % (self._name, os.linesep,  \
            self._orbit, os.linesep, \
            self._diameter, os.linesep, \
            self._mass, os.linesep, \
            ', '.join(self._satellites), os.linesep)
    @property
    def name(self): return self._name
    @property
    def orbit(self): return self._orbit
    @property
    def diameter(self): return self._diameter
    @property
    def mass(self): return self._mass
    @property
    def satellites(self): return self._satellites

Next, we create a dictionary-like class, called Planets, that manages the creation of each Planet object.

class Planets:
    _NUM_PLANETS = 9
    _PLANET_NAMES = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn',
    'Uranus', 'Neptune', 'Pluto']

    def __init__(self):
        self._planets = {}
        self._parser = SafeConfigParser()
        self._parser.read('planets.conf')

    def __len__(self):
        return self._NUM_PLANETS

    def __getitem__(self, key):
        if key in self._planets:
            return self._planets[key]
        if not self._parser.has_section(key):
            raise KeyError(key)
        orbit = self._parser.getfloat(key, 'orbit')
        diameter = self._parser.getfloat(key, 'diameter')
        mass = self._parser.getfloat(key, 'mass')
        satellites = []
        if self._parser.has_option(key, 'satellites'):
            satellites = self._parser.get(key, 'satellites').split()
        self._planets[key] = Planet(key, orbit, diameter, mass, satellites)
        return self._planets[key]

In particular, if a user performs the following operations:

p = Planets()
earth = p['Earth']

then Planets will read the statistics for planet Earth from planets.conf, create a new Planet that represents Earth, and add this planet to its private dictionary of planets (self._planets). Thus, the operation of constructing a planet from a configuration file is only performed if that planet is accessed. On subsequent accesses of that planet, Planets returns the memoized Planet object.

Note that, for fun, we have Planets implement the __len__ and __getitem__ magic methods, thereby fulfilling the protocol of an immutable mapping.

The entire program is listed below.

lazy_planets.py


#!/usr/bin/env python

from ConfigParser import SafeConfigParser
import os

__metaclass__ = type # new-style classes

class Planet:
    def __init__(self, name, orbit, diameter, mass, satellites = []):
        self._name = name
        self._orbit = orbit
        self._diameter = diameter
        self._mass = mass
        self._satellites = satellites

    def __str__(self):
        return '%s%s\torbit: %.2f%s\tdiameter: %.2f%s\tmass: %g%s' +
            '\tsatellites: %s%s' \
            % (self._name, os.linesep,  \
            self._orbit, os.linesep, \
            self._diameter, os.linesep, \
            self._mass, os.linesep, \
            ', '.join(self._satellites), os.linesep)
    @property
    def name(self): return self._name
    @property
    def orbit(self): return self._orbit
    @property
    def diameter(self): return self._diameter
    @property
    def mass(self): return self._mass
    @property
    def satellites(self): return self._satellites

class Planets:
    _NUM_PLANETS = 9
    _PLANET_NAMES = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn',
    'Uranus', 'Neptune', 'Pluto']

    def __init__(self):
        self._planets = {}
        self._parser = SafeConfigParser()
        self._parser.read('planets.conf')

    def __len__(self):
        return self._NUM_PLANETS

    def __getitem__(self, key):
        if key in self._planets:
            return self._planets[key]
        if not self._parser.has_section(key):
            raise KeyError(key)
        orbit = self._parser.getfloat(key, 'orbit')
        diameter = self._parser.getfloat(key, 'diameter')
        mass = self._parser.getfloat(key, 'mass')
        satellites = []
        if self._parser.has_option(key, 'satellites'):
            satellites = self._parser.get(key, 'satellites').split()
        self._planets[key] = Planet(key, orbit, diameter, mass, satellites)
        return self._planets[key]

if __name__ == '__main__':
    p = Planets()
    print('Number of planets: %d' % (len(p), ))
    earth = p['Earth']
    print(earth)
    mars = p['Mars']
    print('The moons of mars are: %s' % (' ,'.join(mars.satellites), ))
    try: ceres = p['Ceres']
    except KeyError, e:
        print 'Ceres is not a planet!'
Here is the output of the program:
$ ./lazy_planets.py 
Number of planets: 9
Earth
 orbit: 149600000.00
 diameter: 12756.30
 mass: 5.972e+24
 satellites: Moon

The moons of mars are: Phobos ,Deimos
Ceres is not a planet!

No comments:

Post a Comment