Section 13.15. Delegation


13.15. *Delegation

13.15.1. Wrapping

"Wrapping" is a term you will hear often in the Python programming world. It is a generic moniker to describe the packaging of an existing object, whether it be a data type or a piece of code, adding new, removing undesired, or otherwise modifying existing functionality to the existing object.

Before Python 2.2, the subclassing or derivation of a standard type in Python was not allowed. Even though you can do that now with the new-style classes, there is a concept that is still popular. You can always wrap any type as the core member of a class so that the new object's behavior mimics all existing behavior of the data type that you want and does not do what you do not want it to do; and perhaps it will do something a little extra. This is called "wrapping a type." In the Appendix, we will discuss how to extend Python, another form of wrapping.

Wrapping consists of defining a class whose instances have the core behavior of a standard type. In other words, it not only sings and dances now, but also walks and talks like our original type. Figure 15-4 illustrates what a type wrapped in a class looks like. The core behavior of a standard type is in the center of the figure, but it is also enhanced by new or updated functionality, and perhaps even by different methods of accessing the actual data.

Class Object (Which Behaves Like a Type)

You may also wrap classes, but this does not make as much sense because there is already a mechanism for taking an object and wrapping it in a manner as described above for a standard type. How would you take an existing class, mimic the behavior you desire, remove what you do not like, and perhaps tweak something to make the class perform differently from the original class? That process, as we discussed recently, is derivation.

Figure 13-4. Wrapping a Type


13.15.2. Implementing Delegation

Delegation is a characteristic of wrapping that simplifies the process with regard to dictating functionality by taking advantage of pre-existing functionality to maximize code reuse.

Wrapping a type generally consists of some sort of customization to the existing type. As we mentioned before, this tweaking comes in the form of new, modified, or removed functionality compared to the original product. Everything else should remain the same, or keep its existing functionality and behavior. Delegation is the process whereby all the updated functionality is handled as part of the new class, but the existing functionality is delegated to the default attributes of the object.

The key to implementing delegation is to override the __getattr__() method with code containing a call to the built-in getattr() function. Specifically, getattr() is invoked to obtain the default object attribute (data attribute or method) and return it for access or invocation. The way the special method __getattr__() works is that when an attribute is searched for, any local ones are found first (the customized ones). If the search fails, then __getattr__() is invoked, which then calls getattr() to obtain an object's default behavior.

In other words, when an attribute is referenced, the Python interpreter will attempt to find that name in the local namespace, such as a customized method or local instance attribute. If it is not found in the local dictionary, then the class namespace is searched, just in case a class attribute was accessed. Finally, if both searches fail, the hunt begins to delegate the request to the original object, and that is when __getattr__() is invoked.

Simple Example Wrapping Any Object

Let us take a look at an example. Here we present a class that wraps nearly any object, providing such basic functionality as string representations with repr() and str(). Additional customization comes in the form of the get() method, which removes the wrapping and returns the raw object. All remaining functionality is delegated to the object's native attributes as retrieved by __getattr__() when necessary.

Here is an example of a wrapping class:

class WrapMe(object):     def __init__(self, obj):         self.__data = obj     def get(self):         return self.__data     def __repr__(self):         return 'self.__data'     def __str__(self):         return str(self.__data)     def __getattr__(self, attr):         return getattr(self.__data, attr)


In our first example, we will use complex numbers, because of all Python's numeric types, complex numbers are the only one with attributes: data attributes as well as its conjugate() built-in method. Remember that attributes can be both data attributes as well as functions or methods:

    >>> wrappedComplex = WrapMe(3.5+4.2j)     >>> wrappedComplex                # wrapped object: repr()     (3.5+4.2j)     >>> wrappedComplex.real           # real attribute     3.5     >>> wrappedComplex.imag           # imaginary attribute     42.2     >>> wrappedComplex.conjugate()    # conjugate() method     (3.5-4.2j)     >>> wrappedComplex.get()          # actual object     (3.5+4.2j)


Once we create our wrapped object type, we obtain a string representation, silently using the call to repr() by the interactive interpreter. We then proceed to access all three complex number attributes, none of which is defined for our class. Confirm this by looking for real, imag, and conjugate in our class definition ... they are not there!

The accessing of these attributes is delegated to the object via the getattr() method. The final call to get() is not delegated because it is defined for our objectit returns the actual data object that we wrapped.

Our next example using our wrapping class uses a list. We will create the object, then perform multiple operations, delegating each time to list methods.

    >>> wrappedList = WrapMe([123, 'foo', 45.67])     >>> wrappedList.append('bar')     >>> wrappedList.append(123)     >>> wrappedList     [123, 'foo', 45.67, 'bar', 123]     >>> wrappedList.index(45.67)     2     >>> wrappedList.count(123)     2     >>> wrappedList.pop()     123     >>> wrappedList     [123, 'foo', 45.67, 'bar']


