I wrote about Python decorators five years ago. [See Python Decorators, part 1 and Python Decorators, part 2] At the time, they were new to me, and I hadn’t thought of good use cases for them. Or really, even just good ways to use them.
But that changed during the last five years as I’ve had occasions to actually use decorators in code. They are extremely handy in certain situations. Today’s post takes a more useful look at Python decorators.
To keep this from being too long, I’m not going to explain decorators in any detail here. The two posts linked to above explain how they work, as does the Python documentation and various tutorials. Very briefly, Python allows decoration of functions, classes, and class methods.
Given these three function definitions:
002| ”’Decorator function for a function.”’
003|
004| def wrapper (*args, **kwargs):
005| ”’Wrapper function — wraps the decorated function.”’
006|
007| # Call the wrapped function…
008| return function(*args, **kwargs)
009|
010| # Return wrapper…
011| return wrapper
012|
013|
014| def MethodDecorator (method):
015| ”’Decorator function for a class method.”’
016|
017| def wrapper (self, *args, **kwargs):
018| ”’Wrapper function — wraps the decorated method.”’
019|
020| # Call the wrapped method…
021| return method(self, *args, **kwargs)
022|
023| # Return wrapper…
024| return wrapper
025|
026|
027| def ClassDecorator (klass):
028| ”’Decorator function for a class.”’
029|
030| def wrapper (*args, **kwargs):
031| ”’Wrapper function — wraps the decorated class.”’
032|
033| # Call the wrapped class…
034| return klass(*args, **kwargs)
035|
036| # Return wrapper…
037| return wrapper
038|
The above just provides bare-bones do-nothing examples of the three basic types of decorators. We use them to decorate functions, classes, and class methods like this:
002|
003| @eg.ClassDecorator
004| class demo_class:
005| ”’Demo class.”’
006|
007| @eg.MethodDecorator
008| def __init__ (self, a, b, c=None):
009| ”’Demo instance method #1.”’
010| self.a = a
011| self.b = b
012| self.c = 0 if c is None else c
013|
014| @eg.MethodDecorator
015| def reset (self, a=None, b=None, c=None):
016| ”’Demo instance method #2.”’
017| self.a = 0 if a is None else a
018| self.b = 0 if b is None else b
019| self.c = 0 if c is None else c
020|
021| @eg.MethodDecorator
022| def __str__ (self):
023| ”’Demo instance method #3.”’
024| return f’[{self.a}, {self.b}, {self.c}]‘
025|
026| @eg.FunctionDecorator
027| def function_1 (a, b, c, d):
028| ”’Demo function #1.”’
029| return a+b+c+d
030|
031| @eg.FunctionDecorator
032| def function_2 (a, b, c=0, d=0):
033| ”’Demo function #2.”’
034| return a–b–c–d
035|
036| @eg.FunctionDecorator
037| def function_3 (a=1, b=2, c=0, d=0):
038| ”’Demo function #3.”’
039| return a*b*c*d
040|
041| if __name__ == ‘__main__’:
042| obj1 = demo_class(21, 42, 63)
043| obj2 = demo_class(86, 99)
044| print(obj1, obj2, ”, sep=‘\n’)
045|
046| rv1 = function_1(21, 42, 63, 84)
047| rv2 = function_2(12, 24, 36, 48)
048| rv3 = function_3(99, 88, 77, 66)
049| print(rv1, rv2, rv3, ”, sep=‘\n’)
050|
The code doesn’t do anything interesting, just demonstrates the syntax of the three decorator types. Note how the decoration — which starts with an at sign (@) — comes on a line before the function, method, or class definition. These decorators end up creating wrappers around the declared function, method, or class. (This is described in detail in the earlier posts.)
Let’s take a closer (but quick) look at each of the three types:
002| ”’Basic template for a function decorator.”’
003|
004| # Gather some basic info…
005| ftn_name = function.__qualname__
006| ftn_type = function.__class__.__name__
007| print(f’@Decorator: {ftn_name} ({ftn_type})‘)
008|
009| def wrapper (*args, **kwargs):
010| ”’Wrapper function — wraps the decorated function.”’
011| print(f’@[{ftn_name}]: {args} {kwargs}‘)
012| wrapper.count += 1
013|
014| # Call the function…
015| return function(*args, **kwargs)
016|
017| # Return wrapper…
018| wrapper.count = 0
019| return wrapper
020|
A basic decorator function for decorating functions receives a single parameter — the (now anonymous) function following the decorator. In this template, we grab both the function’s name and type and store them in ftn_name and ftn_type (lines #5 and #6). For illustration purposes, we’ll also print those parameters.
Lines #9 to #15 define the wrapper function that surrounds the client function. We take a generic set of arguments, print them for illustration, and increment a counter that keeps track of how many times the function is called. Note that this counter is defined in line #18 as an attribute of the wrapper function itself.
We use the decorator like this:
002|
003| @BasicFunctionDecorator
004| def function1 (a, b, c, d):
005| ”’Demo function #1.”’
006| return a+b+c+d
007|
008| @BasicFunctionDecorator
009| def function2 (a, b, c=0, d=0):
010| ”’Demo function #2.”’
011| return a–b–c–d
012|
013| @BasicFunctionDecorator
014| def function3 (a=1, b=2, c=0, d=0):
015| ”’Demo function #3.”’
016| return a*b*c*d
017|
018| if __name__ == ‘__main__’:
019| print()
020|
021| # Call function 1 a bunch of times…
022| function1(99, 86, 57, 68)
023| function1(21, 42, –1, –1)
024| function1(a=21, b=42, c=–1, d=–1)
025| function1(99, 86, d=57, c=68)
026| print()
027|
028| # Call function 2 a bunch of times…
029| function2(99, 86, 57, 68)
030| function2(21, 42)
031| function2(a=21, b=42)
032| function2(99, 86, d=57, c=68)
033| print()
034|
035| # Call function 3 a bunch of times…
036| function3(99, 86, 57, 68)
037| function3(21, 42)
038| function3(a=21, b=42)
039| print()
040| print()
041|
042| # Dump function stats…
043| print(f’function1: {function1.count} calls‘)
044| print(f’function2: {function2.count} calls‘)
045| print(f’function3: {function3.count} calls‘)
046| print()
047|
Note how (in lines #43 to #45) we access the count attribute of each function — actually of the wrapper function around the client function. This is one key to making productive decorators — they can have accessible attributes.
When run, this prints:
@Decorator: function1 (function)
@Decorator: function2 (function)
@Decorator: function3 (function)
@[function1]: (99, 86, 57, 68) {}
@[function1]: (21, 42, -1, -1) {}
@[function1]: () {'a': 21, 'b': 42, 'c': -1, 'd': -1}
@[function1]: (99, 86) {'d': 57, 'c': 68}
@[function2]: (99, 86, 57, 68) {}
@[function2]: (21, 42) {}
@[function2]: () {'a': 21, 'b': 42}
@[function2]: (99, 86) {'d': 57, 'c': 68}
@[function3]: (99, 86, 57, 68) {}
@[function3]: (21, 42) {}
@[function3]: () {'a': 21, 'b': 42}
function1: 4 calls
function2: 4 calls
function3: 3 calls
Note how the function arguments are printed. At the end, the counters report the number of calls to each function.
Next, let’s look at the class method decorator function:
002| ”’Basic template for a class method decorator.”’
003|
004| # Gather some basic info…
005| ftn_name = method.__qualname__
006| cls_name = method.__class__.__name__
007| print(f’@MethodDecorator: {ftn_name} ({cls_name})‘)
008|
009| def wrapper (self, *args, **kwargs):
010| ”’Wrapper function — wraps the decorated method.”’
011| if not hasattr(self, ‘log’):
012| setattr(self, ‘log’, [])
013|
014| # Write to the log…
015| self.log.append(f’@[{ftn_name}]: {args} {kwargs}‘)
016|
017| # Call the method…
018| return method(self, *args, **kwargs)
019|
020| # Return wrapper…
021| return wrapper
022|
It’s similar to a function decorator. The main difference is that the wrapper function receives the self parameter, just as an instance method does. Since this is a generic method wrapper that can wrap any class method, we take generic arguments (line #9) and pass them to the method (line #18).
This decorator provides a simple logging feature that records the arguments passed to the method. Rather than keeping the log attribute in the wrapper function (as we did above), we check to see if the object instance has a log attribute and add one if it doesn’t (lines #11 and #12). In line #15, we append to the log attribute.
We use the decorator like this:
002|
003| class point:
004| ”’Simple 2D X-Y point class.”’
005|
006| def __init__ (self, x=0.0, y=0.0):
007| ”’New point instance. Default is (0,0)”’
008| self.x = float(x)
009| self.y = float(y)
010|
011| def __repr__ (self):
012| return f’[{self.x:.3f} x {self.y:.3f}]‘
013|
014| @BasicMethodDecorator
015| def moveto (self, x, y):
016| ”’Move point method.”’
017| self.x = float(x)
018| self.y = float(y)
019| return self
020|
021| @BasicMethodDecorator
022| def reset (self):
023| ”’Reset point. Sets point to (0,0).”’
024| self.x = 0.0
025| self.y = 0.0
026| return self
027|
028| @property
029| @BasicMethodDecorator
030| def magnitude (self):
031| ”’Point (vector) magnitude.”’
032| return pow(pow(self.x,2)+pow(self.y,2), 1/2)
033|
034| @BasicMethodDecorator
035| def diff (self, other):
036| ”’Difference between two points.”’
037| return (abs(self.x – other.x), abs(self.y – other.y))
038|
039| @BasicMethodDecorator
040| def dot (self, other):
041| ”’Return dot product of two points.”’
042| return (self.x * other.x) + (self.y * other.y)
043|
044| if __name__ == ‘__main__’:
045| print()
046| pnt1 = point()
047| pnt2 = point(1, 1)
048|
049| pnt1.moveto(+2.71828, +3.14159)
050| pnt2.moveto(–6.62607, –0.510_998_950)
051| print()
052|
053| # Exercise object 1…
054| print(f’{pnt1=}‘)
055| print(f’{pnt1.magnitude=}‘)
056| print(f’{pnt1.dot(pnt1)=}‘)
057| print(f’{pnt1.dot(pnt2)=}‘)
058| print(f’{pnt1.diff(pnt2)=}‘)
059| print()
060|
061| # Exercise object 2…
062| print(f’{pnt2=}‘)
063| print(f’{pnt2.magnitude=}‘)
064| print(f’{pnt2.dot(pnt1)=}‘)
065| print(f’{pnt2.dot(pnt2)=}‘)
066| print(f’{pnt2.diff(pnt1)=}‘)
067| print()
068|
069| # Dump object 1 stats…
070| for ix,line in enumerate(pnt1.log, start=1):
071| print(f’{ix:02d}: {line}‘)
072| print()
073|
074| # Dump object 2 stats…
075| for ix,line in enumerate(pnt2.log, start=1):
076| print(f’{ix:02d}: {line}‘)
077| print()
078|
Note the use of two decorators in lines #28 and #29 — the built-in property decorator followed by our BasicMethodDecorator. I explained using multiple decorators in the first two posts, and I’ll briefly revisit it below. Suffice here to say that it’s essentially transparent.
We create two class instances (lines #46 and #47), exercise their methods (lines #49 to #67), and then dump their log attributes (lines #69 to #77).
When run, this prints:
@MethodDecorator: point.moveto (function)
@MethodDecorator: point.reset (function)
@MethodDecorator: point.magnitude (function)
@MethodDecorator: point.diff (function)
@MethodDecorator: point.dot (function)
pnt1=[2.718 x 3.142]
pnt1.magnitude=4.1543511992247355
pnt1.dot(pnt1)=17.2586338865
pnt1.dot(pnt2)=-19.6168627509305
pnt1.diff(pnt2)=(9.34435, 3.6525889499999997)
pnt2=[-6.626 x -0.511]
pnt2.magnitude=6.645744771792031
pnt2.dot(pnt1)=-19.6168627509305
pnt2.dot(pnt2)=44.16592357180111
pnt2.diff(pnt1)=(9.34435, 3.6525889499999997)
01: @[point.moveto]: (2.71828, 3.14159) {}
02: @[point.magnitude]: () {}
03: @[point.dot]: ([2.718 x 3.142],) {}
04: @[point.dot]: ([-6.626 x -0.511],) {}
05: @[point.diff]: ([-6.626 x -0.511],) {}
01: @[point.moveto]: (-6.62607, -0.51099895) {}
02: @[point.magnitude]: () {}
03: @[point.dot]: ([2.718 x 3.142],) {}
04: @[point.dot]: ([-6.626 x -0.511],) {}
05: @[point.diff]: ([2.718 x 3.142],) {}
The log lists record each method call and record its arguments.
Lastly, let’s look at a class decorator function:
002| ”’Basic template for a class decorator.”’
003|
004| # Gather some basic info…
005| cls_name = klass.__qualname__
006| top_name = klass.__class__.__name__
007| print(f’@ClassDecorator: {cls_name} ({top_name})‘)
008|
009| def wrapper (*args, **kwargs):
010| ”’Wrapper function — wraps the decorated class.”’
011|
012| # Stats instrumentation…
013| wrapper.log.append(f’@[{cls_name}]: {args} {kwargs}‘)
014| wrapper.counter += 1
015|
016| # Call the wrapped class…
017| return klass(*args, **kwargs)
018|
019| # Set up whatever stats attributes desired…
020| wrapper.log = []
021| wrapper.counter = 0
022|
023| # Return wrapper…
024| return wrapper
025|
In this case, the decorator function receives a class object, but otherwise the basics are the same. As with the function decorator above, we create attributes in the wrapper object: a log attribute and a counter attribute. The wrapper function appends to log and increments counter.
We use the decorator like this:
002|
003| @BasicClassDecorator
004| class beer:
005| ”’Demo class.”’
006|
007| def __init__ (self, company, name, abv=0.0, ibu=0.0):
008| ”’New beer instance.”’
009| self.name = name
010| self.company = company
011| self._abv = abv
012| self._ibu = ibu
013|
014| def __str__ (self):
015| return f’Yum! {self.company} {self.name} ({self.abv}/{self.ibu})‘
016|
017| @property
018| def abv (self): return self._abv
019|
020| @property
021| def ibu (self): return self._ibu
022|
023| if __name__ == ‘__main__’:
024|
025| # Make some beer instances…
026| b0 = beer(‘Newcastle’, ‘Brown Ale’, abv=4.7, ibu=18)
027| b1 = beer(‘New Belgium’, ‘Fat Tire’, abv=5.2, ibu=15)
028| b2 = beer(‘New Belgium’, ‘Voodoo Ranger’, abv=7.0, ibu=50)
029| b3 = beer(‘Stone’, ‘IPA’, abv=6.9, ibu=71)
030| print()
031|
032| # Print them…
033| print(b0)
034| print(b1)
035| print(b2)
036| print(b3)
037| print()
038|
039| # Print the beer log…
040| for ix,line in enumerate(beer.log):
041| print(f’{ix:2d}: {line}‘)
042| print()
043|
Note we’ve again used the built-in property decorator to convert the abv and ibu methods to (read-only) properties.
When run, this prints:
@ClassDecorator: beer (type)
Yum! Newcastle Brown Ale (4.7/18)
Yum! New Belgium Fat Tire (5.2/15)
Yum! New Belgium Voodoo Ranger (7.0/50)
Yum! Stone IPA (6.9/71)
0: @[beer]: ('Newcastle', 'Brown Ale') {'abv': 4.7, 'ibu': 18}
1: @[beer]: ('New Belgium', 'Fat Tire') {'abv': 5.2, 'ibu': 15}
2: @[beer]: ('New Belgium', 'Voodoo Ranger') {'abv': 7.0, 'ibu': 50}
3: @[beer]: ('Stone', 'IPA') {'abv': 6.9, 'ibu': 71}
Note how the BasicMethodDecorator attached the log attribute to each instance, whereas the BasicClassDecorator attaches log to the wrapper, which makes it appear as an attribute of the class.
Note an important aspect of using class decorators: Class attributes become essentially inaccessible. Unless you add features to the wrapper to pass through attribute requests, these will fail. For example:
002|
003| @BasicClassDecorator
004| class chessboard:
005| ”’Demo class with class variables.”’
006| Rows = 8
007| Cols = 8
008|
009| def __init__ (self):
010| ”’New chessboard instance.”’
011| self.board = [[0 for col in range(chessboard.Cols)]
012| for row in range(chessboard.Rows)]
013|
014| if __name__ == ‘__main__’:
015| print()
016|
017| # Make a chessboard object…
018| brd = chessboard()
019| print(brd)
020| print()
021|
Note how lines #11 and #12 access the class attribute chessboard.Cols and chessboard.Rows. The first one encountered raises an error because Rows and Cols are not attributes of the decorator wrapper function object that’s bound to the name chessboard.
When run, this prints:
@ClassDecorator: chessboard (type) Traceback (most recent call last): File "C:\blog\hcc\fragment.py", line 14, in <module> brd = chessboard() File "C:\blog\hcc\examples.py", line 104, in wrapper return klass(*args, **kwargs) File "C:\blog\hcc\fragment.py", line 10, in __init__ for row in range(chessboard.Rows)] AttributeError: 'function' object has no attribute 'Rows'
If we instead access the attributes through the instance — by changing chessboard to self — the code works fine. So, one way to access class attributes is through instances of the class, either on the instance itself or on the class through the instance’s __class__ attribute.
It is possible to access the wrapped class through the wrapper but requires digging into the wrapper function:
002|
003| @BasicClassDecorator
004| class chessboard:
005| ”’Demo class with class variables.”’
006| Rows = 8
007| Cols = 8
008|
009| def __init__ (self):
010| ”’New chessboard instance.”’
011| self.board = [[0 for col in range(self.Cols)]
012| for row in range(self.Rows)]
013|
014| if __name__ == ‘__main__’:
015| print()
016|
017| # Make a chessboard object…
018| brd = chessboard()
019| print(brd)
020| print()
021|
022| nam = chessboard.__closure__[0].cell_contents
023| cls = chessboard.__closure__[1].cell_contents
024| print(f’{nam = }‘)
025| print(f’{cls = }‘)
026| print(f’{cls.Rows = }‘)
027| print(f’{cls.Cols = }‘)
028| print()
029|
When this is run, it prints:
@ClassDecorator: chessboard (type) <__main__.chessboard object at 0x00000228AD99EBA0> nam = 'chessboard' cls = <class '__main__.chessboard'> cls.Rows = 8 cls.Cols = 8
It’s something to keep in mind with class wrappers. Function and method wrappers don’t have the issue, because those objects don’t usually have other attributes of interest. Of course, in cases where they do, steps similar to the above are necessary.
The above should give you a good idea of the possibilities. We’ve already seen how to include useful attributes in the wrapper object. Method decorators provide a means to modify methods (for instance, the built-in method decorators classmethod, staticmethod, and property. We’ll look at more decorators below but first let’s return to the notion of stacking decorators.
There are several instances of it above, and I’ve already said it’s essentially transparent (the exception is when the decorator needs access to the object it thinks it’s decorating rather than another decorator).
We can explore this a little with these two decorator functions:
002| ”’Demo Decorator #1.”’
003| print(f’Decorator1({name})‘)
004|
005| def decorator (function):
006| ”’Actual decorator function.”’
007|
008| # Gather some basic info…
009| ftn_name = function.__qualname__
010| ftn_type = function.__class__.__name__
011| print(f’@[{name}]: {ftn_name} ({ftn_type})‘)
012|
013| def wrapper (*args, **kwargs):
014| ”’Wrapper function.”’
015| print(f’[{name}].args: {args}‘)
016| print(f’[{name}].kwargs: {list(kwargs)}‘)
017|
018| # Call wrapped function and return result…
019| return function(*args, **kwargs)
020|
021| # Return wrapper…
022| return wrapper
023|
024| # Return decorator function…
025| return decorator
026|
027| def Decorator2 (function):
028| ”’Demo Decorator #2.”’
029|
030| ftn_name = function.__qualname__
031| ftn_type = function.__class__.__name__
032| print(f’@Decorator2: {ftn_name} ({ftn_type})‘)
033|
034| def wrapper (*args, **kwargs):
035| ”’Wrapper function.”’
036| print(f’decorator2.args: {args}‘)
037| print(f’decorator2.kwargs: {list(kwargs)}‘)
038|
039| # Call wrapped function and return result…
040| return function(*args, **kwargs)
041|
042| # Return wrapper…
043| return wrapper
044|
The second one (Decorator2, lines #27 to #43) is a basic decorator function such seen above. The first one (Decorator1, lines #1 to #25) has a parameterized decorator (discussed in the earlier posts). Parameterized decorators have doubly nested functions. The first, inner (lines #5 to #22), is the actual decorator function. The outer function (Decorator1) must return it after processing the arguments (line #25).
Inside the inner function is the wrapper function (lines #13 to #19), which is similar to previous wrapper functions (and the one in Decorator2, lines #34 to #40).
We use multiple decorators like this:
002|
003| @Decorator1(‘Agatha’)
004| @Decorator2
005| def function1(*args, **kwargs):
006| ”’ Demo function #1.”’
007| …
008| return
009|
010| @Decorator2
011| @Decorator1(‘Bertha’)
012| def function2(*args, **kwargs):
013| ”’ Demo function #2.”’
014| …
015| return
016|
017| if __name__ == ‘__main__’:
018| print()
019|
020| function1(21,42,fname=‘fred’,fpath=r’C:\demo\hcc\python’)
021| print()
022|
023| function1(1,2,3,4,5,x=0,y=0,z=0)
024| print()
025|
026| function2(1,2,3,4,5)
027| print()
028|
029| function2(cmd=‘reset’,nup=‘SEA’,pir=‘MIL’)
030| print()
031|
The decorators can be stacked in any order. When run, this prints:
Decorator1(Agatha) @Decorator2: function1 (function) @[Agatha]: Decorator2.<locals>.wrapper (function) Decorator1(Bertha) @[Bertha]: function2 (function) @Decorator2: Decorator1.<locals>.decorator.<locals>.wrapper (function) [Agatha].args: (21, 42) [Agatha].kwargs: ['fname', 'fpath'] decorator2.args: (21, 42) decorator2.kwargs: ['fname', 'fpath'] [Agatha].args: (1, 2, 3, 4, 5) [Agatha].kwargs: ['x', 'y', 'z'] decorator2.args: (1, 2, 3, 4, 5) decorator2.kwargs: ['x', 'y', 'z'] decorator2.args: (1, 2, 3, 4, 5) decorator2.kwargs: [] [Bertha].args: (1, 2, 3, 4, 5) [Bertha].kwargs: [] decorator2.args: () decorator2.kwargs: ['cmd', 'nup', 'pir'] [Bertha].args: () [Bertha].kwargs: ['cmd', 'nup', 'pir']
Again, the caveat is the special case where the decorator expects the class (or method or function) and doesn’t expect another decorator. If that matters, the case can be detected and handled (albeit somewhat tediously).
Here’s a function logging decorator that also keeps track of how much time is spent in the function:
002|
003| def FunctionLogger (function):
004| ”’Decorator for logging and timing function calls.”’
005|
006| # Gather some basic info…
007| ftn_name = function.__qualname__
008| ftn_type = function.__class__.__name__
009| print(f’@Logger: {ftn_name} ({ftn_type})‘)
010|
011| class wrapper (list):
012| ”’A wrapper class (rather than a function).”’
013|
014| def __init__ (self):
015| ”’Initialize wrapper instance.”’
016| self.count = 0 # counts calls to wrapped function
017| self.timer = 0 # accumulates time spent in function
018|
019| def __call__ (self, *args, **kwargs):
020| ”’Decorator class instances must be callable!.”’
021| self.append(f’@[{self.timer:12d}]: {args} {kwargs}‘)
022| self.count += 1
023|
024| # Start timer…
025| t0 = perf_counter_ns()
026|
027| # Call wrapped function…
028| retv = function(*args, **kwargs)
029|
030| # Add time spent in function to timer…
031| self.timer += perf_counter_ns() – t0
032|
033| # Return what the function returned…
034| return retv
035|
036| @property
037| def milliseconds (self): return self.timer/pow(10,6)
038|
039| @property
040| def seconds (self): return self.timer/pow(10,9)
041|
042| def __str__ (self): return f’Logger:{ftn_name}‘
043|
044| # Return wrapper…
045| return wrapper()
046|
This decorator adds a new wrinkle by using a wrapper class rather than a wrapper function. This works fine so long as the class we define implements __call__, because a wrapper must be callable. Using a class just makes it easier to add properties and methods to the wrapper. Note how, in line #45, the decorator returns an instance of the class. The instance is the actual wrapper.
Note also that we’re extending the Python list class, so the wrapper will be a list object containing the log lines along with the count and timer attributes as well as the milliseconds and seconds properties.
We use the decorator like this:
002| from random import random
003| from examples import FunctionLogger
004|
005| @FunctionLogger
006| def function1 (a, b, c, d):
007| ”’Demo function #1.”’
008| sleep(random() * 2.0) # random pause
009| return a+b+c+d
010|
011| @FunctionLogger
012| def function2 (a, b, c, d):
013| ”’Demo function #2.”’
014| sleep(random() * 6.0) # random pause
015| return a*b*c*d
016|
017| if __name__ == ‘__main__’:
018| print()
019|
020| # Call function 1 a bunch of times…
021| function1(0, 0, 0, 0)
022| function1(a=1, b=2, c=3, d=4)
023| function1(1, –1, 100, –99)
024| function1(21, 42, d=63, c=84)
025| function1(–9, 99, –999, 9999)
026| function1(9, 99, 999, 9999)
027|
028| # Call function 2 a bunch of times…
029| function2(0, 0, 0, 0)
030| function2(a=1, b=2, c=3, d=4)
031| function2(1, –1, 100, –99)
032| function2(21, 42, 63, 84)
033|
034| def dump_stats (ftn):
035| ”’Dump function stats.”’
036| print(f’{ftn}: {ftn.count} calls, {ftn.seconds} secs‘)
037| for ix,line in enumerate(ftn, start=1):
038| print(f’{ix:02d}: {line}‘)
039| print()
040|
041| dump_stats(function1)
042| dump_stats(function2)
043|
The assumption is that we want to log and time function1 and function2, so we add the FunctionLogger decorator. Then we call both functions a bunch of times, after which we dump the stats.
When run, this prints:
@Logger: function1 (function)
@Logger: function2 (function)
Logger:function1: 6 calls, 2.830552 secs
01: @[ 0]: (0, 0, 0, 0) {}
02: @[ 354015100]: () {'a': 1, 'b': 2, 'c': 3, 'd': 4}
03: @[ 430028000]: (1, -1, 100, -99) {}
04: @[ 1115059700]: (21, 42) {'d': 63, 'c': 84}
05: @[ 2002259500]: (-9, 99, -999, 9999) {}
06: @[ 2078706000]: (9, 99, 999, 9999) {}
Logger:function2: 4 calls, 16.1943 secs
01: @[ 0]: (0, 0, 0, 0) {}
02: @[ 2353524900]: () {'a': 1, 'b': 2, 'c': 3, 'd': 4}
03: @[ 6104884700]: (1, -1, 100, -99) {}
04: @[ 10597902200]: (21, 42, 63, 84) {}
Another useful debugging tool is an error trapper/logger:
002| ”’Decorator function to catch and log errors.”’
003|
004| def decorator (function):
005| ”’Actual decorator function.”’
006|
007| # Gather some basic info…
008| ftn_name = function.__qualname__
009| ftn_type = function.__class__.__name__
010| print(f’@ErrorRecorder: {ftn_name} ({ftn_type})‘)
011|
012| def wrapper (*args, **kwargs):
013| ”’Wrapper function.”’
014| try:
015| wrapper.count += 1
016|
017| # Call wrapped function…
018| return function(*args, **kwargs)
019|
020| except Exception as e:
021| wrapper.errs.append((wrapper.count, e))
022|
023| # On error, return default value…
024| return default_return_value
025|
026| # Create and initialize wrapper attributes…
027| wrapper.fctn = function
028| wrapper.count = 0
029| wrapper.errs = []
030|
031| # Return the wrapper function…
032| return wrapper
033|
034| # Return decorator function…
035| return decorator
036|
To keep things simple, this is a basic function decorator, but what differs is the try-except block in the wrapper. This catches any errors raised by the wrapped function and logs them. Note that the decorator takes an argument — a value to return when the function raises an error — so it has a nested decorator function (lines #4 to #32) with its own nested wrapper function (lines #12 to #24).
We use the decorator like this:
002| from examples import ErrorRecorder
003|
004| @ErrorRecorder(None)
005| def foobar (a, b, c):
006| ”’Example function.”’
007| return (a*log(b))/c
008|
009| if __name__ == ‘__main__’:
010| print()
011|
012| print(f’ 1: result={foobar()}‘)
013| print(f’ 2: result={foobar(1,2,3)}‘)
014| print(f’ 3: result={foobar(7,8,9)}‘)
015| print(f’ 4: result={foobar(–1,3,3.14159)}‘)
016| print(f’ 5: result={foobar(2.718, 2.718, 2.718)}‘)
017| print(f’ 6: result={foobar(0,4,5)}‘)
018| print(f’ 7: result={foobar(3,0,5)}‘)
019| print(f’ 8: result={foobar(3,4,0,–1)}‘)
020| print(f’ 9: result={foobar(3,4)}‘)
021| print(f’10: result={foobar(3,4,5)}‘)
022| print(f’11: result={foobar(0,4,0)}‘)
023| print(f’12: result={foobar(0,0,0)}‘)
024| print(f’13: result={foobar(4,1,1)}‘)
025| print(f’14: result={foobar(“4”,“2”,“0”)}‘)
026| print(f’15: result={foobar(0,7,None)}‘)
027| print()
028|
029| print(f’function-name: {foobar.fctn.__qualname__}‘)
030| print(f’calls: {foobar.count}‘)
031| print(f’errs: {len(foobar.errs)}‘)
032| print()
033|
034| print(f’errors:‘)
035| for ix,err in enumerate(foobar.errs, start=1):
036| print(f’{ix:2d}: @{err[0]} {err[1]}‘)
037| print()
038|
Fifteen calls to the foobar function, which does some math (lines #12 to #26). (Because of the math, certain arguments will cause an exception.) After exercising the function, we print some wrapper stats and then dump any errors logged.
When run, this prints:
@ErrorRecorder: foobar (function) 1: result=None 2: result=0.23104906018664842 3: result=1.617343421306539 4: result=-0.34969944794454716 5: result=0.999896315728952 6: result=0.0 7: result=None 8: result=None 9: result=None 10: result=0.8317766166719343 11: result=None 12: result=None 13: result=0.0 14: result=None 15: result=None function-name: foobar calls: 15 errs: 8 errors: 1: @1 foobar() missing 3 required positional arguments: 'a', 'b', and 'c' 2: @7 math domain error 3: @8 foobar() takes 3 positional arguments but 4 were given 4: @9 foobar() missing 1 required positional argument: 'c' 5: @11 float division by zero 6: @12 math domain error 7: @14 must be real number, not str 8: @15 unsupported operand type(s) for /: 'float' and 'NoneType'
So, we know that, of the 15 calls, #1, #7–#9, and #11–#15 all raised exceptions, and we can see exactly what those exceptions were.
Python allows the attachment of type annotations to variable and function declarations (for a good overview, see Annotations Best Practices).
Starting with Python 3.10, functions, classes, and modules are guaranteed to have the __annotations__ attribute (a dict) containing any annotations for the declared object. That means we can easily access a function’s annotations (if any):
002| ”’Demo function with parameter and return annotations.”’
003| return (a, b, c, d)
004|
005| if __name__ == ‘__main__’:
006| print()
007|
008| for item,value in foobar.__annotations__.items():
009| # Let’s be grammatically correct…
010| vowel = ‘n’ if value.__name__[0] in ‘aeiou’ else ”
011|
012| # Print name and type…
013| print(f’{item} is a{vowel} {value.__name__}‘)
014|
015| print()
016|
If a parameter (or the function itself) doesn’t have a type annotation, there is no corresponding entry in the __annotations__ dictionary.
When run, the above prints:
a is an int b is a float c is a str d is a list return is a tuple
Note how the function’s return value annotation uses the name “return” (a name not allowed for a variable because it’s a Python keyword).
Here’s a decorator function that uses annotations to automatically check the variable types of function arguments:
002| ”’Type checking decorator.”’
003| getname = lambda obj: obj.__name__
004|
005| # Get parameter annotations…
006| anns = ftn.__annotations__
007| ins = [f’{k}:{getname(v)}‘ for k,v in anns.items() if k != ‘return’]
008| ret = anns[‘return’] if ‘return’ in anns else type(None)
009| print(f’TypeCheck: {getname(ftn)}({“,”.join(ins)}):{getname(ret)}‘)
010|
011| def wrapper (*args, **kwargs):
012| ”’Wrapper function.”’
013|
014| # Check args types…
015| for arg,nam in zip(args, anns.keys()):
016| t0 = type(arg).__name__ # name of argument type
017| t1 = getname(anns[nam]) # name of parameter type
018| check = ‘OK’ if type(arg) == anns[nam] else f’[{t0}≠{t1}]‘
019| print(f’{getname(ftn)}: arg[{nam}]:{t1} = {arg}:{t0} {check}‘)
020|
021| # Check kwargs types…
022| for nam,val in kwargs.items():
023| t0 = type(val).__name__
024| t1 = getname(anns[nam]) # name of parameter type
025| check = ‘OK’ if type(val) == anns[nam] else f’[{t0}≠{t1}]‘
026| print(f’{getname(ftn)}: kwarg[{nam}]:{t1} = {val}:{t0} {check}‘)
027| print()
028|
029| # Call the wrapped function…
030| return ftn(*args, **kwargs)
031|
032| # Return the wrapper…
033| return wrapper
034|
The structure is similar to other decorator functions but with a bit more work done. Lines #6 to #9 get the __annotations__ attribute of the passed function and list its contents.
The wrapper (lines #11 to #30) uses the annotations to check the function argument types. First it checks any positional arguments (lines #14 to #19). Then it checks any keyword arguments (line #21 to #27). Note that, for demonstrations purposes, the code only lists the input arguments and whether they are “OK” or type violations. A serious implementation would either log the issues or raise an exception.
Note also that we’re not checking the return type. That’s left as a reader exercise.
We use the decorator like this:
002|
003| @TypeCheck
004| def foobar (a:int=0, b:int=0, c:float=0.0) -> list:
005| ”’Example function.”’
006| return [a, b, c]
007|
008| if __name__ == ‘__main__’:
009| print()
010|
011| rv = foobar(42, ’21’, 3.1415)
012| rv = foobar(63, c=3)
013| rv = foobar(b=21, a=4.2, c=3.1415)
014| rv = foobar(’42’, [21], tuple(‘3.14’))
015|
When run, this prints:
TypeCheck: foobar(a:int,b:int,c:float):list
foobar: arg[a]:int = 42:int OK
foobar: arg[b]:int = 21:str [str≠int]
foobar: arg[c]:float = 3.1415:float OK
foobar: arg[a]:int = 63:int OK
foobar: kwarg[c]:float = 3:int [int≠float]
foobar: kwarg[b]:int = 21:int OK
foobar: kwarg[a]:int = 4.2:float [float≠int]
foobar: kwarg[c]:float = 3.1415:float OK
foobar: arg[a]:int = 42:str [str≠int]
foobar: arg[b]:int = [21]:list [list≠int]
foobar: arg[c]:float = ('3', '.', '1', '4'):tuple [tuple≠float]
You could combine type-checking, error-trapping, and function timing all into one grand debugging decorator.
That’s probably more than enough for this time. Next time we’ll look at some more involved examples of decorators.
Link: Zip file containing all code fragments used in this post.
∅
ATTENTION: The WordPress Reader strips the style information from posts, which can destroy certain important formatting elements. If you’re reading this in the Reader, I highly recommend (and urge) you to [A] stop using the Reader and [B] always read blog posts on their website.
This post is: Python Decorators, redux
Pingback: Python Decorators, more | The Hard-Core Coder