Creating a method function decorator

A decorator for a method of a class definition is identical to a decorator for a standalone function. While it's used in a different context, it will be defined like any other decorator. One small consequence of the different context is that we often, must explicitly name the self variable in decorators intended for methods.

One application for method decoration is to produce an audit trail for object state changes. Business applications often create stateful records; commonly, these are represented as rows in a relational database. We'll look at object representation in Chapter 10, Serializing and Saving – JSON, YAML, Pickle, CSV, and XML, Chapter 11, Storing and Retrieving Objects via Shelve, and Chapter 12, Storing and Retrieving Objects via SQLite.

When we have stateful records, the state changes often need to be auditable. An audit can confirm that appropriate changes have been made to the records. In order to do the audit, the before and after version of each record must be available somewhere. Stateful database records are a long-standing tradition but are not in any way required. Immutable database records are a viable design alternative.

When we design a stateful class, any setter method will cause a state change. If we do this, we can fold in an @audit decorator that can track changes to the object so that we have a proper trail of changes. We'll create an audit log via the logging module. We'll use the __repr__() method function to produce a complete text representation that can be used to examine changes.

The following is an audit decorator:

def audit(method: F) -> F:

@functools.wraps(method)
def wrapper(self, *args, **kw):
template = "%s before %s after %s"
audit_log = logging.getLogger("audit")
before = repr(self) # preserve state as text
try:
result = method(self, *args, **kw)
except Exception as e:
after = repr(self)
audit_log.exception(template, method.__qualname__, before, after)
raise
after = repr(self)
audit_log.info(template, method.__qualname__, before, after)
return result

return cast(F, wrapper)

This audit trail works by creating text mementos of the before setting and after setting state of the object. After capturing the before state, the original method function is applied. If there was an exception, an audit log entry includes the exception details. Otherwise, an INFO entry is written with the qualified name of the method name, the before memento, and the after memento of the object.

The following is a modification of the Hand class that shows how we'd use this decorator:

class Hand:

def __init__(self, *cards: CardDC) -> None:
self._cards = list(cards)

@audit
def __iadd__(self, card: CardDC) -> "Hand":
self._cards.append(card)
self._cards.sort(key=lambda c: c.rank)
return self

def __repr__(self) -> str:
cards = ", ".join(map(str, self._cards))
return f"{self.__class__.__name__}({cards})"

This definition modifies the __iadd__() method function so that adding a card becomes an auditable event. This decorator will perform the audit operation, saving text mementos of Hand before and after the operation.

This use of a method decorator makes a visible declaration that a particular method will make a significant state change. We can easily use code reviews to be sure that all of the appropriate method functions are marked for audit like this. 

In the event that we want to audit object creation as well as state change, we can't use this audit decorator on the __init__() method function. That's because there's no before image prior to the execution of __init__(). There are two things we can do as a remedy to this, as follows:

  • We can add a __new__() method that ensures that an empty _cards attribute is seeded into the class as an empty collection.
  • We can tweak the audit() decorator to tolerate AttributeError that will arise when __init__() is being processed.

The second option is considerably more flexible. We can do the following:

try: 
    before = repr(self) 
except AttributeError as e: 
    before = repr(e) 

This would record a message such as AttributeError: 'Hand' object has no attribute '_cards' for the before status during initialization.

In the next section, we'll see how to create a class decorator.

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

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