Notice that although we are using a class instance for our examples, they exhibit behavior extremely similar to the data types they wrap. Be aware, however, that only existing attributes are delegated in this code.

Special behaviors that are not in a type's method list will not be accessible since they are not attributes. One example is the slicing operations of lists which are built-in to the type and not available as an attribute like the append() method, for example. Another way of putting it is that the slice operator ( [ ] ) is part of the sequence type and is not implemented through the __getitem__() special method.

   >>> wrappedList[3]    Traceback (innermost last):      File "<stdin>", line 1, in ?      File "wrapme.py", line 21, in __getattr__        return getattr(self.data, attr)    AttributeError: __getitem__


The AttributeError exception results from the fact that the slice operator invokes the __getitem__() method, and __getitem__() is not defined as a class instance method nor is it a method of list objects. Recall that getattr() is called only when an exhaustive search through an instance's or class's dictionaries fails to find a successful match. As you can see above, the call to getattr() is the one that fails, triggering the exception.

However, we can always cheat by accessing the real object [with our get() method] and its slicing ability instead:

   >>> realList = wrappedList.get()    >>> realList[3]    'bar'


You probably have a good idea now why we implemented the get() methodjust for cases like this where we need to obtain access to the original object. We can bypass assigning local variable (realList) by accessing the attribute of the object directly from the access call:

    >>> wrappedList.get()[3]     'bar'


The get() method returns the object, which is then immediately indexed to obtain the sliced subset.

    >>> f = WrapMe(open('/etc/motd'))     >>> f     <wrapMe.WrapMe object at 0x40215dac>     >>> f.get()     <open file '/etc/motd', mode 'r' at 0x40204ca0>     >>> f.readline()     'Have a lot of fun...\012'     >>> f.tell()     21     >>> f.seek(0)     >>> print f.readline(),     Have a lot of fun...     >>> f.close()     >>> f.get()     <closed file '/etc/motd', mode 'r' at 0x40204ca0>


Once you become familiar with an object's attributes, you begin to understand where certain pieces of information originate and are able to duplicate functionality with your newfound knowledge:

    >>> print "<%s file %s, mode %s at %x>" % \     ... (f.closed  and 'closed'  or 'open', 'f.name',     'f.mode', id(f.get()))     <closed file '/etc/motd', mode 'r' at 80e95e0>


This concludes the sampling of our simple wrapping class. We have only just begun to touch on class customization with type emulation. You will discover that you can an infinite number of enhancements make to further increase the usefulness of your code. One such enhancement is to add timestamps to objects. In the next subsection, we will add another dimension to our wrapping class: time.

Updating Our Simple Wrapping Class

Creation time, modification time, and access time are familiar attributes of files, but nothing says that you cannot add this type of information to objects. After all, certain applications may benefit from these additional pieces of information.

If you are unfamiliar with using these three pieces of chronological data, we will attempt to clarify them. The creation time (or "ctime") is the time of instantiation, the modification time (or "mtime") refers to the time that the core data was updated [accomplished by calling the new set() method], and the access time (or "atime") is the timestamp of when the data value of the object was last retrieved or an attribute was accessed.

Proceeding to updating the class we defined earlier, we create the module twrapme.py, given in Example 13.7.

Example 13.7. Wrapping Standard Types (twrapme.py)

Class definition that wraps any built-in type, adding time attributes; get(), set(), and string representation methods; and delegating all remaining attribute access to those of the standard type.

1  #!/usr/bin/env python 2 3  from time import time, ctime 4 5  class TimedWrapMe(object): 6 7     def __init__(self, obj): 8        self.__data = obj 9        self.__ctime = self.__mtime = \ 10           self.__atime = time() 11 12    def get(self): 13       self.__atime = time() 14       return self.__data 15 16    def gettimeval(self, t_type): 17       if not isinstance(t_type, str)  or \ 18               t_type[0]  not in 'cma': 19           raise TypeError, \ 20           "argument of 'c', 'm', or 'a' req'd" 21       return getattr(self, '_%s__%stime' % \ 22           (self.__class__.__name__, t_type[0])) 23 24     def gettimestr(self, t_type): 25        return ctime(self.gettimeval(t_type)) 26 27     def set(self, obj): 28        self.__data = obj 29        self.__mtime = self.__atime = time() 30 31     def __repr__(self):                # repr() 32        self.__atime = time() 33        return 'self.__data' 34 35     def __str__(self):                 # str() 36        self.__atime = time() 37        return str(self.__data) 38 39     def __getattr__(self, attr):       # delegate 40        self.__atime = time() 41        return getattr(self.__data, attr)

