[Tutor] methods and functions

Neil Schemenauer nas-pytut@python.ca
Fri Jun 13 13:34:06 2003


Let me try to explain the zen of object oriented programming (IMHO).
First, forget about inheritance; it's handy for sharing code but it's
not the core idea of OO programming.

Imagine programming without methods or classes.  All you have are
functions and strutures (i.e. records or, in Python, classes with no
methods).  Here's some example code using that style:

    TYPE_CIRCLE = 1
    TYPE_RECTANGLE = 2

    class Circle:
        pass

    def make_circle(position, radius):
        c = Circle()
        c.type = TYPE_CIRCLE
        c.position = position
        c.radius = radius
        c.draw = draw_circle
        return c

    class Rectangle:
        pass

    def make_rectangle(...):
        r = Rectangle()
        r.type = TYPE_CIRCLE
        ...
    
    def draw_circle(c):
        ...

    def draw_rectangle(r):
        ...

    def main():
        ...
        for shape in set_of_shapes:
            if shape.type is TYPE_CIRCLE:
                draw_circle(shape)
            elif shape.type is TYPE_RECTANGLE:
                draw_rectangle(shape)
            else:
                raise TypeError, "can't draw shape %r" % shape
        ...

    main()

Somewhere in our program we want to draw a set of shapes.  Some could be
circles, others could be rectangles.  Having to check what kind of shape
we have whenever we want to draw it is a pain, especially if we have to
draw it in different places in the program.  Duplicated code is evil so
let's make a draw() function to prevent the duplication:

    if draw(shape):
        if shape.type is TYPE_CIRCLE:
            draw_circle(shape)
        elif shape.type is TYPE_RECTANGLE:
            draw_rectangle(shape)
        else:
            raise TypeError, "can't draw shape %r" % shape

Now drawing shapes is easy, just call the draw() function.

    for shape in set_of_shapes:
        draw(shape)

The draw() function is one way of getting polymorphic behavior.  In OO
lingo, polymorphic means that the function does different things
depending on the type it is operating with.

Implementin polymorphism this way is pretty tedious.  Imagine that we
have many different types of shapes we want to draw.  Being clever
programmers, we realize that we can store a reference to the proper draw
function on the struture.  E.g.

    def make_circle(position, radius):
        c = Circle()
        c.position = position
        c.radius = radius
        c.draw = draw_circle # <- new addition
        return c

If we do this for all our shapes we can now write our draw() function
like this:

    def draw(shape):
        shape.draw(shape)

That's a an improvement and is essentially how people write OO style
code in non-OO languages like C.  Note that we don't need the
TYPE_* constants and that the code will be more efficient if we have
many types (there will be no long if/elif block).  Let's see how an OO
language like Python makes things easier.  Normal Python code would look
like this:

    class Circle:
        def __init__(self, position, radius):
            self.position = position
            self.radius = radius

        def draw(self):
            ...

    ...

    def main():
        ...
        for shape in set_of_shapes:
            shape.draw()
        ...

Python does a few things under the covers to make this work.  Every
instance of Circle points to its class via the __class__ attribute.
The class object has the attributes __init__ and draw, both referring to
function objects.  Circle(p, r) creates a new circle instance.  It does
this by first creating an empty instance and then calling __init__(p, r)
on the empty instance.  The __init__() method works the same as the
draw() but I'll explain how methods work by using draw() as the example.

Say we have a circle instance c (created by Circle(...)).  To call the 
draw method we write:

    c.draw()

The call is done by first looking up the draw attribute and then calling
it.   When looking up draw, if the c instance has a draw attribute it
will be returned directly.  If the c instance does not have a
draw attribute, as in this case, Python does not give up but instead
follows the __class__ attribute of the c instance and looks in that
object for a draw attribute.  It finds the draw unbound method
(essentially a function).  Now for the magic.  Because the draw method
is being accessed through an instance, Python wraps it in another object
to make it a bound method.

You can try this if you like:

    >>> class Circle:
    ...    def draw(self):
    ...       pass
    ... 
    >>> Circle.draw
    <unbound method Circle.draw>
    >>> c = Circle()
    >>> c.draw
    <bound method Circle.draw of <__main__.Circle instance at 0x816470c>>
    >>> c.__class__
    <class __main__.Circle at 0x81640ec>
    >>> c.__class__.draw
    <unbound method Circle.draw>

A bound method behaves like a function but prepends the instance to the
list of arguments.  That is, calling c.draw() has exactly the same
effect as calling Circle.draw(c).  The important thing to note is that
the first form allows for polymorphism.  The caller does not have the
know the type of c in order to call the right draw() method.

Polymorphism is really the central idea of OO programming.  Methods make
is easy to implement polymorphism.  That's the difference between
functions and methods.

  Neil