Learning Python - Mark Lutz [543]
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
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. * * * Class Blunders II: Retaining Multiple Instances Curiously, the decorator function in this example can almost be coded as a class instead of a function, with the proper operator overloading protocol. The following slightly simplified alternative works similarly because its __init__ is triggered when the @ decorator is applied to the class, and its __call__ is triggered when a subject class instance is created. Our objects are really instances of Tracer this time, and we essentially just trade an enclosing scope reference for an instance attribute here: class Tracer: def __init__(self, aClass): # On @decorator self.aClass = aClass # Use instance attribute def __call__(self, *args): # On instance creation self.wrapped = self.aClass(*args) # ONE (LAST) INSTANCE PER CLASS! return self def __getattr__(self, attrname): print('Trace: ' + attrname) return getattr(self.wrapped, attrname) @Tracer # Triggers __init__ class Spam: # Like: Spam = Tracer(Spam) def display(self): print('Spam!' * 8) ... food = Spam() # Triggers __call__ food.display() # Triggers __getattr__ As we saw in the abstract earlier, though, this class-only alternative handles multiple classes as before, but it won’t quite work for multiple instances of a given class: each instance construction call triggers __call__, which overwrites the prior instance. The net effect is that Tracer saves just one instance—the last one created. Experiment with this yourself to see how, but here’s an example of the problem: @Tracer class Person: # Person = Tracer(Person) def __init__(self, name): # Wrapper bound to Person self.name = name bob = Person('Bob') # bob is really a Wrapper print(bob.name) # Wrapper embeds a Person Sue = Person('Sue') print(sue.name) # sue overwrites bob print(bob.name) # OOPS: now bob's name is 'Sue'! This code’s output follows—because this tracer only has a single shared instance, the second overwrites the first: Trace: name Bob Trace: name Sue Trace: name Sue The problem here is bad state retention—we make one decorator instance per class, but not per class instance, such that only the last instance is retained. The solution, as in our prior class blunder for decorating methods, lies in abandoning class-based