How did we update the code? Well, first, you will notice the addition of three new methods: gettimeval(), gettimestr(), and set(). We also added lines of code throughout which update the appropriate timestamps based on the type of access performed.

The gettimeval() method takes a single character argument, either "c," "m," or "a," for create, modify, or access time, respectively, and returns the corresponding time that is stored as a float value. gettimestr() simply returns a pretty-printable string version of the time as formatted by the time.ctime() function.

Let us take a test drive of our new module. We have already seen how delegation works, so we are going to wrap objects without attributes to highlight the new functionality we just added. In our example, we will wrap an integer, then change it to a string.

    >>> timeWrappedObj = TimedWrapMe(932)     >>> timeWrappedObj.gettimestr('c')     ' Wed Apr 26 20:47:41 2006'     >>> timeWrappedObj.gettimestr('m')     'Wed Apr 26 20:47:41 2006'     >>> timeWrappedObj.gettimestr('a')     'Wed Apr 26 20:47:41 2006'     >>> timeWrappedObj     932     >>> timeWrappedObj.gettimestr('c')     'Wed Apr 26 20:47:41 2006'     >>> timeWrappedObj.gettimestr('m')     'Wed Apr 26 20:47:41 2006'     >>> timeWrappedObj.gettimestr('a')     'Wed Apr 26 20:48:05 2006'


You will notice that when an object is first wrapped, the creation, modification, and last access times are all the same. Once we access the object, the access time is updated, but not the others. If we use set() to replace the object, the modification and last access times are updated. One final read access to our object concludes our example.

   >>> timeWrappedObj.set('time is up!')    >>> timeWrappedObj.gettimestr('m')    'Wed Apr 26 20:48:35 2006'    >>> timeWrappedObj    'time is up!'    >>> timeWrappedObj.gettimestr('c')    'Wed Apr 26 20:47:41 2006'    >>> timeWrappedObj.gettimestr('m')    'Wed Apr 26 20:48:35 2006'    >>> timeWrappedObj.gettimestr('a')    'Wed Apr 26 20:48:46 2006'


Wrapping a Specific Object with Enhancements

The next example represents a class that wraps a file object. Our class will behave in the exact same manner as a regular file object with one exception: in write mode, only strings in all capital letters are written to the file.

The problem we are trying to solve here is for a case where you are writing text files whose data is to be read by an old mainframe computer. Many older style machines are restricted to uppercase letters for processing, so we want to implement a file object where all text written to the file is automatically converted to uppercase without the programmer's having to worry about it. In fact, the only noticeable difference is that rather than using the open() built-in function, a call is made to instantiate the CapOpen class. Even the parameters are exactly the same as for open().

Example 13.8 represents that code, written as capOpen.py. Let us take a look at an example of how to use this class:

   >>> f = CapOpen('/tmp/xxx', 'w')    >>> f.write('delegation example\n')    >>> f.write('faye is good\n')    >>> f.write('at delegating\n')    >>> f.close()    >>> f    <closed file '/tmp/xxx', mode 'w' at 12c230>


Example 13.8. Wrapping a File Object (capOpen.py)

This class extends on the example from one of the Python FAQs, providing a file like object that customizes the write () method while delegating the rest of the functionality to the file object.

1  #!/usr/bin/env python 2 3  class CapOpen(object): 4      def __init__(self, fn, mode='r', buf=-1): 5          self.file = open(fn, mode, buf) 6 7      def __str__(self): 8          return str(self.file) 9 10     def __repr__(self): 11         return 'self.file' 12 13     def write(self, line): 14         self.file.write(line.upper()) 15 16     def __getattr__(self, attr): 17         return getattr(self.file, attr)

As you can see, the only call out of the ordinary is the first one to CapOpen() rather than open(). All other code is identical to what you would do if you were interacting with a real file object rather than a class instance that behaves like a file object. All attributes other than write() have been delegated to the file object. To confirm the success of our code, we load up the file and display its contents. (Note that we can use either open() or CapOpen(), but chose only CapOpen() because we have been working with it here in this example.)

   >>> f = CapOpen('/tmp/xxx', 'r')    >>> for eachLine  in f:    ...     print eachLine,    ...    DELEGATION EXAMPLE    FAYE IS GOOD    AT DELEGATING




Core Python Programming
Core Python Programming (2nd Edition)
ISBN: 0132269937
EAN: 2147483647
Year: 2004
Pages: 334
Authors: Wesley J Chun

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