SoFunction
Updated on 2024-12-10

An analysis of Python writing function decorators

Writing function decorators

This section focuses on writing function decorators.

trace a call

The following code defines and applies a function decorator that counts the number of calls to the decorated function and prints trace information for each call.

class tracer:
def __init__(self,func):
 = 0
 = func
def __call__(self,*args):
 += 1
print('call %s to %s' %(, .__name__))
(*args)
@tracer
def spam(a, b, c):
print(a + b + c)

This is a decorator written through the syntax of class decoration and tested as follows:

>>> spam(1,2,3)
call 1 to spam
6
>>> spam('a','b','c')
call 2 to spam
abc
>>> 
2
>>> spam
<__main__.tracer object at 0x03098410>

At runtime, the tracer class is kept separate from the decorated function, and subsequent calls to the decorated function are intercepted in order to add a logical layer to count and print each call.

After decoration, spam is actually an instance of the tracer class.

The @decorator syntax avoids accidentally calling the initial function directly. Consider the non-decorator equivalent code shown below:

calls = 0
def tracer(func,*args):
global calls
calls += 1
print('call %s to %s'%(calls,func.__name__))
func(*args)
def spam(a,b,c):
print(a+b+c)

The test is as follows:

?
1
2
3
4
5
>>> spam(1,2,3)
6
>>> tracer(spam,1,2,3)
call 1 to spam
6

This alternative can be used on any function and requires no special @ syntax, but unlike the decorator version, it requires additional syntax at each place in the code where the function is called. Although decorators are not required, they are usually the most convenient.

Extension - support for keyword parameters

The following code is an extended version of the previous example, adding support for keyword arguments:

class tracer:
def __init__(self,func):
 = 0
 = func
def __call__(self,*args,**kargs):
 += 1
print('call %s to %s' %(, .__name__))
(*args,**kargs)
@tracer
def spam(a, b, c):
print(a + b + c)
@tracer
def egg(x,y):
print(x**y)

The test is as follows:

>>> spam(1,2,3)
call 1 to spam
6
>>> spam(a=4,b=5,c=6)
call 2 to spam
15
>>> egg(2,16)
call 1 to egg
65536
>>> egg(4,y=4)
call 2 to egg
256

It can also be seen that the code here also uses the [Class Instance Attribute] to save the state, i.e. the number of calls. Both the wrapped function and the call counter are specific to each instance of the information.

Writing decorators using def function syntax

The same effect can be achieved by defining a decorator function using def. But there is a catch, we also need a counter in the enclosing scope which changes with each call. We can naturally think of global variables as follows:

calls = 0
def tracer(func):
def wrapper(*args,**kargs):
global calls
calls += 1
print('call %s to %s'%(calls,func.__name__))
return func(*args,**kargs)
return wrapper
@tracer
def spam(a,b,c):
print(a+b+c)
@tracer
def egg(x,y):
print(x**y)

Here calls is defined as a global variable, which is cross-program and belongs to the whole module, not to each function, in which case the counter is incremented for any traced function call, as tested below:

>>> spam(1,2,3)
call 1 to spam
6
>>> spam(a=4,b=5,c=6)
call 2 to spam
15
>>> egg(2,16)
call 3 to egg
65536
>>> egg(4,y=4)
call 4 to egg
256

You can see that the program uses the same counter for both the spam function and the egg function.

So how to implement a counter for each function, we can use the new nonlocal statement in Python 3 as follows:

def tracer(func):
calls = 0
def wrapper(*args,**kargs):
nonlocal calls
calls += 1
print('call %s to %s'%(calls,func.__name__))
return func(*args,**kargs)
return wrapper
@tracer
def spam(a,b,c):
print(a+b+c)
@tracer
def egg(x,y):
print(x**y)
spam(1,2,3)
spam(a=4,b=5,c=6)
egg(2,16)
egg(4,y=4)

Run the following:

call 1 to spam
6
call 2 to spam
15
call 1 to egg
65536
call 2 to egg
256

In this way, the calls variable is defined inside the tracer function so that it exists in a closed function scope, and then the scope is modified by a nonlocal statement that modifies the calls variable. That way, we can achieve the functionality we need.

Trap: Decorative Class Methods

