Class Decorators

Function decorators proved so useful that the model was extended to allow class decoration in Python 2.6 and 3.0. Class decorators are strongly related to function decorators; in fact, they use the same syntax and very similar coding patterns. Rather than wrapping individual functions or methods, though, class decorators are a way to manage classes, or wrap up instance construction calls with extra logic that manages or augments instances created from a class.

Usage

Syntactically, class decorators appear just before class statements (just as function decorators appear just before function definitions). In symbolic terms, assuming that decorator is a one-argument function that returns a callable, the class decorator syntax:

@decorator # Decorate class class C:

x = C(99) # Make an instance is equivalent to the following—the class is automatically passed to the decorator function, and the decorator's result is assigned back to the class name: class C:

C = decorator(C) # Rebind class name to decorator result x = C(99) # Essentially calls decorator(C)(99)

The net effect is that calling the class name later to create an instance winds up triggering the callable returned by the decorator, instead of calling the original class itself.

Implementation

New class decorators are coded using many of the same techniques used for function decorators. Because a class decorator is also a callable that returns a callable, most combinations of functions and classes suffice.

However it's coded, the decorator's result is what runs when an instance is later created. For example, to simply manage a class just after it is created, return the original class itself:

def decorator(C):

# Process class C return C

To instead insert a wrapper layer that intercepts later instance creation calls, return a different callable object:

def decorator(C):

# Save or use class C

# Return a different callable: nested def, class with_call_, etc.

The callable returned by such a class decorator typically creates and returns a new instance of the original class, augmented in some way to manage its interface. For example, the following inserts an object that intercepts undefined attributes of a class instance:

def decorator(cls): # On @ decoration class Wrapper:

def _init_(self, *args): # On instance creation self.wrapped = cls(*args)

def _getattr_(self, name): # On attribute fetch return getattr(self.wrapped, name) return Wrapper

x = C(6, 7) # Really calls Wrapper(6, 7) print(x.attr) # Runs Wrapper._getattr_, prints "spam"

In this example, the decorator rebinds the class name to another class, which retains the original class in an enclosing scope and creates and embeds an instance of the original class when it's called. When an attribute is later fetched from the instance, it is intercepted by the wrapper's_getattr_and delegated to the embedded instance of the original class. Moreover, each decorated class creates a new scope, which remembers the original class. We'll flesh out this example into some more useful code later in this chapter.

Like function decorators, class decorators are commonly coded as either "factory"

functions that create and return callables, classes that use_init__or__call_methods to intercept call operations, or some combination thereof. Factory functions typically retain state in enclosing scope references, and classes in attributes.

Supporting multiple instances

As with function decorators, with class decorators some callable type combinations work better than others. Consider the following invalid alternative to the class decorator of the prior example:

class Decorator:

def _call_(self, *args): # On instance creation self.wrapped = self.C(*args) return self def _getattr_(self, attrname): # On atrribute fetch return getattr(self.wrapped, attrname)

This code handles multiple decorated classes (each makes a new Decorator instance)

and will intercept instance creation calls (each runs__call_). Unlike the prior version, however, this version fails to handle multiple instances of a given class—each instance creation call overwrites the prior saved instance. The original version does support multiple instances, because each instance creation call makes a new independent wrapper object. More generally, either of the following patterns supports multiple wrapped instances:

def decorator(C): class Wrapper:

self.wrapped = C(*args) return Wrapper class Wrapper: ... def decorator(C): # On @ decoration def onCall(*args): # On instance creation return Wrapper(C(*args)) # Embed instance in instance return onCall

We'll study this phenomenon in a more realistic context later in the chapter; in practice, though, we must be careful to combine callable types properly to support our intent.

# On @ decoration

# On instance creation

+2 -1

Post a comment