SoFunction
Updated on 2025-03-02

Detailed explanation of the implementation method of Python function decorator

This article describes the implementation method of Python function decorator. Share it for your reference, as follows:

Writing a function decorator

Here we mainly introduce the relevant content of writing function decorators.

Tracking calls

The following code defines and applies a function decorator to count the number of calls to the decorated function and prints the 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, 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>

When running, the tracer class and the decorative function are saved separately and subsequent calls to the decorative 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.

@Decorator syntax avoids direct accidental calls to the original function. Consider the peer code for non-decorators as 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 tests are as follows:

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

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

Extension—Support keyword parameters

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

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 tests are 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

You can also see that the code here also uses [Class Instance Properties] to save the state, that is, the number of calls. The wrapped function and call counter are information for each instance.

Write decorator using def function syntax

The same effect can be achieved using def to define the decorator function. But there is a problem, we also need to enclose a counter in the scope, which changes with each call. We can think of global variables naturally, 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 are defined as global variables, which are cross-programs, belong to the entire module, not for each function. In this way, for any tracked function calls, the counter will be incremented, as shown 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 for spam function and egg function, the program uses the same counter.

So how to implement a counter for each function? We can use the newly added nonlocal statement in Python3, 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 as follows:

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 through the nonlocal statement and the calls variable. This allows us to realize the functions we need.

Trap: Decorative method

[Note that decorators written using classes cannot be used to decorate functions with self parameters in a certain class. This is introduced in the Python decorator basics]

That is, if the decorator is written using the following class:

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 is decorated with the method in the class as follows:

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

The program will definitely go wrong at this time. The root of the problem lies in the tracer class__call__Methodself——It is a tracer instance, when we use__call__When rebinding the decorative method name to a class instance object, Python only passes the tracer instance to self, and it does not pass the Person body in the parameter list at all. In addition, since tracer does not know any information about the Person instance we want to use method calls to process, there is no way to create a binding method with an instance, so there is no way to correctly allocate the call.

At this time, we can only write decorators by nesting functions.

Timing call

The following decorator will time the call of a decorative function—both for one call and the total time 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 operation results 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

It should be noted here that the map operation returns an iterator in Python 3, so its map operation cannot directly correspond to the work of list parsing, that is, it does not take time in fact. So uselist(map())To force it to build a list like list parsing

Add decorator parameters

Sometimes we need a decorator to do an extra work, such as providing an output tag and being able to turn the tracking message on or off. This requires the use of decorator parameters. We can use decorator parameters to formulate configuration options, which can be encoded according to the function of each decorative. For example, add a tag like this:

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

We can use such results in a timer to allow a tag and a tracking control flag to be passed during decoration. 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 timing function decorator can be used for any function, both in module and in interactive mode. We can test 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

For more information about Python, please view the special topic of this site: "Python data structure and algorithm tutorial》、《Summary of Python Socket Programming Tips》、《Summary of Python function usage tips》、《Summary of Python string operation skills"and"Python introduction and advanced classic tutorials

I hope this article will be helpful to everyone's Python programming.