Note that decorators written using classes cannot be used to decorate functions of a class with a self parameter, as described in Python Decorator Basics].
That is, if the decorator is written as follows using classes:

class tracer:
def __init__(self,func):
 = 0
 = func
def __call__(self,*args,**kargs):
 += 1
print('call %s to %s'%(,.__name__))
return (*args,**kargs)

when it decorates the following methods in the class:

class Person:
def __init__(self,name,pay):
 = name
 = pay
@tracer
def giveRaise(self,percent):
 *= (1.0 + percent)

At this point the program will definitely go wrong. The root of the problem is that the __call__ method of the tracer class has a self - which is a tracer instance - and when we rebind the decorative method name to a class instance object with __call__, Python only passes the tracer instance to the self, it doesn't even did not pass the Person body in the argument list. In addition, since the tracer doesn't know anything about the Person instance we're dealing with with the method call, there's no way to create a method with a binding to an instance, and so there's no way to assign the call correctly.

At this point we can only write decorators by nesting functions.

time call

The following decorator will time calls to a decorated function - both for a single call and in total for all calls.

import time
class timer:
def __init__(self,func):
 = func
 = 0
def __call__(self,*args,**kargs):
start = ()
result = (*args,**kargs)
elapsed = ()- start
 += elapsed
print('%s:%.5f,%.5f'%(.__name__,elapsed,))
return result
@timer
def listcomp(N):
return [x*2 for x in range(N)]
@timer
def mapcall(N):
return list(map((lambda x :x*2),range(N)))
result = listcomp(5)
listcomp(50000)
listcomp(500000)
listcomp(1000000)
print(result)
print('allTime = %s'%)
print('')
result = mapcall(5)
mapcall(50000)
mapcall(500000)
mapcall(1000000)
print(result)
print('allTime = %s'%)
print('map/comp = %s '% round(/,3))

The results of the run are as follows:

listcomp:0.00001,0.00001
listcomp:0.00885,0.00886
listcomp:0.05935,0.06821
listcomp:0.11445,0.18266
[0, 2, 4, 6, 8]
allTime = 0.18266365607537918
mapcall:0.00002,0.00002
mapcall:0.00689,0.00690
mapcall:0.08348,0.09038
mapcall:0.16906,0.25944
[0, 2, 4, 6, 8]
allTime = 0.2594409060462425
map/comp = 1.42

The important thing to note here is that the map operation returns an iterator in Python 3, so its map operation can't correspond directly to a list parsing job, i.e. it doesn't actually take time. So use list(map()) to force it to build a list like list parsing does

Adding Decorator Parameters

Sometimes we need a decorator to do an extra job, such as providing an output label and the ability to turn tracking messages on or off. This requires the use of decorator parameters, which we can use to make configuration options that can be coded for each decorated function. For example, add labels like the following:

def timer(label = ''):
def decorator(func):
def onCall(*args):
...
print(label,...)
return onCall
return decorator
@timer('==>')
def listcomp(N):...

We can use such a result in a timer to allow a label and a tracking control flag to be passed in when decorating. For example, the following code:

import time
def timer(label= '', trace=True):
class Timer:
def __init__(self,func):
 = func
 = 0
def __call__(self,*args,**kargs):
start = ()
result = (*args,**kargs)
elapsed = () - start
 += elapsed
if trace:
ft = '%s %s:%.5f,%.5f'
values = (label,.__name__,elapsed,)
print(format % value)
return result
return Timer

This timed function decorator can be used for any function, both in the module and in interactive mode. We can test it in interactive mode as follows:

>>> @timer(trace = False)
def listcomp(N):
return [x * 2 for x in range(N)]
>>> x = listcomp(5000)
>>> x = listcomp(5000)
>>> x = listcomp(5000)
>>> listcomp
<__main__.timer.<locals>.Timer object at 0x036DCC10>
>>> 
0.0011475424533080223
>>>
>>> @timer(trace=True,label='\t=>')
def listcomp(N):
return [x * 2 for x in range(N)]
>>> x = listcomp(5000)
=> listcomp:0.00036,0.00036
>>> x = listcomp(5000)
=> listcomp:0.00034,0.00070
>>> x = listcomp(5000)
=> listcomp:0.00034,0.00104
>>> 
0.0010432902706075842</locals>

About Python write function decorator related knowledge I will introduce to you here, I hope to help you!