Last time I explored a number of useful Python decorators. The post was a revisit to a topic I first posted about five years ago [see Python Decorators, part 1 and Python Decorators, part 2]. Back then I didn’t really know what to do with decorators, but I could see they were useful.
Since then, I’ve found many applications for them, hence the revisit to the topic. In this post, we’ll continue looking at useful applications. If nothing else, they may provide some ideas for decorators of your own.
Jumping right in, the last post featured decorators that logged any arguments passed to the decorated function. Those logged anything found in *args (positional arguments) and anything found in **kwargs (keyword arguments). Which works for general debugging, but what if your interest is in only one argument? This might be nice if the function takes lots of arguments or if its arguments can be especially long. You might not want to log that much data.
Here’s a decorator the logs just one argument:
002| ”’Decorator for logging a specific parameter.”’
003| print(f’DataRecorder({which})‘)
004| if not (isinstance(which,int) or isinstance(which,str)):
005| raise TypeError(f’Parameter spec must be int or string.‘)
006|
007| def decorator (function):
008| ”’Actual decorator function returned in initialization.”’
009| ftn_name = function.__qualname__
010| print(f’@DataRecorder: {ftn_name}‘)
011|
012| def wrapper (*args, **kwargs):
013| ”’Wrapper function.”’
014| wrapper.count += 1
015| print(f’{ftn_name}[{wrapper.count}]: {args} {kwargs}‘)
016|
017| # An integer indexes a parameter in args…
018| if isinstance(which,int) and (which < len(args)):
019| arg = args[which]
020| wrapper.data.append((wrapper.count, arg))
021|
022| # A string names a parameter in kwargs…
023| elif isinstance(which,str) and (which in kwargs):
024| arg = kwargs[which]
025| wrapper.data.append((wrapper.count, arg))
026|
027| # Call wrapped function and return its result…
028| return function(*args, **kwargs)
029|
030| # Initialize the wrapper object’s special properties…
031| wrapper.count = 0
032| wrapper.data = []
033|
034| # Return the function wrapper…
035| return wrapper
036|
037| # Return the decorator…
038| return decorator
039|
The decorator takes a parameter, which, that determines which parameter of the decorated function to log. If which is an integer, it indexes a positional argument (lines #17 to #20). If it’s a string, it names a keyword argument (lines #22 to #25).
As usual, a decorator taking one or more parameters is double-nested. The DataRecorder function returns the inner decorator function (line #38), and that function returns the inner-inner wrapper function (line #35).
As in previous examples, this decorator stores the logging stats in attributes it creates — count:int and data:list — on the wrapper function object.
We use the decorator like this:
002|
003| @DataRecorder(2)
004| def function1 (x=0, y=0, z=0):
005| ”’Demo function #1.”’
006| return x*y*z
007|
008| @DataRecorder(‘y’)
009| def function2 (x=0, y=0, z=0):
010| ”’Demo function #2.”’
011| return x*y*z
012|
013| if __name__ == ‘__main__’:
014| print()
015|
016| # Call function 1 a bunch of times…
017| function1()
018| function1(1)
019| function1(1,2)
020| function1(1,2,3)
021| function1(4,5,6)
022| function1(7,8,9)
023| print()
024|
025| # Call function 2 a bunch of times…
026| function2()
027| function2(x=1,y=2)
028| function2(x=2,z=9)
029| function2(3, y=5)
030| function2(x=7,y=8,z=9)
031| print()
032|
033| # Dump function 1 stats…
034| vals = “, “.join(f’[{ix}]{v}‘ for ix,v in function1.data)
035| print(f’function1: {function1.count} calls‘)
036| print(f’values: {vals}‘)
037| print()
038|
039| # Dump function 2 stats…
040| vals = “, “.join(f’[{ix}]{v}‘ for ix,v in function2.data)
041| print(f’function2: {function2.count} calls‘)
042| print(f’values: {vals}‘)
043| print()
044|
When run, this prints:
DataRecorder(2)
@DataRecorder: function1
DataRecorder(y)
@DataRecorder: function2
function1[1]: () {}
function1[2]: (1,) {}
function1[3]: (1, 2) {}
function1[4]: (1, 2, 3) {}
function1[5]: (4, 5, 6) {}
function1[6]: (7, 8, 9) {}
function2[1]: () {}
function2[2]: () {'x': 1, 'y': 2}
function2[3]: () {'x': 2, 'z': 9}
function2[4]: (3,) {'y': 5}
function2[5]: () {'x': 7, 'y': 8, 'z': 9}
function1: 6 calls
values: [4]3, [5]6, [6]9
function2: 5 calls
values: [2]2, [4]5, [5]8
The number in square brackets is the number of the call. At the end (last five lines), the logged parameter values are listed with the call number prepended.
With a bit more code, we can improve the decorator to handle multiple arguments. And rather than logging parameters, let’s actually change them. The requirement here is for a decorator that allows applying an arbitrary function to any input(s), positional or keyword:
002| ”’Decorator for string handling.”’
003|
004| def decorator (function):
005| ”’Actual decorator function.”’
006| ftn_name = function.__qualname__
007| print(f’@StringHandler: {ftn_name}‘)
008|
009| def wrapper (*args, **kwargs):
010| ”’Wrapper function.”’
011| #print(f’StringHandler: {args} {kwargs}’)
012| ftns = list(ftn_list) # (need mutable copies)
013| _args = list(args)
014|
015| # Process positional arguments…
016| for ax in arg_indexes:
017| s = _args[ax]
018| f = ftns.pop(0)
019| _args[ax] = f(s)
020|
021| # Process keyword arguments…
022| for kw in kw_names:
023| s = kwargs[kw]
024| f = ftns.pop(0)
025| kwargs[kw] = f(s)
026|
027| # Call the function with modified args…
028| retv = function(*tuple(_args), **kwargs)
029|
030| # (Optional) Process the return argument…
031| if 0 < len(ftns):
032| f = ftns.pop(0)
033| retv = f(retv)
034|
035| # Return the function’s return value…
036| return retv
037|
038| # Return the function wrapper…
039| return wrapper
040|
041| # Return the decorator…
042| return decorator
043|
This decorator has three input parameters: a list of positional argument indexes (arg_indexes), a list of keyword argument names (kw_names), and a list of functions to apply to the selected arguments (ftn_list).
There must be as many functions in ftn_list as there are indexes and names combined in the first two lists. The functions must expect and return a single parameter, which should be the same as the argument type (unless the function is converting types — see next decorator).
There can be one additional function in ftn_list. If so, it is applied to the decorated function’s return value.
The code should be fairly self-explanatory. Lines #15 to #19 process positional arguments, and lines #21 to #25 process keyword arguments. Lines #30 to #33 process the return value. Note that in all cases, we supply the argument to the handling function and pass its return value to the decorated function.
We use the decorator like this:
002|
003| caps = lambda s:s.upper()
004| nocaps = lambda s:s.lower()
005| clean = lambda s:s.strip()
006|
007| #@StringHandler([], [], [caps])
008| @StringHandler([0,1,2], [], [clean,clean,clean])
009| @StringHandler([1,2], [‘x’,‘y’], [caps,caps,caps,caps,nocaps])
010| def string_function (a, b, c, x=”, y=”, z=”):
011| ”’Example function taking a bunch of strings.”’
012| print(f’a: {a}‘)
013| print(f’b: {b}‘)
014| print(f’c: {c}‘)
015| print(f’x: {x}‘)
016| print(f’y: {y}‘)
017| print(f’z: {z}‘)
018| print()
019| return f’{a}–{b}–{c}:{x}–{y}–{z}‘
020|
021| if __name__ == ‘__main__’:
022| print()
023|
024| args1 = [‘ dog\n’, ‘\tcat\t’, ‘horse ‘]
025| props1 = dict(x=‘steak’, y=‘eggs’, z=‘bacon’)
026| t = string_function(*args1, **props1)
027| print(f’rv={t}‘)
028| print()
029|
030| args2 = [‘ apple ‘, ‘ orange\n\n’, ‘\t\tgrape\n’]
031| props2 = dict(x=‘burger’, y=‘fries’, z=‘coke’)
032| t = string_function(*args2, **props2)
033| print(f’rv={t}‘)
034| print()
035| print()
036|
Note how we’re using two occurrences of the StringHandler decorator to apply two sets of argument transformations (lines #8 and #9). In one case, we have a function that strips whitespace from strings in positional indexes 0, 1, and 2, in the other, functions that force the case of strings for two positional arguments (1 and 2) as well as two keyword arguments (“x” and “y”).
The commented-out line #7 shows how to apply a function to just the return value.
When run, this prints:
@StringHandler: string_function @StringHandler: StringHandler.<locals>.decorator.<locals>.wrapper a: dog b: CAT c: HORSE x: STEAK y: EGGS z: bacon rv=dog-cat-horse:steak-eggs-bacon a: apple b: ORANGE c: GRAPE x: BURGER y: FRIES z: coke rv=apple-orange-grape:burger-fries-coke
Note how, despite the forced uppercase of some inputs, the return value is forced to lowercase, not by the commented-out line but by the extra function passed in line #9.
Here’s a variation, a pair of decorators that allow converting str to bytes and vice-versa:
002| ”’Decorator for converting bytes to string.”’
003| print(f’Byte2String({which}, {encoding})‘)
004|
005| # We expect 1,2,3, for argument indexes, 0 for return value…
006| ix = which – 1
007|
008| def decorator (function):
009| ”’Decorator function.”’
010| ftn_name = function.__qualname__
011| print(f’@Byte2String: {ftn_name}‘)
012|
013| def wrapper (*args):
014| ”’Wrapper function.”’
015| _args = list(args)
016|
017| # Positive or zero index for arguments…
018| if 0 <= ix:
019| # Index and convert a passed argument…
020| _args[ix] = str(_args[ix], encoding=encoding)
021|
022| # Call function…
023| retv = function(*_args)
024|
025| # Negative index for return value…
026| if ix < 0:
027| # Convert return argument…
028| return str(retv, encoding=encoding)
029|
030| # Return value…
031| return retv
032|
033| # Return function wrapper…
034| return wrapper
035|
036| # Return decorator function…
037| return decorator
038|
039| def String2Byte (which=1, encoding=‘utf8’):
040| ”’Decorator for converting string to bytes.”’
041| print(f’String2Byte({which}, {encoding})‘)
042|
043| # We expect 1,2,3, for argument indexes, 0 for return value…
044| ix = which – 1
045|
046| def decorator (function):
047| ”’Decorator function.”’
048| ftn_name = function.__qualname__
049| print(f’@String2Byte: {ftn_name}‘)
050|
051| def wrapper (*args):
052| ”’Wrapper function.”’
053| _args = list(args)
054|
055| # Positive or zero index for arguments…
056| if 0 <= ix:
057| # Index and convert a passed argument…
058| _args[ix] = bytes(_args[ix], encoding=encoding)
059|
060| # Call function…
061| retv = function(*_args)
062|
063| # Negative index for return value…
064| if ix < 0:
065| # Convert return argument…
066| return bytes(retv, encoding=encoding)
067|
068| # Return value…
069| return retv
070|
071| # Return function wrapper…
072| return wrapper
073|
074| # Return decorator function…
075| return decorator
076|
As with DataRecorder above, we support modifying only a single argument. Additionally, we’ll only handle positional arguments. Most situations where this conversion is relevant involve just one parameter that very likely to be positional (if not, the code above shows how to include keyword arguments).
The two decorators are identical except for the str or bytes conversion. If you’ve ever had to deal with functions that expect bytes when the code generally deals with str (or in rare cases vice versa), this is a handy technique.
We use the decorator like this:
002|
003| ”’Return an object’s typename.”’
004| typename = lambda obj: type(obj).__name__
005|
006| @String2Byte(2) # translate second argument to bytes
007| def function1 (n:int, data:bytes, z:float) -> bytes:
008| ”’Demo function #1.”’
009| return data
010|
011| @Byte2String(3) # translate third argument to str
012| def function2 (a:str, b:int, data:str) -> str:
013| ”’Demo function #2.”’
014| return data
015|
016| @String2Byte(0) # translate return value to bytes
017| def function3 (data:str, x:int) -> str:
018| ”’Demo function #3.”’
019| return data
020|
021| @Byte2String(0, ‘utf16’) # translate return value to UTF16 string…
022| def function4 (data:bytes, m:float, n:float) -> bytes:
023| ”’Demo function #4.”’
024| return data
025|
026| if __name__ == ‘__main__’:
027| print()
028|
029| retv = function1(11, ‘Ćóñàφωċĥ’, 3.14159)
030| print(f’{retv} ({typename(retv)})‘)
031| print()
032|
033| retv = function2(‘eb’, 42, bytes(“Hello”,encoding=‘utf8’))
034| print(f’{retv} ({typename(retv)})‘)
035| print()
036|
037| retv = function3(‘질쯊췌쿎퇐폒’, 21)
038| print(f’{retv} ({typename(retv)})‘)
039| print()
040|
041| retv = function4(bytes(“World”,encoding=‘utf16’), 2.7, 1.6)
042| print(f’{retv} ({typename(retv)})‘)
043| print()
044|
When run, this prints:
String2Byte(2, utf8) @String2Byte: function1 Byte2String(3, utf8) @Byte2String: function2 String2Byte(0, utf8) @String2Byte: function3 Byte2String(0, utf16) @Byte2String: function4 b'\xc4\x86\xc3 … \x8b\xc4\xa5' (bytes) Hello (str) b'\xec\xa7\x88 … \xed\x8f\x92' (bytes) World (str)
(Long strings trimmed to fit.).
Note that, if we want to alter both an input argument and the return value, we can stack two decorators, like this:
002|
003| typename = lambda obj: type(obj).__name__
004|
005| @String2Byte()
006| @Byte2String(0)
007| def bytes_handling_function (data:bytes) -> bytes:
008| print(f’bytes_handling_function({len(data)} {typename(data)})‘)
009| print(f’data = {data.hex(“.”)}‘)
010| print()
011| return data
012|
013| if __name__ == ‘__main__’:
014| print()
015|
016| retv = bytes_handling_function(“Hello, World!”)
017| print(f’{retv = }‘)
018| print()
019|
When run, this prints:
String2Byte(1, utf8) Byte2String(0, utf8) @Byte2String: bytes_handling_function @String2Byte: Byte2String.<locals>.decorator.<locals>.wrapper bytes_handling_function(13 bytes) data = 48.65.6c.6c.6f.2c.20.57.6f.72.6c.64.21 retv = 'Hello, World!'
The function bytes_handling_function takes and returns bytes, but in our code, thanks to the decorators, we can treat it as if it takes and returns strings.
Last time we saw a decorator for timing a function. It recorded the total time spent in the decorated function as well as logging each call with a timestamp and arguments list. That decorator acted on each function individually, but suppose we wanted to time a group of functions?
Here’s a decorator that lets us declare “group” names under which timing stats are gathered:
002|
003| FunctionTimes = {‘main’:{}, ‘subs’:{}}
004|
005| def FunctionTimer (group=‘main’):
006| ”’Decorator function for timing functions.”’
007| print(f’@FunctionTimer({group})‘)
008|
009| # Add requested group if it doesn’t exist…
010| if group not in FunctionTimes:
011| FunctionTimes[group] = {}
012|
013| def decorator (function):
014| ”’The actual decorator function.”’
015| func_name = function.__qualname__
016| print(f’@FunctionTimer.decorator({func_name})‘)
017|
018| if func_name not in FunctionTimes[group]:
019| FunctionTimes[group][func_name] = [0.0]
020|
021| def wrapper (*args, **kwargs):
022| ”’The wrapper function.”’
023| print(f’FunctionTimer[{func_name}]‘)
024|
025| # Call the function and capture start and end NS…
026| t0 = perf_counter_ns()
027| retv = function(*args, **kwargs)
028| t1 = perf_counter_ns() – t0
029|
030| # Log the total time and this call time…
031| FunctionTimes[group][func_name][0] += t1
032| FunctionTimes[group][func_name].append(t1)
033| print(f’[{func_name}]: {t1/pow(10,6)} msecs‘)
034|
035| # Return timed function’s value (if any)…
036| return retv
037|
038| # Return the wrapper function…
039| return wrapper
040|
041| # Return the actual decorator function…
042| return decorator
043|
044| FunctionTimer.data = FunctionTimes
045|
This is very similar to the timing decorator from last time, so I won’t spend much time describing the code. The key difference is the addition of the FunctionTimes dictionary (line #3). This dictionary has (user-supplied) group names as keys. The values are themselves dictionaries with the decorated function names as keys. Note that in line #44, we set the data attribute on the FunctionTimer function object to reference this dictionary.
The decorator takes a group name, so we have a double-nested set of functions. The outer function (lines #5 to #42) checks FunctionTimes to see if the group name exists as a key. If not, it adds a new entry (lines #9 to #11). Then it just returns the nested decorator function (line #42).
The decorator function (lines #13 to #39) gets the decorated function’s name (line #15) and checks to see if that name is in the group. If not, it adds a new entry (lines #18 and #19). Then it returns the nested wrapper function (line #39).
The wrapper function (lines #21 to #36) times the decorated function (lines #25 to #28) and logs the data (lines # to #).
We use the decorator like this:
002| from random import random
003| from examples import FunctionTimer
004|
005| FAST = False
006| DELAY = 1/3
007| plural = lambda n: ” if n==1 else ‘s’
008|
009| @FunctionTimer()
010| def fast_function (t=0.777, d=0.456):
011| u = DELAY if FAST else t
012| v = 0 if FAST else d
013| sleep(u + (random() * v))
014|
015| @FunctionTimer(‘group1’)
016| def slow_function (t=2.71828, d=1.111):
017| u = DELAY if FAST else t
018| v = 0 if FAST else d
019| sleep(u + (random() * v))
020|
021| @FunctionTimer(‘group1’)
022| def another_slow_function (t=2.71828, d=1.111):
023| u = DELAY if FAST else t
024| v = 0 if FAST else d
025| sleep(u + (random() * v))
026|
027| @FunctionTimer(‘group2’)
028| def slower_function (t=3.14159, d=1.616):
029| u = DELAY if FAST else t
030| v = 0 if FAST else d
031| sleep(u + (random() * v))
032|
033| @FunctionTimer(‘group2’)
034| def another_slower_function (t=3.14159, d=1.616):
035| u = DELAY if FAST else t
036| v = 0 if FAST else d
037| sleep(u + (random() * v))
038|
039| @FunctionTimer(‘group3’)
040| def slowest_function (t=6.28318, d=2.718):
041| u = DELAY if FAST else t
042| v = 0 if FAST else d
043| sleep(u + (random() * v))
044|
045| if __name__ == ‘__main__’:
046| print()
047| print(‘Running some slow functions… (be patient)\n’)
048|
049| fast_function()
050| slow_function()
051| another_slow_function()
052| fast_function()
053| slower_function()
054| another_slower_function()
055| fast_function()
056|
057| print(‘\n…Hang in there…\n’)
058|
059| fast_function()
060| slow_function()
061| another_slow_function()
062| fast_function()
063| slower_function()
064| another_slower_function()
065| fast_function()
066|
067| print(‘\n…Got a bit more to go…\n’)
068|
069| fast_function()
070| slow_function()
071| another_slow_function()
072| fast_function()
073| slower_function()
074| another_slower_function()
075| fast_function()
076| slowest_function()
077| fast_function()
078| slowest_function()
079| fast_function()
080| print(‘\n…Done!\n’)
081|
082| for ch in FunctionTimer.data:
083| grp = FunctionTimer.data[ch]
084| print(f’[{ch}] ({len(grp)} function{plural(len(grp))})‘)
085|
086| for nam in sorted(grp):
087| print(f’:{nam}::‘)
088| ts = [f’{d/pow(10,9):.6f}‘ for d in grp[nam]]
089| print(f’{“, “.join(ts)}‘)
090| print()
091| print()
092|
Sorry for the long bit of code here. I wanted to have enough functions and enough calls to them to make it interesting.
There’s nothing special about the functions. They use time.sleep function to give them varying delay times. Each function takes a t (time) and a d (delta) parameter that determine the minimum time plus a random additional time. The FAST global variable, if True, allows speeding through the test for development or debugging.
When run, this prints:
@FunctionTimer(main) @FunctionTimer.decorator(fast_function) @FunctionTimer(group1) @FunctionTimer.decorator(slow_function) @FunctionTimer(group1) @FunctionTimer.decorator(another_slow_function) @FunctionTimer(group2) @FunctionTimer.decorator(slower_function) @FunctionTimer(group2) @FunctionTimer.decorator(another_slower_function) @FunctionTimer(group3) @FunctionTimer.decorator(slowest_function) Running some slow functions... (be patient) FunctionTimer[fast_function] [fast_function]: 1118.0416 msecs FunctionTimer[slow_function] [slow_function]: 2927.4383 msecs FunctionTimer[another_slow_function] [another_slow_function]: 3236.9994 msecs FunctionTimer[fast_function] [fast_function]: 1176.5464 msecs FunctionTimer[slower_function] [slower_function]: 3172.3967 msecs FunctionTimer[another_slower_function] [another_slower_function]: 3657.9977 msecs FunctionTimer[fast_function] [fast_function]: 1031.5982 msecs ...Hang in there... FunctionTimer[fast_function] [fast_function]: 789.7108 msecs FunctionTimer[slow_function] [slow_function]: 3817.0942 msecs FunctionTimer[another_slow_function] [another_slow_function]: 2811.3104 msecs FunctionTimer[fast_function] [fast_function]: 1086.5883 msecs FunctionTimer[slower_function] [slower_function]: 3194.0699 msecs FunctionTimer[another_slower_function] [another_slower_function]: 4089.8889 msecs FunctionTimer[fast_function] [fast_function]: 1027.6462 msecs ...Got a bit more to go... FunctionTimer[fast_function] [fast_function]: 1042.563 msecs FunctionTimer[slow_function] [slow_function]: 3778.7955 msecs FunctionTimer[another_slow_function] [another_slow_function]: 2739.5931 msecs FunctionTimer[fast_function] [fast_function]: 934.2696 msecs FunctionTimer[slower_function] [slower_function]: 4429.907 msecs FunctionTimer[another_slower_function] [another_slower_function]: 3640.9476 msecs FunctionTimer[fast_function] [fast_function]: 1028.4622 msecs FunctionTimer[slowest_function] [slowest_function]: 8002.107 msecs FunctionTimer[fast_function] [fast_function]: 1088.5107 msecs FunctionTimer[slowest_function] [slowest_function]: 7987.3595 msecs FunctionTimer[fast_function] [fast_function]: 959.5968 msecs ...Done! [main] (1 function) :fast_function:: 11.283534, 1.118042, 1.176546, 1.031598, 0.789711, 1.086588, 1.027646, 1.042563, 0.934270, 1.028462, 1.088511, 0.959597 [group1] (2 functions) :another_slow_function:: 8.787903, 3.236999, 2.811310, 2.739593 :slow_function:: 10.523328, 2.927438, 3.817094, 3.778796 [group2] (2 functions) :another_slower_function:: 11.388834, 3.657998, 4.089889, 3.640948 :slower_function:: 10.796374, 3.172397, 3.194070, 4.429907 [group3] (1 function) :slowest_function:: 15.989467, 8.002107, 7.987360
Which also goes on for a bit. Sorry!
Note that, in the listing at the end, the first number listed for each function is the total time (in seconds) spent in the function. The numbers that follow are the times spent on individual calls (in the order called).
The decorators so far have all been for functions. Nearly identical decorators can be created for class instance methods — just remember to include the self parameter [see the previous post for more on class instance method decorators].
Class decorators are a bit different because they wrap a class object rather than a function object. The decorator protocol is the same but wrapping a (newly created!) class offers opportunities to modify the class.
For instance, suppose you intend to create a number of different (but related) classes, and you plan to always implement the same simple __repr__ method:
002|
003| …
004|
005| def __repr__ (self):
006| tname = self.__class__.__name__
007| return f’<{tname} @{id(self):08x}>‘
008|
009| …
010|
Which, for the example above, would print as:
<ExampleClass @2783de82900>
We can use the following class decorator to automatically add such a function to any class it decorates:
002| ”’Decorator function to add repr() function to a class.”’
003| print(f’@AddSimpleReprMethod({cls.__name__})‘)
004|
005| def my_repr (self):
006| return f”<{type(self).__name__} @{id(self):08x}>“
007|
008| cls.__repr__ = my_repr
009| return cls
010|
Note that, unlike all other decorators so far, this decorator does not return a wrapper around the function or, in this case, class — it returns the class itself. But first it modifies the class by installing the __repr__ method (line #8).
Obviously, a decorator like this can make many different modifications to classes it decorates. This represents a horizonal form of class hierarchy for cases where different classes can’t share a parent that implements common methods.
We use the decorator like this:
002|
003| @AddSimpleReprMethod
004| class EgClass1:
005| …
006|
007| @AddSimpleReprMethod
008| class EgClass2:
009| …
010|
011| @AddSimpleReprMethod
012| class EgClass3:
013| …
014|
015| @AddSimpleReprMethod
016| class EgClass4:
017| …
018|
019| @AddSimpleReprMethod
020| class EgClass5:
021| …
022|
023| if __name__ == ‘__main__’:
024| print()
025|
026| c1 = EgClass1()
027| c2 = EgClass2()
028| c3 = EgClass3()
029| c4 = EgClass4()
030| c5 = EgClass5()
031| print(f’c1: {c1}‘)
032| print(f’c2: {c2}‘)
033| print(f’c3: {c3}‘)
034| print(f’c4: {c4}‘)
035| print(f’c5: {c5}‘)
036| print()
037|
The example assumes that, for whatever reason, these five classes can’t share a common ancestor but should have a common method (in this case __repr__).
When run, this prints:
@AddSimpleReprMethod(EgClass1) @AddSimpleReprMethod(EgClass2) @AddSimpleReprMethod(EgClass3) @AddSimpleReprMethod(EgClass4) @AddSimpleReprMethod(EgClass5) c1: <EgClass1 @24f6b5c2a50> c2: <EgClass2 @24f6b5c2ba0> c3: <EgClass3 @24f6b5c2cf0> c4: <EgClass4 @24f6b5c2e40> c5: <EgClass5 @24f6b5c2f90>
Demonstrating that the defined __repr__ function does exist in each class.
Here’s another version:
002| ”’Decorator function to modify repr() function of a class.”’
003| print(f’Change repr for {cls.__name__}‘)
004|
005| def my_repr (self):
006| attrs = (f”{key}={val!r}“ for key,val in vars(self).items())
007| return f”{type(self).__name__}({‘, ‘.join(attrs)})“
008|
009| cls.__repr__ = my_repr
010| return cls
011|
This one creates a __repr__ method that lists all the instance’s attributes.
Use it like this:
002|
003| @AddReprMethod
004| class foobar:
005| ”’Example class #1.”’
006| def __init__ (self, name):
007| self.name = name
008| self.count = 0
009| self.timer = 0.0
010|
011| @AddReprMethod
012| class tasbot:
013| ”’Example class #2.”’
014| def __init__ (self, x:float=0.0, y:float=0.0):
015| self.x = x
016| self.y = y
017|
018| if __name__ == ‘__main__’:
019| print()
020|
021| foo1 = foobar(‘Fred’)
022| foo2 = foobar(‘Barney’)
023| print(f’{foo1!r}\n{foo2!r}\n‘)
024|
025| tas1 = tasbot()
026| tas2 = tasbot(4.2, 2.1)
027| print(f’{tas1!r}\n{tas2!r}\n‘)
028|
When run, this prints:
Change repr for foobar Change repr for tasbot foobar(name='Fred', count=0, timer=0.0) foobar(name='Barney', count=0, timer=0.0) tasbot(x=0.0, y=0.0) tasbot(x=4.2, y=2.1)
Note that this form of output can be used to recreate the instances:
002|
003| @AddReprMethod
004| class faxbun:
005| ”’Example class #3.”’
006| def __init__ (self, name:str, x:float=0.0, y:float=0.0):
007| self.name = name
008| self.x = x
009| self.y = y
010|
011| if __name__ == ‘__main__’:
012| print()
013|
014| obj1 = faxbun(‘Sam’)
015| obj2 = faxbun(‘Judy’, 4.2, 2.1)
016|
017| txt1 = repr(obj1)
018| txt2 = repr(obj2)
019| print(txt1, txt2, ”, sep=‘\n’)
020|
021| dup1 = eval(txt1)
022| dup2 = eval(txt2)
023| print(f’{dup1} [type={type(dup1).__name__}]‘)
024| print(f’{dup2} [type={type(dup2).__name__}]‘)
025| print()
026|
The built-in eval function treats the text as declaring an object instance.
When run, this prints:
Change repr for faxbun faxbun(name='Sam', x=0.0, y=0.0) faxbun(name='Judy', x=4.2, y=2.1) faxbun(name='Sam', x=0.0, y=0.0) [type=faxbun] faxbun(name='Judy', x=4.2, y=2.1) [type=faxbun]
A common use for such a __repr__ method is to save object instances and their state to a text file and then restoring them from that file later. In such cases, you definitely want any object you intend to save to have this __repr__ method.
Last time, I mentioned the built-in method decorators staticmethod and classmethod. Both modify the method signature. The former to have no reference to the enclosing class (nor an instance of that class), the latter to receive a reference to the class but not to any instance.
For this one, I think it makes the most sense to start with a use case:
002| from examples import StaticMethod, ClassMethod
003|
004| class point:
005| ”’Example class.”’
006| dent = 42
007|
008| def __init__ (self, x=0.0, y=0.0):
009| ”’New point instance.”’
010| self.x = float(x)
011| self.y = float(y)
012|
013| @StaticMethod
014| def distance (a, b) -> float:
015| ”’Example static method.”’
016| print(f’distance({a}, {b})‘)
017| return sqrt(pow(a.x–b.x,2) + pow(a.y–b.y,2))
018|
019| @ClassMethod
020| def setdent (cls, value):
021| ”’Example class method.”’
022| print(f’setdent({cls.__name__}, {value})‘)
023| point.dent = value
024| return point.dent
025|
026| def __repr__ (self):
027| return f’<{type(self).__name__} [{self.dent}] @{id(self):08x}>‘
028|
029| def __str__ (self):
030| return f’[{self.x:.3f} x {self.y:.3f}]‘
031|
032| if __name__ == ‘__main__’:
033| print()
034| p0 = point()
035| p1 = point(–5, +5)
036|
037| print(f’{point.distance(p0, p1)}‘)
038| print(f’{p0.distance(p0, p1)}‘)
039| print()
040|
041| print(f’{point.setdent(27)}‘)
042| print(f’{p0.setdent(31)}‘)
043| print()
044|
Above is a simple 2D point class with a static method (lines #13 to #17) and a class method (lines #19 to #24). Note that we’re using StaticMethod and ClassMethod decorators rather than Python’s built-in staticmethod and classmethod. The former two are defined below.
Static methods take an ordinary set of parameters — there is no self or cls parameter. Static methods can know about the class because they’re defined inside the class (and its namespace), but they otherwise function as ordinary functions. Class methods take a cls parameter, which is a reference to the class rather than a specific instance, as with self.
When run, this prints:
@StaticMethod: distance @ClassMethod: setdent distance([0.000 x 0.000], [-5.000 x 5.000]) 7.0710678118654755 distance([0.000 x 0.000], [-5.000 x 5.000]) 7.0710678118654755 setdent(point, 27) 27 setdent(point, 31) 31
We need something a bit different in method decorators that change the signature of the wrapped function. The usual technique works fine on instances, but not on classes. To implement static or class methods, we need to use a Python descriptor:
002| ”’Decorator to turn a method into a static method.”’
003|
004| # Gather some basic info…
005| ftn_name = method.__name__
006| print(f’@StaticMethod: {ftn_name}‘)
007|
008| class StaticMethodProperty:
009| ”’Property class to implement the static method wrapper.”’
010|
011| def __init__ (self, ftn):
012| print(f’{ObjIdStr(self)} init‘)
013| self.method = ftn
014|
015| def __set_name__ (self, owner, name):
016| # Remember the function’s name…
017| self.name = name
018|
019| def __get__ (self, obj, cls):
020| ”’Access the property.”’
021| caller = ‘<null>’ if obj is None else ObjIdStr(obj)
022| print(f’{ObjIdStr(self)} get: {caller}‘)
023| def proxy (*args, **kwargs):
024| ”’Proxy function that wraps class method.”’
025| return self.method(*args, **kwargs)
026| return proxy
027|
028| def __set__ (self, obj, value):
029| ”'(Only protects calls on instance objects.”’
030| raise NotImplementedError(f’You may not modify {self.name}!‘)
031|
032| def __delete__ (self, obj):
033| ”'(Only protects calls on instance objects.”’
034| raise NotImplementedError(f’You may not delete {self.name}!‘)
035|
036| # Return new instance of StaticMethodProperty…
037| return StaticMethodProperty(method)
038|
039| ObjIdStr = lambda obj: f’<{type(obj).__name__} @{id(obj):08x}>‘
040|
041| def ClassMethod (method):
042| ”’Decorator to turn a method into a class method.”’
043|
044| ftn_name = method.__name__
045| print(f’@ClassMethod: {ftn_name}‘)
046|
047| class ClassMethodProperty:
048| ”’Property class to implement the class method wrapper.”’
049|
050| def __init__ (self, ftn):
051| #print(f'{ObjIdStr(self)} init’)
052| self.method = ftn
053|
054| def __set_name__ (self, owner, name):
055| # Remember the function’s name…
056| self.name = name
057|
058| def __get__ (self, obj, cls):
059| ”’Access the property.”’
060| caller = ‘<null>’ if obj is None else ObjIdStr(obj)
061| print(f’{ObjIdStr(self)} get: {caller}‘)
062| def proxy (*args, **kwargs):
063| ”’Proxy function that wraps class method.”’
064| return self.method(cls, *args, **kwargs)
065| return proxy
066|
067| def __set__ (self, obj, value):
068| ”'(Only protects calls on instance objects.”’
069| raise NotImplementedError(f’You may not modify {self.name}!‘)
070|
071| def __delete__ (self, obj):
072| ”'(Only protects calls on instance objects.”’
073| raise NotImplementedError(f’You may not delete {self.name}!‘)
074|
075| # Return new instance of ClassMethodProperty…
076| return ClassMethodProperty(method)
077|
See Python Descriptors, part 1 and Python Descriptors, part 2 for details on how descriptors work. The short version is that they provide much more control over how the decorated method gets accessed.
When client code uses a static or class method, it invokes the descriptor’s __get__ method, which returns a wrapper function, proxy. The client code, in calling the method, calls the proxy, which invokes the actual method.
Something to be aware of in Python is that it’s hard to entirely protect classes from client code. Python has no sense of private or protected data, as some languages do. Client code can overwrite or modify these decorated methods.
Here’s an example:
002|
003| class TestClass:
004| ”’Example class #1.”’
005|
006| @StaticMethod
007| def barfoo (a, b, c):
008| return a*b*c
009|
010| @ClassMethod
011| def foobar (cls, x, y):
012| return x+y
013|
014| def __repr__ (self):
015| return ObjIdStr(self)
016|
017| if __name__ == ‘__main__’:
018| print()
019|
020| # Access through the class…
021| print(f’{TestClass.barfoo(21,42,63)=}‘)
022| print(f’{TestClass.foobar(21,42)=}‘)
023| print()
024|
025| # Access through an instance…
026| g = TestClass()
027| print(f’{g=}‘)
028| print(f’{g.barfoo(63,84,108)=}‘)
029| print(f’{g.foobar(63,84)=}‘)
030| print()
031|
032| # Try setting or deleting on the instance…
033| try: g.barfoo = [1,2,3]
034| except Exception as e: print(e)
035|
036| try: del g.barfoo
037| except Exception as e: print(e)
038|
039| # Try setting or deleting on the instance…
040| try: g.foobar = [1,2,3]
041| except Exception as e: print(e)
042|
043| try: del g.foobar
044| except Exception as e: print(e)
045| print()
046|
047| # Try setting or deleting on the class…
048| try:
049| tmp = TestClass.barfoo
050| TestClass.barfoo = [1,2,3]
051| print(f’{TestClass.barfoo=}‘)
052| except Exception as e: print(e)
053| finally: TestClass.barfoo = tmp
054|
055| try:
056| del TestClass.barfoo
057| print(f’{hasattr(TestClass,“barfoo”)=}‘)
058| except Exception as e: print(e)
059|
060| # Try setting or deleting on the class…
061| try:
062| tmp = TestClass.foobar
063| TestClass.foobar = [1,2,3]
064| print(f’{TestClass.foobar=}‘)
065| except Exception as e: print(e)
066| finally: TestClass.foobar = tmp
067|
068| try:
069| del TestClass.foobar
070| print(f’{hasattr(TestClass,“foobar”)=}‘)
071| except Exception as e: print(e)
072| print()
073|
When run, this prints:
@StaticMethod: barfoo <StaticMethodProperty @293c066e900> init @ClassMethod: foobar <StaticMethodProperty @293c066e900> get: <null> TestClass.barfoo(21,42,63)=55566 <ClassMethodProperty @293c066eba0> get: <null> TestClass.foobar(21,42)=63 g=<TestClass @293c066ecf0> <StaticMethodProperty @293c066e900> get: <TestClass @293c066ecf0> g.barfoo(63,84,108)=571536 <ClassMethodProperty @293c066eba0> get: <TestClass @293c066ecf0> g.foobar(63,84)=147 You may not modify barfoo! You may not delete barfoo! You may not modify foobar! You may not delete foobar! <StaticMethodProperty @293c066e900> get: <null> TestClass.barfoo=[1, 2, 3] hasattr(TestClass,"barfoo")=False <ClassMethodProperty @293c066eba0> get: <null> TestClass.foobar=[1, 2, 3] hasattr(TestClass,"foobar")=False
Using descriptors to implement the property decorator is left as a reader exercise.
Lastly, all the decorators in these two posts are functions, which raises the question of whether a decorator can be a class. It can, though as we’ll see, using a decorator class requires a slight change in usage.
Here’s a basic decorator class (for decorating functions):
002| ”’Basic Decorator *class*. Use instances for decorators.”’
003|
004| class DecoratorFunctionClass:
005| ”’Decorator class for wrapping a function.”’
006|
007| def __init__ (self, function):
008| ”’New wrapper instance; receives decorated function.”’
009| print(f’@DecoratorFunction({function})‘)
010| self.function = function
011|
012| def __call__ (self, *args, **kwargs):
013| ”’Call to the decorated function.”’
014| print(f’Decorator({len(args)} args, {len(kwargs)} kwargs)‘)
015|
016| # Call the wrapped function; return its return value…
017| return self.function(*args, **kwargs)
018|
019| def __init__ (self):
020| ”’New decorator instance.”’
021| print(f’@Decorator()‘)
022|
023| def __call__ (self, function):
024| ”’Return a new wrapper for a function.”’
025| return BasicDecoratorClass.DecoratorFunctionClass(function)
026|
Note that we’re also using an inner class (lines #4 to #17) to create wrapper instances.
Using this decorator class requires making instances of it:
002|
003| @BasicDecoratorClass()
004| def function0 ():
005| return (21, 42, 63)
006|
007| @BasicDecoratorClass()
008| def function1 (x:int, y:int, z:float, a:str, b:str):
009| return (((x*y)/z), a+b)
010|
011| if __name__ == ‘__main__’:
012| print()
013|
014| retv = function0()
015| print(f’{retv=}‘)
016| print()
017|
018| retv = function1(21,42, 3.14159, ‘Hello, ‘, ‘World!’)
019| print(f’{retv=}‘)
020| print()
021|
022| print()
023|
Note that line #3 and line #7 create instances of the decorator. They are not calls to a decorator function to set parameters, as we’ve used in some previous decorator functions.
When run, this prints:
@Decorator() @DecoratorFunction(<function function0 at 0x000001F02EB2E160>) @Decorator() @DecoratorFunction(<function function1 at 0x000001F02EB2DA80>) Decorator(0 args, 0 kwargs) retv=(21, 42, 63) Decorator(5 args, 0 kwargs) retv=(280.7495567531091, 'Hello, World!')
This has run longer than usual (especially considering the first one did, too), but it’s a meaty topic with a lot of potential. I hope these examples have opened doors to new ways to use Python.
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, more
Regarding the last example, if you wanted a class-based decorator that took initialization arguments, here’s how you might write it:
002| DictKeys = lambda d: ",".join(d.keys())
003|
004| class BasicDecoratorClass:
005| ”’Basic Decorator *class*. Use instances for decorators.”’
006|
007| class DecoratorFunctionClass:
008| ”’Decorator class for wrapping a function.”’
009|
010| def __init__ (self, function, **props):
011| ”’New wrapper instance; receives decorated function.”’
012| print(f’@Decorator({function.__name__}, {DictKeys(props)})‘)
013| self.function = function
014| self.name = function.__name__
015| self.props = props
016|
017| def __call__ (self, *args, **kwargs):
018| ”’Call to the decorated function.”’
019| print(f’{self.name}(({ArgVals(args)}), {{{DictKeys(kwargs)}}})‘)
020|
021| # Call the wrapped function; return its return value…
022| return self.function(*args, **kwargs)
023|
024| def __init__ (self, **props):
025| ”’New decorator instance.”’
026| print(f’@Decorator({DictKeys(props)})‘)
027| self.props = props
028|
029| def __call__ (self, function):
030| ”’Return a new wrapper for a function.”’
031| return BasicDecoratorClass.DecoratorFunctionClass(function, **self.props)
032|
033|
034| @BasicDecoratorClass(a=21, b=42, c=63)
035| def function0 ():
036| return (21, 42, 63)
037|
038| @BasicDecoratorClass(name="Dude")
039| def function1 (x:int, y:int, z:float, a:str, b:str):
040| return (((x*y)/z), a+b)
041|
042| if __name__ == ‘__main__’:
043| print()
044|
045| retv = function0()
046| print(f’{retv=}‘)
047| print()
048|
049| retv = function1(21,42, 3.14159, ‘Hello, ‘, ‘World!’)
050| print(f’{retv=}‘)
051| print()
052|
053| print()
054|
When run, this prints:
@Decorator(a,b,c) @Decorator(function0, a,b,c) @Decorator(name) @Decorator(function1, name) function0((), {}) retv=(21, 42, 63) function1((21,42,3.14159,Hello, ,World!), {}) retv=(280.7495567531091, 'Hello, World!')