Recipe6.5.Delegating Automatically as an Alternative to Inheritance


Recipe 6.5. Delegating Automatically as an Alternative to Inheritance

Credit: Alex Martelli, Raymond Hettinger

Problem

You'd like to inherit from a class or type, but you need some tweak that inheritance does not provide. For example, you want to selectively hide some of the base class' methods, which inheritance doesn't allow.

Solution

Inheritance is quite handy, but it's not all-powerful. For example, it doesn't let you hide methods or other attributes supplied by a base class. Containment with automatic delegation is often a good alternative. Say, for example, you need to wrap some objects to make them read-only; thus preventing accidental alterations. Therefore, besides stopping attribute-setting, you also need to hide mutating methods. Here's a way:

# support 2.3 as well as 2.4 try: set except NameError: from sets import Set as set class ROError(AttributeError): pass class Readonly: # there IS a reason to NOT subclass object, see Discussion     mutators = {         list: set('''_ _delitem_ _ _ _delslice_ _ _ _iadd_ _ _ _imul_ _                  _ _setitem_ _ _ _setslice_ _ append extend insert                  pop remove sort'''.split( )),         dict: set('''_ _delitem_ _ _ _setitem_ _ clear pop popitem                  setdefault update'''.split( )),         }     def _ _init_ _(self, o):         object._ _setattr_ _(self, '_o', o)         object._ _setattr_ _(self, '_no', self.mutators.get(type(o), ( )))     def _ _setattr_ _(self, n, v):         raise ROError, "Can't set attr %r on RO object" % n     def _ _delattr_ _(self, n):         raise ROError, "Can't del attr %r from RO object" % n     def _ _getattr_ _(self, n):         if n in self._no:             raise ROError, "Can't get attr %r from RO object" % n         return getattr(self._o, n)

Code using this class Readonly can easily add other wrappable types with Readonly.mutators[sometype] = the_mutators.

Discussion

Automatic delegation, which the special methods _ _getattr_ _, _ _setattr_ _, and _ _delattr_ _ enable us to perform so smoothly, is a powerful, general technique. In this recipe, we show how to use it to get an effect that is almost indistinguishable from subclassing while hiding some names. In particular, we apply this quasi-subclassing to the task of wrapping objects to make them read-only. Performance isn't quite as good as it might be with real inheritance, but we get better flexibility and finer-grained control as compensation.

The fundamental idea is that each instance of our class holds an instance of the type we are wrapping (i.e., extending and/or tweaking). Whenever client code tries to get an attribute from an instance of our class, unless the attribute is specifically defined there (e.g., the mutators dictionary in class Readonly), _ _getattr_ _ TRansparently shunts the request to the wrapped instance after appropriate checks. In Python, methods are also attributes, accessed in just the same way, so we don't need to do anything different to access methods. The _ _getattr_ _ approach used to access data attributes works for methods just as well.

This is where the comment in the recipe about there being a specific reason to avoid subclassing object comes in. Our _ _getattr_ _ based approach does work on special methods too, but only for instances of old-style classes. In today's object model, Python operations access special methods on the class, not on the instance. Solutions to this issue are presented next in Recipe 6.6 and in Recipe 20.8. The approach adopted in this recipemaking class Readonly old style, so that the issue can be locally avoided and delegated to other recipesis definitely not recommended for production code. I use it here only to keep this recipe shorter and to avoid duplicating coverage that is already amply given elsewhere in this cookbook.

_ _setattr_ _ plays a role similar to _ _getattr_ _, but it gets called when client code sets an instance attribute; in this case, since we want to make a read-only wrapper, we simply forbid the operation. Remember, to avoid triggering _ _setattr_ _ from inside the methods you code, you must never code normal self.n = v statements within the methods of classes that have _ _setattr_ _. The simplest workaround is to delegate the setting to class object, just like our class Readonly does twice in its _ _init_ _ method. Method _ _delattr_ _ completes the picture, dealing with any attempts to delete attributes from an instance.

Wrapping by automatic delegation does not work well with client or framework code that, one way or another, does type-testing. In such cases, the client or framework code is breaking polymorphism and should be rewritten. Remember not to use type-tests in your own client code, as you probably do not need them anyway. See Recipe 6.13 for better alternatives.

In old versions of Python, automatic delegation was even more prevalent, since you could not subclass built-in types. In modern Python, you can inherit from built-in types, so you'll use automatic delegation less often. However, delegation still has its placeit is just a bit farther from the spotlight. Delegation is more flexible than inheritance, and sometimes such flexibility is invaluable. In addition to the ability to delegate selectively (thus effectively "hiding" some of the attributes), an object can delegate to different subobjects over time, or to multiple subobjects at one time, and inheritance doesn't offer anything comparable.

Here is an example of delegating to multiple specific subobjects. Say that you have classes that are chock full of "forwarding methods", such as:

class Pricing(object):     def _ _init_ _(self, location, event):         self.location = location         self.event = event     def setlocation(self, location):         self.location = location     def getprice(self):         return self.location.getprice( )     def getquantity(self):         return self.location.getquantity( )     def getdiscount(self):         return self.event.getdiscount( )     and many more such methods

Inheritance is clearly not applicable because an instance of Pricing must delegate to specific location and event instances, which get passed at initialization time and may even be changed. Automatic delegation to the rescue:

class AutoDelegator(object):     delegates = ( )     do_not_delegate = ( )     def _ _getattr_ _(self, key):         if key not in do_not_delegate:             for d in self.delegates:                 try:                     return getattr(d, key)                 except AttributeError:                     pass         raise AttributeError, key class Pricing(AutoDelegator):     def  _ _init_ _(self, location, event):         self.delegates = [location, event]     def setlocation(self, location):         self.delegates[0] = location

In this case, we do not delegate the setting and deletion of attributes, only the getting of attributes (and nonspecial methods). Of course, this approach is fully applicable only when the methods (and other attributes) of the various objects to which we want to delegate do not interfere with each other; for example, location must not have a getdiscount method; otherwise, it would preempt the delegation of that method, which is intended to go to event.

If a class that does lots of delegation has a few such issues to solve, it can do so by explicitly defining the few corresponding methods, since _ _getattr_ _ enters the picture only for attributes and methods that cannot be found otherwise. The ability to hide some attributes and methods that are supplied by a delegate, but the delegator does not want to expose, is supported through attribute do_not_delegate, which any subclass may override. For example, if class Pricing wanted to hide a method setdiscount that is supplied by, say, event, only a tiny change would be required:

class Pricing(AutoDelegator):     do_not_delegate = ('set_discount',)

while all the rest remains as in the previous snippet.

See Also

Recipe 6.13; Recipe 6.6; Recipe 20.8; Python in a Nutshell chapter on OOP; PEP 253 (http://www.python.org/peps/pep-0253.html) for more details about Python's current (new-style) object model.



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