Aspect-oriented Python and Metaclasses
I came across a nice introductory post on AOP in Python authored by Dethe Elza titled Aspect-oriented Python.
He refers to two main tactics when dealing with before/after aspects in Python 2.5 and above: Decorators using the @ syntax and context managers using the “with” keyword.
I’d like to add one more tactic to his list. The Metaclass approach.
Metaclasses are a good fit for Aspect-oriented programming in Python.
I’ll demonstrate this through a simple example.
import inspect
import logging
import re
def before(fn):
def wrapped(*args, **kws):
logging.warn('about to call function %s' % fn.func_name)
return fn(*args, **kws)
return wrapped
def after(fn):
def wrapped(*args, **kws):
retVal = fn(*args, **kws)
logging.warn('just returned from function %s' % fn.func_name)
return retVal
return wrapped
class SimpleLoggingMeta(type):
def __init__(cls, name, bases, ns):
# get list of decorators
decorators = ns.get('decorators', (before, after,))
# get method_pattern
method_pattern = ns.get('method_pattern', '.*')
for key, value in ns.items():
# skip the constructor and objects
# in the namespace that aren't methods
if key in ('__init__') or \
not inspect.isfunction(value): continue
# check if method matches the method pattern
if re.search(method_pattern, key):
# apply the decorators
for decorator in decorators: value = decorator(value)
setattr(cls, key, value)
class Person(object):
__metaclass__ = SimpleLoggingMeta
def __init__(self, first, middle, last):
self.first = first
self.middle = middle
self.last = last
def name(self):
logging.warn('inside name')
return '%s %s' % (self.first, self.last)
def full_name(self):
logging.warn('inside full_name')
return '%s %s %s' % (self.first, self.middle, self.last)
def initials(self):
logging.warn('inside initials')
return '%s%s%s' % \
(self.first[0], self.middle[0], self.last[0])
if __name__ == "__main__":
person = Person('Joe', 'Plumber', 'Sixpack')
person.name()
person.full_name()
person.initials()
Output:
WARNING:root:about to call function name WARNING:root:inside name WARNING:root:just returned from function wrapped WARNING:root:about to call function full_name WARNING:root:inside full_name WARNING:root:just returned from function wrapped WARNING:root:about to call function initials WARNING:root:inside initials WARNING:root:just returned from function wrapped
In the example above, the SimpleLoggingMeta metaclass simplifies the decoration process by automatically applying decorators that are declared in the metaclass or the namespace of the class itself.
Classes can define their own “decorators” and “method_pattern” attributes if metaclass defaults don’t satisfy the concerns.
We will now go ahead and adjust the Person class to apply the before, after and property decorators to all methods of the class that have the following regex pattern “.*name$“
class Person(object):
__metaclass__ = SimpleLoggingMeta
decorators = (before, after, property,)
method_pattern = ".*name$"
def __init__(self, first, middle, last):
self.first = first
self.middle = middle
self.last = last
def name(self):
logging.warn('inside name')
return '%s %s' % (self.first, self.last)
def full_name(self):
logging.warn('inside full_name')
return '%s %s %s' % (self.first, self.middle, self.last)
def initials(self):
logging.warn('inside initials')
return '%s%s%s' % \
(self.first[0], self.middle[0], self.last[0])
if __name__ == "__main__":
person = Person('Joe', 'Plumber', 'Sixpack')
person.name
person.full_name
person.initials()
Output:
WARNING:root:about to call function name WARNING:root:inside name WARNING:root:just returned from function wrapped WARNING:root:about to call function full_name WARNING:root:inside full_name WARNING:root:just returned from function wrapped WARNING:root:inside initials
So what happened? The before, after and property decorators were applied to the methods that match the method pattern “.*name$” which in the case of the Person class is the name and the full_name methods.
The above example is by no means perfect, but I hope you get the idea.
Metaclasses don’t have to be scary.
Recent Comments