Software Carpentry logo

Python Advanced Object-Oriented Programming

April 24, 2010: We are pleased to announce that Version 4 of this course is now under development. For updates and an early peek at the content, please check out the Software Carpentry blog at http://www.software-carpentry.org/blog/.

1) Introduction

2) You Can Skip This Lecture If...

3) Length

class Recent(object):

    def __init__(self, number=3):
        self.number = number
        self.items = []

    def __str__(self):
        return str(self.items)

    def add(self, item):
        self.items.append(item)
        self.items = self.items[-self.number:]

    def __len__(self):
        return len(self.items)

if __name__ == '__main__':
    history = Recent()
    for era in ['Permian', 'Triassic', 'Jurassic', 'Cretaceous', 'Tertiary']:
        history.add(era)
        print len(history), history
1 ['Permian']
2 ['Permian', 'Triassic']
3 ['Permian', 'Triassic', 'Jurassic']
3 ['Triassic', 'Jurassic', 'Cretaceous']
3 ['Jurassic', 'Cretaceous', 'Tertiary']

4) Overloading Operators

class Recent(object):

    def __add__(self, item):
        self.items.append(item)
        self.items = self.items[-self.number:]
        return self

    def __init__(self, number=3):
        self.number = number
        self.items = []

    def __str__(self):
        return str(self.items)

    def __len__(self):
        return len(self.items)

if __name__ == '__main__':
    history = Recent()
    for era in ['Permian', 'Triassic', 'Jurassic', 'Cretaceous', 'Tertiary']:
        history = history + era
        print len(history), history
1 ['Permian']
2 ['Permian', 'Triassic']
3 ['Permian', 'Triassic', 'Jurassic']
3 ['Triassic', 'Jurassic', 'Cretaceous']
3 ['Jurassic', 'Cretaceous', 'Tertiary']

5) Commutativity

6) Other Special Methods

7) Example: Sparse Vector

8) How Long is a Sparse Vector?

v = SparseVector() # all values initialized to 0.0
v[27] = 1.0        # length is now 28
v[43] = 1.0        # length is now 44
v[43] = 0.0        # is the length still 44, or 28?

9) Vector Behavior

class SparseVector(object):
    '''Implement a sparse vector.  If a value has not been set
    explicitly, its value is zero.'''

    def __init__(self):
        '''Construct a sparse vector with all zero entries.'''
        self.data = {}

    def __len__(self):
        '''The length of a vector is one more than the largest index.'''
        if self.data:
            return 1 + max(self.data.keys())
        return 0

    def __getitem__(self, key):
        '''Return an explicit value, or 0.0 if none has been set.'''
        if key in self.data:
            return self.data[key]
        return 0.0

    def __setitem__(self, key, value):
        '''Assign a new value to a vector entry.'''
        if type(key) is not int:
            raise KeyError, 'non-integer index to sparse vector'
        self.data[key] = value

10) Dot Product

    def __mul__(self, other):
        '''Calculate dot product of a sparse vector with something else.'''

        result = 0.0
        for k in self.data:
            result += self.data[k] * other[k]
        return result

    def __rmul__(self, other):
        return self.__mul__(other)

11) Addition

    def __add__(self, other):
        '''Add something to a sparse vector.'''

        # Initialize result with all non-zero values from this vector.
        result = SparseVector()
        result.data.update(self.data)

        # If the other object is also a sparse vector, add non-zero values.
        if isinstance(other, SparseVector):
            for k in other.data:
                result[k] = result[k] + other[k]

        # Otherwise, use brute force.
        else:
            for i in range(len(other)):
                result[i] = result[i] + other[i]

        return result

    # Right-hand add does the same thing as left-hand add.
    __radd__ = __add__

12) Testing

if __name__ == '__main__':

    x = SparseVector()
    x[1] = 1.0
    x[3] = 3.0
    x[5] = 5.0
    print 'len(x)', len(x)
    for i in range(len(x)):
        print '...', i, x[i]

    y = SparseVector()
    y[1] = 10.0
    y[2] = 20.0
    y[3] = 30.0

    print 'x + y', x + y
    print 'y + x', y + x

    print 'x * y', x * y
    print 'y * x', y * x

    z = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5]

    print 'x + z', x + z
    print 'x * z', x * z
    print 'z + x', z + x
len(x) 6
... 0 0.0
... 1 1.0
... 2 0.0
... 3 3.0
... 4 0.0
... 5 5.0
x + y [0.0, 11.0, 20.0, 33.0, 0.0, 5.0]
y + x [0.0, 11.0, 20.0, 33.0, 0.0, 5.0]
x * y 100.0
y * x 100.0
x + z [0.0, 1.1, 0.2, 3.3, 0.4, 5.5]
x * z 3.5
z + x [0.0, 1.1, 0.2, 3.3, 0.4, 5.5]

13) Static Data Members

class Counter(object):

    num = 0     # Number of Counter objects created.

    def __init__(self, name):
        Counter.num += 1
        self.name = name

if __name__ == '__main__':
    print 'initial count', Counter.num
    first = Counter('first')
    print 'after creating first object', Counter.num
    second = Counter('second')
    print 'after creating second object', Counter.num
