Learning Python - Mark Lutz [574]
# Class decorator to trace external instance attribute fetches
def Tracer(aClass): # On @ decorator
class Wrapper:
def __init__(self, *args, **kargs): # On instance creation
self.wrapped = aClass(*args, **kargs) # Use enclosing scope name
def __getattr__(self, attrname):
print('Trace:', attrname) # Catches all but .wrapped
return getattr(self.wrapped, attrname) # Delegate to wrapped object
return Wrapper
@Tracer
class Person: # Person = Tracer(Person)
def __init__(self, name, hours, rate): # Wrapper remembers Person
self.name = name
self.hours = hours
self.rate = rate # In-method fetch not traced
def pay(self):
return self.hours * self.rate
bob = Person('Bob', 40, 50) # bob is really a Wrapper
print(bob.name) # Wrapper embeds a Person
print(bob.pay()) # Triggers __getattr__
When this code is run, the decorator uses class name rebinding to wrap instance objects in an object that produces the trace lines in the following output:
Trace: name
Bob
Trace: pay
2000
Although it’s possible for a metaclass to achieve the same effect, it seems less straightforward conceptually. Metaclasses are designed explicitly to manage class object creation, and they have an interface tailored for this purpose. To use a metaclass to manage instances, we have to rely on a bit more magic. The following metaclass has the same effect and output as the prior decorator:
# Manage instances like the prior example, but with a metaclass
def Tracer(classname, supers, classdict): # On class creation call
aClass = type(classname, supers, classdict) # Make client class
class Wrapper:
def __init__(self, *args, **kargs): # On instance creation
self.wrapped = aClass(*args, **kargs)
def __getattr__(self, attrname):
print('Trace:', attrname) # Catches all but .wrapped
return getattr(self.wrapped, attrname) # Delegate to wrapped object
return Wrapper
class Person(metaclass=Tracer): # Make Person with Tracer
def __init__(self, name, hours, rate): # Wrapper remembers Person
self.name = name
self.hours = hours
self.rate = rate # In-method fetch not traced
def pay(self):
return self.hours * self.rate
bob = Person('Bob', 40, 50) # bob is really a Wrapper
print(bob.name) # Wrapper embeds a Person
print(bob.pay()) # Triggers __getattr__
This works, but it relies on two tricks. First, it must use a simple function instead of a class, because type subclasses must adhere to object creation protocols. Second, it must manually create the subject class by calling type manually; it needs to return an instance wrapper, but metaclasses are also responsible for creating and returning the subject class. Really, we’re using the metaclass protocol to imitate decorators in this example, rather than vice versa; because both run at the conclusion of a class statement, in many roles they are just variations on a theme. This metaclass version produces the same output as the decorator when run live:
Trace: name
Bob
Trace: pay
2000
You should study both versions of these examples for yourself to weigh their tradeoffs. In general, though, metaclasses are probably best suited to class management, due to their design; class decorators can manage either instances or classes, though they may not be the best option for more advanced metaclass roles that we don’t have space to cover in this book (if you want to learn more about decorators and metaclasses after reading this chapter, search the Web or Python’s standard manuals). The next section concludes this chapter with one more common use case—applying operations to a class’s methods automatically.
Example: Applying Decorators to Methods
As we saw in the prior section, because they are both run at the end of a class statement, metaclasses and decorators can often be used interchangeably, albeit with different syntax. The choice between the two is arbitrary in many contexts. It’s also possible to use them in combination, as complementary tools. In this section, we’ll explore an example of