|I l@ve RuBoard|
15.10 Adding Functionality to a Class
Credit: Ken Seehof
Again, this is a case for introspection and dynamic change. The enhance_method function alters a klass class object to substitute a named method with an enhanced version, decorated by the replacement function argument. The method_logger method exemplifies a typical case of replacement by decorating any method with print statements tracing its calls and returns:
# requires Python 2.1, or 2.2 with classic classes only from _ _future_ _ import nested_scopes import new def enhance_method(klass, method_name, replacement): 'replace a method with an enhanced version' method = getattr(klass, method_name) def enhanced(*args, **kwds): return replacement(method, *args, **kwds) setattr(klass, method_name, new.instancemethod(enhanced, None, klass)) def method_logger(old_method, self, *args, **kwds): 'example of enhancement: log all calls to a method' print '*** calling: %s%s, kwds=%s' % (old_method._ _name_ _, args, kwds) return_value = old_method(self, *args, **kwds) # call the original method print '*** %s returns: %r' % (old_method._ _name_ _, return_value) return return_value def demo( ): class Deli: def order_cheese(self, cheese_type): print 'Sorry, we are completely out of %s' % cheese_type d = Deli( ) d.order_cheese('Gouda') enhance_method(Deli, 'order_cheese', method_logger) d.order_cheese('Cheddar')
This recipe is useful when you need to modify the behavior of a standard or third-party Python module, but changing the module itself is undesirable. In particular, this recipe can be handy for debugging, since you can use it to log all calls to a library method that you want to watch without changing the library code or needing interactive access to the session. The method_logger function in the recipe shows this specific logging usage, and the demo function shows typical usage.
Here's another, perhaps more impressive, use for this kind of approach. Sometimes you need to globally change the behavior of an entire third-party Python library. For example, say a Python library that you downloaded has 50 different methods that all return error codes, but you want these methods to raise exceptions instead (again, you don't want to change their code). After importing the offending module, you repeatedly call this recipe's enhance_method function to hook a replacement version that checks the return value and issues an exception if an error occurred around each method, wrapping each of the 50 methods in question with the same enhancement metafunction.
The heart of the recipe is the enhance_method function, which takes the class object, method name string, and replacement decorator function as arguments. It extracts the method with the getattr built-in function and replaces the method with the reciprocal setattr built-in function. The replacement is a new instance method (actually, an unbound method, as specified by the second None argument to new.instancemethod) that wraps an enhanced function, which is built with a local def. This relies on lexically nested scopes, since the local (nested) enhanced function must be able to see the method and replacement names that are local variables of the enclosing (outer) enhance_method function. The reliance on nested scopes is the reason this recipe specifies Python 2.1 or 2.2 (to work in 2.1, it needs the from _ _future_ _ import nested_scopes statement at the start of the module).
15.10.4 See Also
|I l@ve RuBoard|