prototype-based inheritance in Python

Kragen Sitaker kragen at dnaco.net
Sat Sep 23 00:24:06 EDT 2000


Yesterday, on comp.lang.python, someone was playing around with adding
C#-like (Delphi-like, C++Builder-like, JavaBeans-like) properties to
Python in a few lines of code, using __getattr__ and __setattr__.

I'd had a conversation with Chris Olds a few weeks ago, speculating on
the feasibility of implementing Self-like prototype-based inheritance
in Python.  

So I was inspired to do it.  See test() at the bottom for one way to
use it; you should be able to use this Object class as a mixin, too,
but I haven't tried it.

It needs at least Python 1.5.2 to work; 1.5.1 chokes on a couple of
things.

I disclaim any copyright in this code.  If someone is going to have
license hassles with it down the line, it won't be from me, and I think
my employer would have a hard time claiming I wrote it as part of my
job. ;)

# Simple implementation of Self-like prototype-based inheritance.
# By Kragen Sitaker, 2000-09-22.
# Sorry if it's unpythonic or crude; I don't know Python very well.
#
# Ideally we'd like to be able to assign methods dynamically.
# This means our "methods" must really be functions.
# It would be nice if we could just use native Python method
# objects; unfortunately, they are harder to declare (you must do it at top
# level of a class) and suffer from restrictions intended to make them safer:
# they (apparently) can't be rebound to point to a new instance, and they
# (apparently) can't be called on instances that don't inherit from their
# real class.
# However, because of lambda arg binding and __call__able objects,
# it is possible to reimplement all of the standard method-call machinery in
# Python:

class Method:
    def __init__(self, instance, function):
        self.instance = instance
        self.function = function

    def __call__(self, *args):
        nargs = list(args)
        nargs[0:0] = [self.instance]
        return apply(self.function, nargs)

    def prototype_method_rebind(self, instance):
        return Method(instance, self.function)

# For now, we only handle inheritance for getting.
# Self handled it for setting, too; if you inherited an attribute from somebody,
# you setting that attribute would set it in the somebody, not in you.  That's
# unpythonic, so we don't do that.

class Object:
    def setproto(self, other):
        self.__prototype = other

    # what a nasty rat's nest.  Surely there is a simpler way to write this
    # function.
    def __getattr__(self, name):
        try:
            prototype = self.__dict__['_Mixin__prototype']
        except KeyError:
            raise AttributeError, name
        except:
            raise

        result = getattr(prototype, name)

        try:
            result_rebind = result.prototype_method_rebind
        except AttributeError:  # it's not a Method object, return it unchanged
            return result
        except:  # I don't think this can happen
            raise
        return result_rebind(self)

    # we want to recognize when someone tries to bind functions to properties
    # and turn them into Method objects
    def __setattr__(self, name, newval):
        if callable(newval):
            self.__dict__[name] = Method(self, newval)
        else:
            self.__dict__[name] = newval

TestFailure = "test failed"

# If this function runs to completion instead of raising an exception, that
# means the class more or less works.
def test():
    x = Object()
    ok = 0
    try:
        x.baz
    except AttributeError:
        ok = 1
    except:
        raise
    if not ok:
        raise TestFailure, "undefined attribute was found"

    x.spam = 3
    if x.spam != 3:
        raise TestFailure, "setting a number didn't work"

    x.foot = lambda self, other: self.spam + other
    if x.foot.__class__ != Method:
        raise TestFailure, "setting a method didn't work"
    if x.foot(4) != 7:
        raise TestFailure, "Johnny can't add"

    y = Object()
    y.setproto(x)
    if y.foot(5) != 8:
        raise TestFailure, "delegation didn't work"
    y.spam = 40
    if y.foot(5) != 45:
        raise TestFailure, "couldn't set y.spam"
    if x.foot(4) != 7:
        raise TestFailure, "broke x messing with y"

-- 
<kragen at pobox.com>       Kragen Sitaker     <http://www.pobox.com/~kragen/>
Perilous to all of us are the devices of an art deeper than we ourselves
possess.
                -- Gandalf the Grey [J.R.R. Tolkien, "Lord of the Rings"]



More information about the Python-list mailing list