Welcome, guest | Sign In | My Account | Store | Cart

A metaclass is used to wrap all (or just some) methods for logging purposes. The underlying mechanism can be used as well to check pre/post conditions, attribute access,... The basic point is, that the actual class must not be changed in any way to achive the desired effect.

Python, 59 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import re

log = open('log','w')
indent = 0
indStr = '  '

def logmethod(methodname):
    def _method(self,*argl,**argd):
        global indent

        #parse the arguments and create a string representation
        args = []
        for item in argl:
            args.append('%s' % str(item))
        for key,item in argd.items():
            args.append('%s=%s' % (key,str(item)))
        argstr = ','.join(args)   
        print >> log,"%s%s.%s(%s) " % (indStr*indent,str(self),methodname,argstr)
        indent += 1
        # do the actual method call
        returnval = getattr(self,'_H_%s' % methodname)(*argl,**argd)
        indent -= 1
        print >> log,'%s:'% (indStr*indent), str(returnval)
        return returnval

    return _method


class LogTheMethods(type):
    def __new__(cls,classname,bases,classdict):
        logmatch = re.compile(classdict.get('logMatch','.*'))
        
        for attr,item in classdict.items():
            if callable(item) and logmatch.match(attr):
                classdict['_H_%s'%attr] = item    # rebind the method
                classdict[attr] = logmethod(attr) # replace method by wrapper

        return type.__new__(cls,classname,bases,classdict)

class Test(object):
    __metaclass__ = LogTheMethods
    logMatch = '.*'

    def __init__(self):
        self.a = 10

    def meth1(self):pass
    def add(self,a,b):return a+b
    def fac(self,val): # faculty calculation
        if val == 1:
            return 1
        else:
            return val * self.fac(val-1)

if __name__ == '__main__':
    l = Test()
    l.meth1()
    print l.add(1,2)
    print l.fac(10)

This recipe shows, how a metaclass can help various programmers needs. In this example, all method calls (and there return value) are written into a log file. Running the code will result in the following log file:


<__main__.Test object at 0x8158c24>.__init__() : None <__main__.Test object at 0x8158c24>.meth1() : None <__main__.Test object at 0x8158c24>.add(1,2) : 3 <__main__.Test object at 0x8158c24>.fac(10) ||<__main__.Test object at 0x8158c24>.fac(9) ||||<__main__.Test object at 0x8158c24>.fac(8) ||||||<__main__.Test object at 0x8158c24>.fac(7) ||||||||<__main__.Test object at 0x8158c24>.fac(6) ||||||||||<__main__.Test object at 0x8158c24>.fac(5) ||||||||||||<__main__.Test object at 0x8158c24>.fac(4) ||||||||||||||<__main__.Test object at 0x8158c24>.fac(3) ||||||||||||||||<__main__.Test object at 0x8158c24>.fac(2) ||||||||||||||||||<__main__.Test object at 0x8158c24>.fac(1) ||||||||||||||||||: 1 ||||||||||||||||: 2 ||||||||||||||: 6 ||||||||||||: 24 ||||||||||: 120 ||||||||: 720 ||||||: 5040 ||||: 40320 ||: 362880

: 3628800

The only interesting bit is the rebinding of the methods and their replacement with a wrapper method. To make this example a little less dull, only methods are wrapped whose method name match some regular expression. This might be actually close to Aspect Oriented Programming (but I don't know enough about this to say for sure)

2 comments

Robert Brewer 18 years, 1 month ago  # | flag

Non-metaclass solution. You can perform the same task on the fly, on any object, without metaclasses. This means you can reroute object methods from outside the object itself. Just reproduce the body of LogTheMethods in a "normal" function. The only bit you need to change is, instead of assigning classdict[attr] = logmethod(attr), you use new.instancemethod(function, instance, class)

if callable(item):
    anInstance = item.im_self
    attr = item.__name__
    anInstance.__dict__['_H_%s' % attr] = item
    anInstance.__dict__[attr] = \
                    new.instancemethod(logmethod(attr),
                                       anInstance,
                                       anInstance.__class__)

In this example, I rewrote the block so that one needs only pass in a reference to any method, and the other properties are introspected. I use this technique to "lock" object methods at runtime: accessing the original method requires a corresponding key. None of the locked objects need a metaclass defined, or __metaclass__ declared.

Jojo Mwebaze 11 years, 2 months ago  # | flag

I tired using recipe above but i run into some problems, i will recreate the error with a simple class

import metaclass

class BankAccount(object): __metaclass__ = metaclass.LogTheMethods logMatch = '.*' def __init__(self, initial_balance=0, other=0): self.balance = initial_balance self.ir=other def deposit(self, amount): self.balance += amount return self.balance def withdraw(self, amount): self.balance -= amount return self.balance def overdrawn(self): return self.balance < 0 def interest(self): self.balance = self.balance + ( self.balance * self.ir) return self.balance dumps = lambda self: repr(self) __str__ = lambda self: self.dumps()

if __name__ == '__main__': b=BankAccount() b.deposit(1000) b.withdraw(2000) b.interest() print b.balance

and this is the error

File "/net/plaut/data/users/jmwebaze/data/awe/astro/experimental/provenance/metaclass.py", line 18, in _method print >> log,"%s%s.%s(%s) " % (indStr*indent,str(self),methodname,argstr) RuntimeError: maximum recursion depth exceeded


when i remove the last lines dumps = lambda self: repr(self) __str__ = lambda self: self.dumps()

The the program runns without any problem.

How do i solve this recursion error?

Created by Stephan Diehl on Sun, 4 May 2003 (PSF)
Python recipes (4591)
Stephan Diehl's recipes (5)

Required Modules

Other Information and Tasks