jpb.coop

Making super safe, a cooperative methods library

Contents

Introduction

Python's super keyword are a very useful tool to write cooperative methods. This is a method that cooperates with the other overrides in the same hierarchy. A good example of such a method is __init__, as all the overrides must be called in class-hierarchy ascending order to properly build an object.

Some interesting links before you proceed:

The problem

Making cooperative methods in linear hierarchies is simple, but that is not the case in the presence of multiple inheritance. The problem lies on the fact that the next override to be called is not known at class definition time. James Knight rant against super makes a very clear exposition of the problem and proposes a methodology to use super consistently, if used at all.

We believe that super is very useful and many interesting and expressive programming patterns arise when using horizontal hierarchies extensively. This library is attempt to make these safer and usable in large projects.

Basic usage

Our library does a lot magic inside. When defining a class that we want to have cooperative methods -- all your classes because __init__ should be cooperative indeed! -- you have to tell Python this fact. There are two ways to do so. In all the code bellow assume that we have imported the library as in:

from jpb.coop import *

The class decorator method

You can use a class decorator to do so. When using this method, you should decorate every single cooperative class in this way:

class MyClass(object):
    ...
MyClass = cooperative_class(MyClass)

Or in Python >= 2.7 with simplified syntax:

@cooperative_class
class MyClass(object):
    ...

The metaclass method

You can also setup a cooperative class by overriding its metaclass, as in:

class MyClass(object):
    __metaclass__ = CooperativeMeta
    ...

Or in Python >= 2.7 with simplified syntax:

class MyClass(object, metaclass=CooperativeMeta):
    ...

Note that this automatically makes every child of MyClass cooperative too. Also, we provide a Cooperative base class that installs the metaclass. The easiest way to use the library is to just inherit from it in your root classes:

class MyClass(Cooperative):
    ...

Defining constructors

In a cooperative class, just trying to override the constructor will yield an error, as in:

class MyClass(Cooperative):
    def __init__(self):
        pass

Yields:

CooperativeError: Constructor should cooperate in cooperative class

You can fix the problem with the cooperate decorator. This will automatically call the superclass constructor. For example:

class Base(Cooperative):
    @cooperate
    def __init__(self):
        print "Base.__init__"

class Deriv(Cooperative):
    @cooperate
    def __init__(self):
        print "Deriv.__init__"

Deriv()

When instantiating Deriv all constructors get called in increasing order. The execution of this code outputs on the screens:

Base.__init__
Deriv.__init__

Parameter passing

When classes cooperate, you do not know the concrete of your upper class. Inheriting from something means that they will be among your super classes in that order, but there might be other classes that get in between. This means, that you do not know the signature of the __init__ method that is called next in the chain. To solve this, we have to ensure two things:

  1. That all cooperating overrides have the same positional arguments. Concretely, __init__ should just have no positional arguments at all, and if you declare any an error will be raised.
  2. We still want to be able to pass different parameters to the different classes above. What we do is a technique call keyword picking: we cherry pick any keyword parameters we need and pass the remaining ones to upper classes. The cooperate decorator takes care of that.

This example should clarify this:

class Base(Cooperative):
    @cooperate
    def __init__(base_param=None)
        print "base_param = ", base_param

class Base(Cooperative):
    @cooperate
    def __init__(deriv_param=None)
        print "deriv_param = ", base_param

Base(deriv_param = "Hello",
     base_param  = "world!")

This will output:

base_param = Hello
deriv_param = World!

As you see, all parameters should be passed with name. You can pass parameters to upper classes constructors directly, each parameter arrives the first class that picks it properly. There are more ways to this, but lets move now to see how to declare your own cooperative methods.

Defining cooperative methods

While __init__ and __del__ are cooperative by default, other methods and their overriding rules behave as normally. However it might be interesting to have other methods behave like this. For example, in a computer game entities might have an update method that updates its state on every new frame tick.

Every part of the entity should cooperate for the update, this, we can enforce cooperative overrides for this method declaring it to be cooperative in its first definition. Note that you can only cooperate on a method that has been declared cooperative before in the hierarchy, otherwise an error thrown. Also, the same method should be declared cooperative only once in the hierarchy, and an error is shown otherwise. This makes sure that you are overriding the method that you want to override and that you made no mistakes when multiple-inheriting in large code bases. For example:

class Entity(Cooperative):
    @cooperative
    def update(self, timer):
        print "Entity.update"

class Player(Entity):
    @cooperate
    def update(self, timer):
        print "Player.update"

Player().update(0)

Will print:

Entity.update
Player.update

(c) Juan Pedro BolĂ­var Puente 2012