Decorators We have discussed decorators before. Typically we described them as a class that takes an argument that supports some protocol (methods) and returns an object that supports the same protocol. The decorator object performs a bit different than the decorated object. The decorator object stores a reference to the decorated object and calls it when necessary. The examples in this lecture, and the the Check_Annotations class in Programming Assignment #4, call decorate functions by using the __call__ protocol to decorate/augment how functions are called. ------------------------------------------------------------------------------ Special Python Syntax for Decorators If Decorator is the name of a decorator,we can use it to decorate a function, by writing either def f(params-annotation) -> result-annotation: ... f = Check_Annotation(f) or @Check_Annotation def f(params-annotation) -> result-annotation: We can use multiple decorators. The meaning of @D1 @D2 def f(...) ... is f = D1(D2(f)) so D1 decorates the result of D2 decorating f. ------------------------------------------------------------------------------ Example Decorators Here are three decorators for functions (three are classes; one can be written easily as a function too) Track_Calls remembers the function it is decorating and resets a counter to 0; the decorator object overloads the __call__ method so that all calls to the decorator object increment a counter and then actually call the decorated function (which if recursive...). Once the function's values is returned, the counter instance can be accessed and reset for a further call; the need/ability to access this instance variable is why Track_Calls is a class, not a function. class Track_Calls: def __init__(self,f): self._f = f self.calls = 0 def __call__(self,*args,**kargs): # bundle arbitrary arguments self.calls += 1 return self._f(*args,**kargs) # unbundle arbitrary arguments Memoize remembers the function it is decorating and initializes a dict to {}. It will use this dict to cache (keep track of) the arguments to calls and the value ultimately returned by the function. The decorator object overloads the __call__ method so that all calls to the decorator object check to see if the arguments are already cached in the dict, and if so their associated value is returned; if not the funcition is called, its answer is cached in the dict with the function's arguments, and the answer is returned. In this way, a function never has to compute the same value twice. This might be useful for multiply recursive calls, as in the fibonacci functioin. class Memoize: def __init__(self,f): self._f = f self.cache = {} def __call__(self,*args): if args in self.cache: return self.cache[args] else: answer = self._f(*args) self.cache[args] = answer return answer We can also write memoize as a function, to return a wrapper function (it can be named anything) that does the same operations as the class above. def memoize(f): cache = {} def wrapper(*args): if args in cache: return cache[args] else: answer = f(*args) cache[args] = answer return answer return wrapper Illustrate_Recursive remembers the function it is decorating and initializes a tracing variable to fals. The decorator object overloads the __call__ method so that all calls to the decorator object just return the result of calling the decorated function (if tracing is off). Calling ...illustrate(...) calls the illustrate method, which sets up for tracing, and then uses __call__ to trace all entrances and exists to the decorated function printing indented/outdented information for each function call/return. class Illustrate_Recursive: def __init__(self,f): self._f = f self._trace = False def illustrate(self,*args,**kargs): self._indent = 0 self._trace = True answer = self.__call__(*args,**kargs) self._trace = False return answer def __call__(self,*args,**kargs): if self._trace: if self._indent == 0: print('Starting recursive illustration'+30*'-') print (self._indent*"."+"calling", self._f.__name__+str(args)+str(kargs)) self._indent += 2 answer = self._f(*args) if self._trace: self._indent -= 2 print (self._indent*"."+self._f.__name__+str(args)+str(kargs)+" returns", answer) if self._indent == 0: print('Ending recursive illustration'+30*'-') return answer Here is an example of illustrating how the fibonacci function is decorated and what information it produces when called with the argument 5. @Illustrate_Recursive def fib(n): assert n>=0, 'fib cannot have negative n('+str(n)+')' if n == 0: return 1 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) print(fib.illustrate(5)) Starting recursive illustration------------------------------ calling fib(5,){} ..calling fib(4,){} ....calling fib(3,){} ......calling fib(2,){} ........calling fib(1,){} ........fib(1,){} returns 1 ........calling fib(0,){} ........fib(0,){} returns 1 ......fib(2,){} returns 2 ......calling fib(1,){} ......fib(1,){} returns 1 ....fib(3,){} returns 3 ....calling fib(2,){} ......calling fib(1,){} ......fib(1,){} returns 1 ......calling fib(0,){} ......fib(0,){} returns 1 ....fib(2,){} returns 2 ..fib(4,){} returns 5 ..calling fib(3,){} ....calling fib(2,){} ......calling fib(1,){} ......fib(1,){} returns 1 ......calling fib(0,){} ......fib(0,){} returns 1 ....fib(2,){} returns 2 ....calling fib(1,){} ....fib(1,){} returns 1 ..fib(3,){} returns 3 fib(5,){} returns 8 Ending recursive illustration-----------------------------