Wednesday, February 1, 2012

Modifying python classes at run-time

Adding methods to python objects and classes at run-time can be very handy when API specifications are fluid or when loading custom file formats. Think for example of an object implementing a protocol defined as XML or another descriptive language. Or an object giving access to stored database methods. Instead of only having a general execute method or having to recreate a static API file, all the described methods can be added to a class at runtime.

Adding a method to an object.

Let us say there is a class called B, with one method:

class B:
  def method1(self):
    print "method1"
And we want to add a second method at run-time. A naive solution would look like this:

def method2(self):
  self.method1()
  print "method2"
b = B()
b.method2 = method2
b.method2()

TypeError: method2() takes exactly 1 argument (0 given)

This doesn't work since no self will be passed to method2. By just adding it as an attribute, it does not become a class method. It is possible to fix this by using a closure:

def makeMethod2(self):
  def thunk():
    return method2(self)
  return thunk
b = B()
b.method2 = makeMethod2(b)

Adding a method to a class.

If all instances of B need to have the method, it has to be added to the class B, not the instance b:

B.method2 = method2
b = B()
b.method2()

Since the method name is only known at run-time, it will be added using setattr instead of an assignment:

setattr(B, "method2", method2)
b = B()
b.method2()

The method body will also be created dynamically. Let us assume that there is a general execute method in B which calls the specified service method.

def execute(self, method, parameters):
 
print "executing %s with %s" % (method, params)

Now method2 can be created as follows:

class B:

  def execute(self, method, *
params):
    print "executing %s with %s" % (method, params)
 
  @classmethod
  def addMethod(cls, name):
    def method(self, *
params):
      self.execute(name, *
params)
    setattr(cls, name, method)
B.addMethod("method2")
b = B()
b.method2(1, 2)

An @classmethod in python creates a method which is passed the class object as opposed to the instance object. As extra parameter we pass the name of the method. The method itself passes all the parameters it receives to the execute method.
In practice it is better to use **params to get named parameters, instead of positional parameters. The method is then always called with named parameters:

b.method2(c=1, d=2)

Additionally a dictionary containing a parameter-type mapping can be passed to addMethod, this way method or execute can do a type check on the parameters when called.

No comments:

Post a Comment