initial count 0
after creating first object 1
after creating second object 2

14) Static Methods

class Experiment(object):

    already_done = {}

    @staticmethod
    def get_results(name, *params):
        if name in Experiment.already_done:
            return Experiment.already_done[name]
        exp = Experiment(name, *params)
        exp.run()
        Experiment.already_done[name] = exp
        return exp

    def __init__(self, name, *params):
        self.name = name
        self.params = params

    def run(self):
        pass # would do something in a real program

if __name__ == '__main__':
    first = Experiment.get_results('anti-gravity')
    second = Experiment.get_results('time travel')
    third = Experiment.get_results('anti-gravity')
    print 'first ', id(first)
    print 'second', id(second)
    print 'third ', id(third)
first  5120204
second 5120396
third  5120204

15) Design Patterns

16) The Singleton Pattern

17) Singleton Implementation

class AntennaClass(object):
    '''Singleton that controls a radio telescope.'''

    # The unique instance of the class.
    instance = None

    # The constructor fails if an instance already exists.
    def __init__(self, max_rotation):
        assert AntennaClass.instance is None, 'Trying to create a second instance!'
        self.max_rotation = max_rotation
        AntennaClass.instance = self

# Make the creation function look like a class constructor.
def Antenna(max_rotation):
    '''Create and store an AntennaClass instance, or return the one
    that has already been created.'''
    if AntennaClass.instance:
        return AntennaClass.instance
    return AntennaClass(max_rotation)

18) Demonstration

first = Antenna(23.5)
print 'first instance:', id(first)
second = Antenna(47.25)
print 'second instance:', id(second)
first instance: 10685200
second instance: 10685200

19) The Visitor Pattern

20) Visitor Implementation

class NestedListVisitor(object):
    '''Visit each element in a list of nested lists.'''

    def __init__(self, data):
        '''Construct, but do not run.'''
        assert type(data) is list, 'Only works on lists!'
        self.data = data

    def run(self):
        '''Iterate over all values.'''
        self.recurse(self.data)

    def recurse(self, current):
        '''Loop over a particular list or sub-list (not meant
        to be called by users).'''
        if type(current) is list:
            for v in current:
                self.recurse(v)
        else:
            self.visit(current)

    def visit(self, value):
        '''Users should fill this method in.'''
        pass

21) Demonstration

class MaxOfN(NestedListVisitor):

    def __init__(self, data):
        NestedListVisitor.__init__(self, data)
        self.max = None
        self.count = 0

    def visit(self, value):
        self.count += 1
        if self.max is None:
            self.max = value
        else:
            self.max = max(self.max, value)

test_data = [['gold', 'lead'], 'zinc', [['silver', 'iron'], 'mercury']]
test = MaxOfN(test_data)
test.run()
print 'max:', test.max
print 'count:', test.count
max: zinc
count: 6

22) The Abstract Factory Pattern

23) Abstract Factory Builder

class AbstractFamily(object):
    '''Builders for particular families derive from this.'''

    def __init__(self, family):
        self.family = family

    def get_name(self):
        return self.name

    def make_controller(self):
        raise NotImplementedError('make_controller missing')

    def make_configuration_panel(self):
        raise NotImplementedError('make_configuration_panel missing')

24) Abstract Factory Manager

class FactoryManager(object):
    '''Manage builders by family.'''

    def __init__(self, current_family=None):
        self.builders = {}
        self.family = current_family

    def set_family(self, family):
        assert family, 'Empty family'
        self.family = family

    def add(self, builder):
        name = builder.get_name()
        self.builders[name] = builder

    def make_controller(self):
        self._check_state()
        return self.builders[self.family].make_controller()

    def make_configuration_panel(self):
        self._check_state()
        return self.builders[self.family].make_configuration_panel()

    def _check_state(self):
        assert self.family, 'No family specified'
        assert self.family in self.builders, 'Unknown family:', self.family

25) Demonstration

factory = FactoryManager()
factory.add(RCT100Factory())
factory.add(Subalta4CFactory())
factory.set_family('Subalta4C')
controller = factory.make_controller()
configuration_panel = factory.make_configuration_panel()

26) The Command Pattern

27) Base Command Class

class AbstractCommand(object):
    '''Base class for commands.'''
    def is_undoable(self):
        return False # by default, can't undo/redo operations
    def do(self, robot):
        raise NotImplementedError("Don't know how to do %s" % self.name)
    def undo(self, robot):
        pass
    def redo(self, robot):
        pass

28) A Particular Command

class MoveCommand(AbstractCommand):
    '''Move the robot arm.'''

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def is_undoable(self):
        return True

    def do(self, robot):
        robot.translate(self.x, self.y, self.z)

    def undo(self, robot):
        robot.translate(-self.x, -self.y, -self.z)

    def redo(self, robot):
        self.do(robot)

29) Demonstration

robot = Robot()
commands = [MoveCommand(5.0, 2.0, 2.3),
            RotateCommand(-90.0, 0.0, 0.0),
            MoveCommand(1.0, 2.0, 2.0),
            CloseHandCommand()]
for c in commands:
    c.do(robot)

30) A Few Others

31) Summary