Tracing Object Interfaces

The singleton example of the prior section illustrated using class decorators to manage all the instances of a class. Another common use case for class decorators augments the interface of each generated instance. Class decorators can essentially install on instances a wrapper logic layer that manages access to their interfaces in some way.

For example, in Chapter 30, the __getattr__ operator overloading method is shown as a way to wrap up entire object interfaces of embedded instances, in order to implement the delegation coding pattern. We saw similar examples in the managed attribute coverage of the prior chapter. Recall that __getattr__ is run when an undefined attribute name is fetched; we can use this hook to intercept method calls in a controller class and propagate them to an embedded object.

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

For reference, here’s the original nondecorator delegation example, working on two built-in type objects:

class Wrapper:
    def __init__(self, object):
        self.wrapped = object                    # Save object
    def __getattr__(self, attrname):
        print('Trace:', attrname)                # Trace fetch
        return getattr(self.wrapped, attrname)   # Delegate fetch

>>> x = Wrapper([1,2,3])                         # Wrap a list
>>> x.append(4)                                  # Delegate to list method
Trace: append
>>> x.wrapped                                    # Print my member
[1, 2, 3, 4]

>>> x = Wrapper({"a": 1, "b": 2})                # Wrap a dictionary
>>> list(x.keys())                               # Delegate to dictionary method
Trace: keys                                      # Use list() in 3.0
['a', 'b']

In this code, the Wrapper class intercepts access to any of the wrapped object’s attributes, prints a trace message, and uses the getattr built-in to pass off the request to the wrapped object. Specifically, it traces attribute accesses made outside the wrapped object’s class; accesses inside the wrapped object’s methods are not caught and run normally by design. This whole-interface model differs from the behavior of function decorators, which wrap up just one specific method.

Class decorators provide an alternative and convenient way to code this __getattr__ technique to wrap an entire interface. In 2.6 and 3.0, for example, the prior class example can be coded as a class decorator that triggers wrapped instance creation, instead of passing a pre-made instance into the wrapper’s constructor (also augmented here to support keyword arguments with **kargs and to count the number of accesses made):

def Tracer(aClass):                                   # On @ decorator
    class Wrapper:
        def __init__(self, *args, **kargs):           # On instance creation
            self.fetches = 0
            self.wrapped = aClass(*args, **kargs)     # Use enclosing scope name
        def __getattr__(self, attrname):
            print('Trace: ' + attrname)               # Catches all but own attrs
            self.fetches += 1
            return getattr(self.wrapped, attrname)    # Delegate to wrapped obj
    return Wrapper

@Tracer
class Spam:                                    # Spam = Tracer(Spam)
    def display(self):                         # Spam is rebound to Wrapper
        print('Spam!' * 8)

@Tracer
class Person:                                  # Person = Tracer(Person)
    def __init__(self, name, hours, rate):     # Wrapper remembers Person
        self.name = name
        self.hours = hours
        self.rate = rate
    def pay(self):                             # Accesses outside class traced
        return self.hours * self.rate          # In-method accesses not traced

food = Spam()                                  # Triggers Wrapper()
food.display()                                 # Triggers __getattr__
print([food.fetches])

bob = Person('Bob', 40, 50)                    # bob is really a Wrapper
print(bob.name)                                # Wrapper embeds a Person
print(bob.pay())

print('')
sue = Person('Sue', rate=100, hours=60)        # sue is a different Wrapper
print(sue.name)                                # with a different Person
print(sue.pay())

print(bob.name)                                # bob has different state
print(bob.pay())
print([bob.fetches, sue.fetches])              # Wrapper attrs not traced

It’s important to note that this is very different from the tracer decorator we met earlier. In Coding Function Decorators, we looked at decorators that enabled us to trace and time calls to a given function or method. In contrast, by intercepting instance creation calls, the class decorator here allows us to trace an entire object interface—i.e., accesses to any of its attributes.

The following is the output produced by this code under both 2.6 and 3.0: attribute fetches on instances of both the Spam and Person classes invoke the __getattr__ logic in the Wrapper class, because food and bob are really instances of Wrapper, thanks to the decorator’s redirection of instance creation calls:

Trace: display
Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam!
[1]
Trace: name
Bob
Trace: pay
2000

Trace: name
Sue
Trace: pay
6000
Trace: name
Bob
Trace: pay
2000
[4, 2]

Notice that the preceding code decorates a user-defined class. Just like in the original example in Chapter 30, we can also use the decorator to wrap up a built-in type such as a list, as long as we either subclass to allow decoration syntax or perform the decoration manually—decorator syntax requires a class statement for the @ line.

In the following, x is really a Wrapper again due to the indirection of decoration (I moved the decorator class to module file tracer.py in order to reuse it this way):

>>> from tracer import Tracer     # Decorator moved to a module file

>>> @Tracer
... class MyList(list): pass      # MyList = Tracer(MyList)

>>> x = MyList([1, 2, 3])         # Triggers Wrapper()
>>> x.append(4)                   # Triggers __getattr__, append
Trace: append
>>> x.wrapped
[1, 2, 3, 4]

>>> WrapList = Tracer(list)       # Or perform decoration manually
>>> x = WrapList([4, 5, 6])       # Else subclass statement required
>>> x.append(7)
Trace: append
>>> x.wrapped
[4, 5, 6, 7]

The decorator approach allows us to move instance creation into the decorator itself, instead of requiring a premade object to be passed in. Although this seems like a minor difference, it lets us retain normal instance creation syntax and realize all the benefits of decorators in general. Rather than requiring all instance creation calls to route objects through a wrapper manually, we need only augment classes with decorator syntax:

@Tracer                                          # Decorator approach
class Person: ...
bob = Person('Bob', 40, 50)
sue = Person('Sue', rate=100, hours=60)

class Person: ...                                # Non-decorator approach
bob = Wrapper(Person('Bob', 40, 50))
sue = Wrapper(Person('Sue', rate=100, hours=60))

Assuming you will make more than one instance of a class, decorators will generally be a net win in terms of both code size and code maintenance.


Note

Attribute version skew note: As we learned in Chapter 37, __getattr__ will intercept accesses to operator overloading methods like __str__ and __repr__ in Python 2.6, but not in 3.0.

In Python 3.0, class instances inherit defaults for some (but not all) of these names from the class (really, from the automatic object superclass), because all classes are “new-style.” Moreover, in 3.0 implicitly invoked attributes for built-in operations like printing and + are not routed through __getattr__ (or its cousin, __getattribute__). New-style classes look up such methods in classes and skip the normal instance lookup entirely.

Here, this means that the __getattr__-based tracing wrapper will automatically trace and propagate operator overloading calls in 2.6, but not in 3.0. To see this, display “x” directly at the end of the preceding interactive session—in 2.6 the attribute __repr__ is traced and the list prints as expected, but in 3.0 no trace occurs and the list prints using a default display for the Wrapper class:

>>> x                                   # 2.6
Trace: __repr__
[4, 5, 6, 7]
>>> x                                   # 3.0
<tracer.Wrapper object at 0x026C07D0>

To work the same in 3.0, operator overloading methods generally need to be redefined redundantly in the wrapper class, either by hand, by tools, or by definition in superclasses. Only simple named attributes will work the same in both versions. We’ll see this version skew at work again in a Private decorator later in this chapter.