Chapter 12. Class Decorators

Just as we can create decorators for functions and methods, we can also create decorators for entire classes. Class decorators take a class object (the result of the class statement), and should return a class—normally a modified version of the class they decorate. In this subsection we will study two class decorators to see how they can be implemented.

In the book’s examples there is a SortedList.py module that defines the Sort-edList custom collection class. This class aggregates a plain list as the private attribute self.__list, and eight of the SortedList class’s methods simply pass on their work to the private attribute. For example, here are how the Sort-edList.clear() and SortedList.pop() methods are implemented:

def clear(self):
    self.__list = []

def pop(self, index=-1):
    return self.__list.pop(index)


There is nothing we can do about the clear() method since there is no corresponding method for the list type, but for pop(), and the other six methods that SortedList delegates, we can simply call the list class’s corresponding method. This can be done by using the @delegate class decorator from the book’s Util module. Here is the start of a new version of the SortedList class:

@Util.delegate("__list", ("pop", "__delitem__", "__getitem__",
                    "__iter__", "__reversed__", "__len__", "__str__"))
class SortedList:


The first argument is the name of the attribute to delegate to, and the second argument is a sequence of one or more methods that we want the delegate() decorator to implement for us so that we don’t have to do the work ourselves. The SortedList class in the SortedListDelegate.py file uses this approach and therefore does not have any code for the methods listed, even though it fully supports them. Here is the class decorator that implements the methods for us:

def delegate(attribute_name, method_names):
    def decorator(cls):
        nonlocal attribute_name

        if attribute_name.startswith("__"):
            attribute_name = "_" + cls.__name__ + attribute_name
        for name in method_names:
            setattr(cls, name, eval("lambda self, *a, **kw: "
                                    "self.{0}.{1}(*a, **kw)".format(
                                    attribute_name, name)))
        return cls
    return decorator


We could not use a plain decorator because we want to pass arguments to the decorator, so we have instead created a function that takes our arguments and that returns a class decorator. The decorator itself takes a single argument, a class (just as a function decorator takes a single function or method as its argument).

We must use nonlocal so that the nested function uses the attribute_name from the outer scope rather than attempting to use one from its own scope. And we must be able to correct the attribute name if necessary to take account of the name mangling of private attributes. The decorator’s behavior is quite simple: It iterates over all the method names that the delegate() function has been given, and for each one creates a new method which it sets as an attribute on the class with the given method name.

We have used eval() to create each of the delegated methods since it can be used to execute a single statement, and a lambda statement produces a method or function. For example, the code executed to produce the pop() method is:

lambda self, *a, **kw: self._SortedList__list.pop(*a, **kw)


We use the * and ** argument forms to allow for any arguments even though the methods being delegated to have specific argument lists. For example, list.pop() accepts a single index position (or nothing, in which case it defaults to the last item). This is okay because if the wrong number or kinds of arguments are passed, the list method that is called to do the work will raise an appropriate exception.

The second class decorator we will review is used with the FuzzyBool.py module from the book’s examples. This module defines the FuzzyBool class, and although the implementation only defines two comparison special methods, __lt__() and __eq__() (for < and ==), all the other comparison methods are supported automatically thanks to the use of a class decorator:

@Util.complete_comparisons
class FuzzyBool:


The other four comparison operators were provided by the complete_comparisons() class decorator. Given a class that defines only < (or < and ==), the decorator produces the missing comparison operators by using the following logical equivalences:

image

If the class to be decorated has < and ==, the decorator will use them both, falling back to doing everything in terms of < if that is the only operator supplied. (In fact, Python automatically produces > if < is supplied, != if == is supplied, and >= if <= is supplied, so it is sufficient to just implement the three operators <, <=, and == and to leave Python to infer the others. However, using the class decorator reduces the minimum that we must implement to just <. This is convenient, and also ensures that all the comparison operators use the same consistent logic.)

def complete_comparisons(cls):
    assert cls.__lt__ is not object.__lt__, (
            "{0} must define < and ideally ==".format(cls.__name__))
    if cls.__eq__ is object.__eq__:
        cls.__eq__ = lambda self, other: (not
                (cls.__lt__(self, other) or cls.__lt__(other, self)))
    cls.__ne__ = lambda self, other: not cls.__eq__(self, other)
    cls.__gt__ = lambda self, other: cls.__lt__(other, self)
    cls.__le__ = lambda self, other: not cls.__lt__(other, self)
    cls.__ge__ = lambda self, other: not cls.__lt__(self, other)
    return cls


One problem that the decorator faces is that class object from which every other class is ultimately derived defines all six comparison operators, all of which raise a TypeError exception if used. So we need to know whether < and == have been reimplemented (and are therefore usable). This can easily be done by comparing the relevant special methods in the class being decorated with those in object.

If the decorated class does not have a custom < the assertion fails because that is the decorator’s minimum requirement. And if there is a custom == we use it; otherwise, we create one. Then all the other methods are created and the decorated class, now with all six comparison methods, is returned.

Using class decorators is probably the simplest and most direct way of changing classes. Another approach is to use metaclasses, a topic we will cover later in this short cut.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset