Recipe20.7.Adding Functionality to a Class by Enriching All Methods


Recipe 20.7. Adding Functionality to a Class by Enriching All Methods

Credit: Stephan Diehl, Robert E. Brewer

Problem

You need to add functionality to an existing class without changing the source code for that class. Specifically, you need to enrich all methods of the class, adding some extra functionality "around" that of the existing methods.

Solution

Recipe 20.6 previously showed a way to solve this task for one method by writing a closure that builds and applies a wrapper, exemplified by function add_tracing_prints_to_method in that recipe's Solution. This recipe generalizes that one, wrapping methods throughout a class or hierarchy, directly or via a custom metaclass.

Module inspect lets you easily find all methods of an existing class, so you can systematically wrap them all:

import inspect def add_tracing_prints_to_all_methods(class_object):     for method_name, v in inspect.getmembers(class_object, inspect.ismethod):         add_tracing_prints_to_method(class_object, method_name)

If you need to ensure that such wrapping applies to all methods of all classes in a whole hierarchy, the simplest way may be to insert a custom metaclass at the root of the hierarchy, so that all classes in the hierarchy will get that same metaclass. This insertion does normally need a minimum of "invasiveness"placing a single statement

    _ _metaclass_ _ = MetaTracer

in the body of that root class. Custom metaclass MetaTracer is, however, quite easy to write:

class MetaTracer(type):     def _ _init_ _(cls, n, b, d):         super(MetaTracer, cls)._ _init_ _(n, b, d)         add_tracing_prints_to_all_methods(cls)

Even such minimal invasiveness sometimes is unacceptable, or you need a more dynamic way to wrap all methods in a hierarchy. Then, as long as the root class of the hierarchy is new-style, you can arrange to get function add_tracing_prints_to_all_methods dynamically called on all classes in the hierarchy:

def add_tracing_prints_to_all_descendants(class_object):     add_tracing_prints_to_all_methods(class_object)     for s in class_object._ _subclasses_ _( ):         add_tracing_prints_to_all_descendants(s)

The inverse function unwrapfunc, in Recipe 20.6, may also be similarly applied to all methods of a class and all classes of a hierarchy.

Discussion

We could code just about all functionality of such a powerful function as add_tracing_prints_to_all_descendants in the function's own body. However, it would not be a great idea to bunch up such diverse functionality inside a single function. Instead, we carefully split the functionality among the various separate functions presented in this recipe and previously in Recipe 20.6. By this careful factorization, we obtain maximum reusability without code duplication: we have separate functions to dynamically add and remove wrapping from a single method, an entire class, and a whole hierarchy of classes; each of these functions appropriately uses the simpler ones. And for cases in which we can afford a tiny amount of "invasiveness" and want the convenience of automatically applying the wrapping to all methods of classes descended from a certain root, we can use a tiny custom metaclass.

add_tracing_prints_to_all_descendants cannot apply to old-style classes. This limitation is inherent in the old-style object model and is one of the several reasons you should always use new-style classes in new code you write: classic classes exist only to ensure compatibility in legacy programs. Besides the problem with classic classes, however, there's another issue with the structure of add_tracing_prints_to_all_descendants: in cases of multiple inheritance, the function will repeatedly visit some classes.

Since the method-wrapping function is carefully designed to avoid double wrapping, such multiple visits are not a serious problem, costing just a little avoidable overhead, which is why the function was acceptable for inclusion in the "Solution". In other cases in which we want to operate on all descendants of a certain root class, however, multiple visits might be unacceptable. Moreover, it is clearly not optimal to entwine the functionality of getting all descendants with that of applying one particular operation to each of them. The best idea is clearly to factor out the recursive structure into a generator, which can avoid duplicating visits with the memo idiom:

def all_descendants(class_object, _memo=None):     if _memo is None:         _memo = {  }     elif class_object in _memo:         return     yield class_object     for subclass in class_object._ _subclasses_ _( ):         for descendant in all_descendants(subclass, _memo):             yield descendant

Adding tracing prints to all descendants now simplifies to:

def add_tracing_prints_to_all_descendants(class_object):     for c in all_descendants(class_object):         add_tracing_prints_to_all_methods(c)

In Python, whenever you find yourself with an iteration structure of any complexity, or recursion, it's always worthwhile to check whether it's feasible to factor out the iterative or recursive control structure into a separate, reusable generator, so that all iterations of that form can become simple for statements. Such separation of concerns can offer important simplifications and make code more maintainable.

See Also

Recipe 20.6 for details on how each method gets wrapped; Library Reference and Python in a Nutshell docs on module inspect and the _ _subclasses_ _ special method of new-style classes.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net