Create a Python general-purpose decorator framework

Let's create a Python "decorator". Create a generic decorator framework.

  1. Decorator with no arguments
  2. Decorator passing arguments
  3. Decorator of a framework that integrates these

To make. First, present the decorator framework (goal) and work towards it.

Decorator framework

This article is written to be read down (toward the goal by repeating trial and error), and since it is a long sentence, I will describe only how to write the decorator (framework, framework) first. Include the test [Full version listed last](# decorator_frameworkpy-full-version). If you are busy, please take a look at it when you have time.

decorator_framework.py


from functools import wraps

def my_decorator( *args, **kwargs ):
    #For decorators that clearly take no arguments, start here
    # _my_You can rename the decorator and define it globally
    def _my_decorator( func ):
        # _my_decorator_body()If there is any necessary processing before defining, write it here
        print( "_my_decorator_body()If there is any necessary processing before defining, write it here" )
        @wraps(func)
        def _my_decorator_body( *body_args, **body_kwargs ):
            #Pre-processing is done here
            print( "Pre-processing is done here", args, kwargs, body_args, body_kwargs )
            try:
                #Execution of the decorated body
                ret = func( *body_args, **body_kwargs )
            except:
                raise
            #Post-processing is done here
            print( "Post-processing is done here", args, kwargs, body_args, body_kwargs )
            return ret

        #Write here if processing is required when the decorator is listed#2
        print( "Write here if processing is required when the decorator is listed#2" )
        return _my_decorator_body

    #This is the end of the decorator that clearly takes no arguments

    #Write here if processing is required when the decorator is listed#1
    print( "Write here if processing is required when the decorator is listed#1" )

    if len(args) == 1 and callable( args[0] ):
        #If the decorator is called with no arguments, handle it here
        print( "No arguments" )
        return _my_decorator( args[0] )

    else:
        #If the decorator is called with arguments, handle it here
        print( "There are some arguments:", args )
        return _my_decorator

Even simpler.

decorator_framework.py Simple version


from functools import wraps

def my_decorator( *args, **kwargs ):
    def _my_decorator( func ):
        # _my_decorator_body()If there is any necessary processing before defining, write it here
        @wraps(func)
        def _my_decorator_body( *body_args, **body_kwargs ):
            #Pre-processing is done here
            try:
                #Execution of the decorated body
                ret = func( *body_args, **body_kwargs )
            except:
                raise
            #Post-processing is done here
            return ret

        #Write here if processing is required when the decorator is listed#2
        return _my_decorator_body

    #Write here if processing is required when the decorator is listed#1

    if len(args) == 1 and callable( args[0] ):
        #If the decorator is called with no arguments, handle it here
        return _my_decorator( args[0] )

    else:
        #If the decorator is called with arguments, handle it here
        return _my_decorator

Now let's get down to creating a decorator.

What is a decorator?

The glossary describes the decorator as follows:

decorator

(decorator) A function that returns another function, usually @wrapper syntax. A common use case for decorators is < span class = "pre"> classmethod () and staticmethod () .

The decorator grammar is syntactic sugar. The following two function definitions are semantically identical:

def f(...):
    ...
f = staticmethod(f)

@staticmethod def f(...): ...

The same concept exists in classes, but it's rarely used. For more information on decorators, see function definition and class definition .

(Quoted from decorator-Glossary — Python 3.8.1 Documentation)

In the example quoted above, @ staticmethod is the decorator.

In addition, the description of the function definition item is as follows.

Function definition is one or more decorator expression. When you define a function, the decorator expression is evaluated in the scope that contains the function definition. The result must be a callable object that takes a function object as its only argument. Instead of a function object, the returned value is bound to the function name. Multiple decorators are nested and applied. For example, the following code:

@f1(arg)
@f2
def func(): pass

is roughly equivalent to

def func(): pass
func = f1(arg)(f2(func))

However, in the former code, you can temporarily bind the original function to the name func . Except where there is no.

(Quoted from function definition --8 compound statement — Python 3.8.1 documentation)

Decorator grammar is syntactic sugar

Some people may not be familiar with the word "syntactic sugar". Simply put, syntactic sugar is what allows you to replace complex (obfuscated) descriptions with simple notation and vice versa.

In other words, decorators are rewritable replacements, which is similar to what other languages call "macro". (However, the replacement method has been decided)

Consider the example in the glossary citation.

Example 1


def f(...):
    ...
f = staticmethod(f)  

With this example 1

Example 2


@staticmethod
def f(...):
    ...

It says that this example 2 is the same. What do you mean.

What Example 1 is doing is

  1. Define a function called f ().
  2. Execute staticmethod () with the defined function f () (function object) as an argument.
  3. Change the return value from staticmethod () to the function f again. It means that.

Example 2 is a simplified version of this, where @staticmethod is called a" decorator ".

After that, when the function f () is actually called, the redefined function is called as the return value of staticmethod ().

Decorator structure

Since @staticmethod is actually a standard-defined decorator, it's not a good sample, so we'll move on with @my_decorator below.

There are a few things you can tell from the above example alone.

  1. The decorator my_decorator is a function. (To be exact, a "callable object")
  2. The decorator (function) my_decorator () takes a function object as an argument.
  3. The decorator (function) my_decorator () returns a function.

about it.

First step

The code below shows what we found above.

my_decorator&nbsp;First step


def my_decorator(func):
    return func

It does nothing and returns the function received as an argument. Calls that don't use @ look like sample 001.

sample&nbsp;001


>>> def my_decorator_001(func):
...     return func
...
>>> def base_func():
...     print( "in base_func()" )
...
>>> base_func = my_decorator_001( base_func )
>>> base_func()
in base_func()
>>>

If you use @ as a decorator, it will look like sample 002.

sample&nbsp;002


>>> def my_decorator_001(func):
...     return func
...
>>> @my_decorator_001
... def base_func():
...     print( "in base_func()" )
...
>>> base_func()
in base_func()
>>>

You don't know what's going on with this. However, it's worth noting that when you enter @ my_decorator_001, it's waiting for continuous input.

Let's add a display inside my_decorator ().

sample&nbsp;003


>>> def my_decorator_002(func):
...     print( "in my_decorator_002()" )
...     return func
...
>>> def base_func():
...     print( "in base_func()" )
...
>>> base_func = my_decorator_002(base_func)
in my_decorator_002()
>>> base_func()
in base_func()
>>>

Next, let's use a decorator.

sample&nbsp;004


>>> def my_decorator_002(func):
...     print( "in my_decorator_002()" )
...     return func
...
>>> @my_decorator_002
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_002()
>>> base_func()
in base_func()
>>>

The function my_decorator_002 () is not executed when you write @ my_decorator_002, and ʻin my_decorator ()is displayed when you have finished definingdef base_func ():under it. The point to note is that this is the same thing that happens when you runbase_func = my_decorator_002 (base_func)` in sample 003.

What is the return function?

Previously, the function given to the decorator argument was returned as-is with return. What is the return function?

Let's first try using a global function to see what happens if we return another function.

sample&nbsp;005


>>> def global_func():
...     print( "in global_func()" )
...
>>> def my_decorator_005(func):
...     print( "in my_decorator_005()" )
...     return global_func
...
>>> @my_decorator_005
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_005()
>>> base_func()
in global_func()
>>>

By executing base_func (), global_func () was called without any problem. But, of course, the original base_func () is not running.

Add processing and execute the original function

Consider executing the original function while executing the new function.

The original function is passed as an argument to the decorator function, so calling it will execute the original function.

sample&nbsp;006


>>> def global_func():
...     print( "in global_func()" )
...
>>> def my_decorator_006(func):
...     print( "in my_decorator_006()" )
...     func()  #Call to the original function
...     return global_func
...
>>> @my_decorator_006
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_006()
in base_func()      #I have called the original function here
>>> base_func()     #* I want to call the original function here
in global_func()
>>>

The original function base_func () was called when @ my_decorator_006 was specified. The timing when you want to call base_func () is when you call base_func () with "*". In other words, we want to call base_func () inside global_func ().

Let's modify it a bit so that it can be called inside global_func ().

sample&nbsp;007


>>> #Keep the original function
... original_func = None
>>>
>>> def global_func():
...     global original_func
...     print( "in global_func()" )
...     original_func() #The original function should be called
...
>>> def my_decorator_007(func):
...     global original_func
...     print( "in my_decorator_007()" )
...     original_func = func #The func passed as an argument is the global original_To func
...     return global_func
...
>>> @my_decorator_007
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_007()
>>> base_func()     #* I want to call the original function here
in global_func()
in base_func()
>>>

It's a little confusing, but I was able to call it from global_func () by preparing the global variable ʻoriginal_funcand saving thefunc` passed as an argument here.

However, there is one problem with this. When I try to use the decorator on multiple functions, it doesn't work as expected.

sample&nbsp;008


>>> #Keep the original function
... original_func = None
>>>
>>> def global_func():
...     global original_func
...     print( "in global_func()" )
...     original_func() #The original function should be called
...
>>> def my_decorator_007(func):
...     global original_func
...     print( "in my_decorator_007()" )
...     original_func = func #The func passed as an argument is the global original_To func
...     return global_func
...
>>> @my_decorator_007
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_007()
>>> @my_decorator_007
... def base_func_2():
...     print( "in base_func_2()" )
...
in my_decorator_007()
>>> base_func()     # "in base_func()"I want you to be displayed
in global_func()
in base_func_2()    ← "in base_func()"not"in base_func_2()"Was displayed
>>> base_func_2()   # "in base_func_2()"I want you to be displayed
in global_func()
in base_func_2()
>>>

The call to base_func () has executed base_func_2 (). Since there is only one global variable global_func, the last assigned base_func_2 is being executed.

Allow the decorator to be used multiple times (1)

Let's talk about "closures" so that we can use the decorator multiple times.

Take a look at sample_009.py like this:

sample_009.py


 1  def outer(outer_arg):
 2      def inner(inner_arg):
 3          # outer_arg and inner_Output arg
 4          print( "outer_arg: " + outer_arg + ", inner_arg: " + inner_arg )
 5      return inner    # inner()Returns a function object
 6
 7  f1 = outer("first")
 8  f2 = outer("second")
 9
10  f1("f1")
11  f2("f2")

Execute f2 = outer ("second") on line 8 afterf1 = outer ("first")on line 7. Each is assigned a ʻinnerfunction object, but what aboutf1 ("f1") on line 10 and f2 ("f2") `on line 11?

The 8th line says f2 = outer ("second "), so in the 4th line print (), ʻouter_arg becomes "second" `.

The output of f1 ("f1 ") on line 10 and f2 ("f2 ") on line 11 are respectively.

outer_arg: second, inner_arg: f1
outer_arg: second, inner_arg: f2

It seems to be ...

When I run it, it looks like this:

sample&nbsp;009


>>> def outer(outer_arg):
...     def inner(inner_arg):
...         # outer_arg and inner_Output arg
...         print( "outer_arg:", outer_arg, ", inner_arg:", inner_arg )
...     return inner    # inner()Returns a function object
...
>>> f1 = outer("first")
>>> f2 = outer("second")
>>>
>>> f1("f1")
outer_arg: first, inner_arg: f1
>>> f2("f2")
outer_arg: second, inner_arg: f2
>>>

this is,

  1. The ʻinner ()function definitiondef inner (): ... is "executed" each time the ʻouter () function is called.
  2. The outer ʻouter_arg referenced when the ʻinner () function is defined (executing the definition) is fixed (evaluated) and held by the inner. This is to make the movement. This is called a closure. Variables in the outer scope are evaluated at definition time, not at run time.

Use this to allow the decorator to be used multiple times.

Allow the decorator to be used multiple times (2)

In sample 009, we used a global function, but in order to use the closure function, we define the function inside the decorator function.

sample_010.py


def my_decorator_010(func):
    print( "in my_decorator_010()" )
    def inner():
        print( "in inner() and calling", func )
        func()
        print( "in inner() and returnd from ", func )
    print( "in my_decorator_010(), leaving ..." )
    return inner

@my_decorator_010
def base_func():
    print( "in base_func()" )

@my_decorator_010
def base_func_2():
    print( "in base_func_2()" )

base_func()     # "in base_func()"I want you to be displayed
base_func_2()   # "in base_func_2()"I want you to be displayed

Let's do this (enter interactively).

sample&nbsp;010


>>> def my_decorator_010(func):
...     print( "in my_decorator_010()" )
...     def inner():
...         print( "in inner() and calling", func )
...         func()
...         print( "in inner() and returnd from ", func )
...     print( "in my_decorator_010(), leaving ..." )
...     return inner
...
>>> @my_decorator_010
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_010()
in my_decorator_010(), leaving ...
>>> @my_decorator_010
... def base_func_2():
...     print( "in base_func_2()" )
...
in my_decorator_010()
in my_decorator_010(), leaving ...
>>> base_func()     # "in base_func()"I want you to be displayed
in inner() and calling <function base_func at 0x769d1858>
in base_func()
in inner() and returnd from  <function base_func at 0x769d1858>
>>> base_func_2()   # "in base_func_2()"I want you to be displayed
in inner() and calling <function base_func_2 at 0x769d1930>
in base_func_2()
in inner() and returnd from  <function base_func_2 at 0x769d1930>
>>>

The results were as expected.

So far, we know that the decorator should be coded as follows:

sample&nbsp;011


def my_decorator_011(func):
    def inner():
        #Write the pre-call process here
        func()
        #Write the post-call process here
    return inner

Decorator for functions that return a return value

The decorators up to this point are

  1. Do not return a value
  2. No arguments I was able to use it for the function.

If you want to make a general-purpose decorator, you need to meet the above two requirements.

The program considering the return value is as follows.

sample_012.py


def my_decorator_012(func):
    def inner():
        #Write the pre-call process here
        ret = func()
        #Write the post-call process here
        return ret
    return inner

@my_decorator_012
def base_func_1():
    print( "in base_func_1()" )
    return 1

@my_decorator_012
def base_func_2():
    print( "in base_func_2()" )

r1 = base_func_1()
print( r1 )
base_func_2()

The execution result is as follows.

sample&nbsp;012


>>> def my_decorator_012(func):
...     def inner():
...         #Write the pre-call process here
...         ret = func()
...         #Write the post-call process here
...         return ret
...     return inner
...
>>> @my_decorator_012
... def base_func_1():
...     print( "in base_func_1()" )
...     return 1
...
>>> @my_decorator_012
... def base_func_2():
...     print( "in base_func_2()" )
...
>>> r1 = base_func_1()
in base_func_1()
>>> print( r1 )
1
>>> base_func_2()
in base_func_2()
>>>

It worked even if there was no return value (base_func_2 ()).

Decorator for functions that take arguments

See also: parameter-Glossary — Python 3.8.1 Documentation

Function arguments can be received as formal arguments for which the number of arguments and keyword specification are not determined by the variable-length position parameter and variable-length keyword parameter.

  • Variable length positions : Any number of position arguments (in addition to any position arguments already received by other formal arguments) Is specified. Such formal arguments should be preceded by the formal argument name, such as args below, * < It can be defined by adding / code>:

    def func(*args, **kwargs): ...
    
  • Variable-length keywords : Given any number of keyword arguments (in addition to any keyword arguments already received by other formal arguments) Is specified. Such formal arguments should be ** kwargs in the example above It can be defined by adding span> .

(Quoted from parameter-Glossary — Python 3.8.1 Documentation)

Simply put, you can take variadic arguments by specifying (* args, ** kwargs) as function arguments.

Using this, the decorator for a function that takes an argument can be written as:

sample_013.py


def my_decorator_013(func):
    def inner( *args, **kwargs ):
        #Write the pre-call process here
        ret = func( *args, **kwargs )
        #Write the post-call process here
        return ret
    return inner

@my_decorator_013
def base_func_1(arg1, arg2, arg3="arg3"):
    print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
    return 1

@my_decorator_013
def base_func_2():
    print( "in base_func_2()" )

r1 = base_func_1("arg1","arg2")
print( r1 )
base_func_2()

The following is the execution result.

sample&nbsp;013


>>> def my_decorator_013(func):
...     def inner( *args, **kwargs ):
...         #Write the pre-call process here
...         ret = func( *args, **kwargs )
...         #Write the post-call process here
...         return ret
...     return inner
...
>>> @my_decorator_013
... def base_func_1(arg1, arg2, arg3="arg3"):
...     print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
...     return 1
...
>>> @my_decorator_013
... def base_func_2():
...     print( "in base_func_2()" )
...
>>> r1 = base_func_1("arg1","arg2")
in base_func_1(arg1, arg2, arg3)
>>> print( r1 )
1
>>> base_func_2()
in base_func_2()
>>>

Exception handling

Also consider exceptions.

sample_014.py


def my_decorator_014(func):
    def inner( *args, **kwargs ):
        #Write the pre-call process here
        try:
            ret = func( *args, **kwargs )
        except:
            raise
        #Write the post-call process here
        return ret
    return inner

@my_decorator_014
def base_func_1(arg1, arg2, arg3="arg3"):
    print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
    return 1

@my_decorator_014
def base_func_2():
    print( "in base_func_2()" )
    na = 1 / 0      #Division by zero exception occurs

r1 = base_func_1("arg1","arg2")
print( r1 )

try:
    base_func_2()
except ZeroDivisionError:
    print( "Zero Division Error" )

The following is the execution result.

sample&nbsp;014


>>> def my_decorator_014(func):
...     def inner( *args, **kwargs ):
...         #Write the pre-call process here
...         try:
...             ret = func( *args, **kwargs )
...         except:
...             raise
...         #Write the post-call process here
...         return ret
...     return inner
...
>>> @my_decorator_014
... def base_func_1(arg1, arg2, arg3="arg3"):
...     print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
...     return 1
...
>>> @my_decorator_014
... def base_func_2():
...     print( "in base_func_2()" )
...     na = 1 / 0      #Division by zero exception occurs
...
>>> r1 = base_func_1("arg1","arg2")
in base_func_1(arg1, arg2, arg3)
>>> print( r1 )
1
>>>
>>> try:
...     base_func_2()
... except ZeroDivisionError:
...     print( "Zero Division Error" )
...
in base_func_2()
Zero Division Error
>>>

Pass arguments to the decorator

Since a decorator is a function (a callable object), consider passing an argument to it.

The description of the function definition quoted at the beginning also included an example of a decorator with arguments.

@f1(arg)
@f2
def func(): pass
def func(): pass
func = f1(arg)(f2(func))

Since the decorators are nested, consider only one decorator for simplicity.

@my_decorator('argument')
def func(arg1, arg2, arg3):
    pass

This is equivalent to below.

def func(arg1, arg2, arg3):
    pass
func = my_decorator('argument')(func)

It means to execute _my_decorator (func) using the function (let's say _my_decorator) returned by calling my_decorator ('argument').

The nesting becomes one step deeper, and the outermost function (decorator) receives the argument, and the decorator so far is included in it.

sample015.py


def my_decorator_015( arg1 ):
    def _my_decorator( func ):
        def inner( *args, **kwargs ):
            print( "in inner, arg1={}, func={}".format(arg1, func.__name__) )
            ret = func( *args, **kwargs )
            print( "in inner leaving ..." )
            return ret
        return inner
    return _my_decorator

The following is the execution result.

sample&nbsp;015


>>> def my_decorator_015( arg1 ):
...     def _my_decorator( func ):
...         def inner( *args, **kwargs ):
...             print( "in inner, arg1={}, func={}".format(arg1, func.__name__) )
...             ret = func( *args, **kwargs )
...             print( "in inner leaving ..." )
...             return ret
...         return inner
...     return _my_decorator
...
>>> @my_decorator_015('argument')
... def f_015( arg1, arg2, arg3 ):
...     print( "in func( {}, {}, {} )".format(arg1, arg2, arg3) )
...
>>> f_015( "Argument 1", "Argument 2", "Argument 3" )
in inner, arg1=argument, func=f_015
in func(Argument 1,Argument 2,Argument 3)
in inner leaving ...
>>>

When you call the decorator with arguments, the bottom part of arg1 and func are stored in _my_decorator_body.

             print( "in _my_decorator_body, arg1={}, func={}".format(arg1, func.__name__) )

Therefore, when you call f_015 (), the saved arg1 and func are referenced.

Once summarized

Decorator with no arguments

The decorator that takes no arguments looks like this:

sample_014.py


def my_decorator_014(func):
    def inner( *args, **kwargs ):
        #Write the pre-call process here
        try:
            ret = func( *args, **kwargs )
        except:
            raise
        #Write the post-call process here
        return ret
    return inner

Decorator taking arguments

The decorator that passes the arguments looks like this:

sample_016.py


def my_decorator_016( arg1 ):
    def _my_decorator( func ):
        def inner( *args, **kwargs ):
            #Write the pre-call process here
            try:
                ret = func( *args, **kwargs )
            except:
                raise
            #Write the post-call process here
            return ret
        return inner
    return _my_decorator

Decorator that absorbs the presence or absence of arguments

What if you use a decorator that assumes arguments without giving any arguments?

sample_017.py


def my_decorator_017( arg1 ):
    def _my_decorator( func ):
        def inner( *args, **kwargs ):
            #Write the pre-call process here
            try:
                ret = func( *args, **kwargs )
            except:
                raise
            #Write the post-call process here
            return ret
        return inner
    return _my_decorator

@my_decorator_017
def f_017( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_017.__name__, arg1, arg2, arg3) )

f_017( "Argument 1", "Argument 2", "Argument 3" )

Execution result


$ python sample_017.py
Traceback (most recent call last):
  File "sample_017.py", line 21, in <module>
    f_017( "Argument 1", "Argument 2", "Argument 3" )
TypeError: _my_decorator() takes 1 positional argument but 3 were given
$

This is because you haven't passed any arguments to @ my_decorator_017, so it's called asf_017 = my_decorator_017 (f_017). There are two ways to avoid this.

  1. Even if you don't specify an argument to the decorator, be sure to add () and receive it as a variadic argument.
  2. If no argument is specified for the decorator, a single function object will be passed, so it will be judged.

This is the first example.

sample_018.py


def my_decorator_018( *args, **kwargs ):
    def _my_decorator( func ):
        def inner( *inner_args, **inner_kwargs ):
            #Write the pre-call process here
            try:
                ret = func( *inner_args, **inner_kwargs )
            except:
                raise
            #Write the post-call process here
            return ret
        return inner
    return _my_decorator

@my_decorator_018()
def f_018( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_018.__name__, arg1, arg2, arg3) )

f_018( "Argument 1", "Argument 2", "Argument 3" )

Execution result


$ python sample_018.py
in inner(Argument 1,Argument 2,Argument 3)

The second method uses the first argument of the decorator for the decision.

sample_019.py


def my_decorator_019( *args, **kwargs ):
    def _my_decorator( func ):
        def inner( *inner_args, **inner_kwargs ):
            #Write the pre-call process here
            try:
                ret = func( *inner_args, **inner_kwargs )
            except:
                raise
            #Write the post-call process here
            return ret
        return inner
    if len(args) == 1 and callable(args[0]):
        return _my_decorator( args[0] )
    else:
        return _my_decorator

@my_decorator_019
def f_019_1( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_019_1.__name__, arg1, arg2, arg3) )

@my_decorator_019()
def f_019_2( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_019_2.__name__, arg1, arg2, arg3) )

@my_decorator_019('arg')
def f_019_3( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_019_3.__name__, arg1, arg2, arg3) )

f_019_1( "Argument 1", "Argument 2", "Argument 3" )
f_019_2( "Argument 1", "Argument 2", "Argument 3" )
f_019_3( "Argument 1", "Argument 2", "Argument 3" )

To distinguish between the arguments passed to the decorator and the arguments passed when f_019_ * () is called, they are distinguished by (* args, ** kwargs) and (* inner_args, ** inner_kwargs), respectively. doing.

Execution result


$ python sample_019.py
in inner(Argument 1,Argument 2,Argument 3)
in inner(Argument 1,Argument 2,Argument 3)
in inner(Argument 1,Argument 2,Argument 3)

Now that you have a general-purpose decorator framework ... I'd like to think ... the function names you called (f_019_ * .__ name__) are all ʻinner`.

Final finish

You can see that the function ʻinner is called instead of the original function. However, the original function may refer to attributes such as name (name) and documentation (doc`).

There is a decorator called @wraps that can work around this.

See also: [@ functools.wraps () --functools --- Manipulating higher-order functions and callable objects — Python 3.8.1 documentation](https://docs.python.org/ja/3/library/functools.html? highlight = wraps # functools.wraps) See also: [functools.update_wrapper () --functools --- Manipulating higher-order functions and callable objects — Python 3.8.1 documentation](https://docs.python.org/ja/3/library/functools.html?highlight = wraps # functools.update_wrapper)

Decorating this before the def inner allows you to wrap and preserve the attributes of the argument func passed to the outer function (_my_decorator ()).

sample_020.py


from functools import wraps

def my_decorator_020( *args, **kwargs ):
    def _my_decorator( func ):
        @wraps(func)
        def inner( *inner_args, **inner_kwargs ):
            #Write the pre-call process here
            try:
                ret = func( *inner_args, **inner_kwargs )
            except:
                raise
            #Write the post-call process here
            return ret
        return inner
    if len(args) == 1 and callable(args[0]):
        return _my_decorator( args[0] )
    else:
        return _my_decorator

@my_decorator_020
def f_020_1( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_020_1.__name__, arg1, arg2, arg3) )

@my_decorator_020()
def f_020_2( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_020_2.__name__, arg1, arg2, arg3) )

@my_decorator_020('arg')
def f_020_3( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_020_3.__name__, arg1, arg2, arg3) )

f_020_1( "Argument 1", "Argument 2", "Argument 3" )
f_020_2( "Argument 1", "Argument 2", "Argument 3" )
f_020_3( "Argument 1", "Argument 2", "Argument 3" )

For sample_019.py,@wraps (func)is added before from functools import wraps and def inner (). func is the function passed as an argument to_my_decorator ().

Execution result


$ python sample_020.py
in f_020_1(Argument 1,Argument 2,Argument 3)
in f_020_2(Argument 1,Argument 2,Argument 3)
in f_020_3(Argument 1,Argument 2,Argument 3)
$

The name of the function (f_019_ * .__ name__) is retained as the name of the original function instead of ʻinner`.

It's been a long way, but finally the decorator framework is complete.

Since it was created generically, it is def my_decorator (* args, ** kwargs):, but if the arguments to receive are fixed, it is better to clarify.

decorator_framework.py Full Version

decorator_framework.py&nbsp;Full&nbsp;Version


#!/usr/bin/env python
# -*- coding: utf-8 -*-

###########################################
#Decorator(decorator)
###########################################
from functools import wraps

def my_decorator( *args, **kwargs ):
    """
    for doctest

    >>> @my_decorator
    ... def f1( arg1 ):
    ...     print( arg1 )
    ...
Write here if processing is required when the decorator is listed#1
    No arguments
    _my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
    >>> @my_decorator('mytest1')
    ... def f2( arg2 ):
    ...     print( arg2 )
    ...
Write here if processing is required when the decorator is listed#1
    There are some arguments: ('mytest1',)
    _my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
    >>> @my_decorator
    ... def f3( arg1 ):
    ...     print( arg1 )
    ...     a = 1/0
    ...
Write here if processing is required when the decorator is listed#1
    No arguments
    _my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
    >>> @my_decorator('mytest2')
    ... def f4( arg2 ):
    ...     print( arg2 )
    ...     a = 1/0
    ...
Write here if processing is required when the decorator is listed#1
    There are some arguments: ('mytest2',)
    _my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
    >>> try:
    ...     f1( "Hello, World! #1" )
    ... except:
    ...     print( "error #1" )
    ...
Pre-processing is done here(<function f1 at 0x...>,) {} ('Hello, World! #1',) {}
    Hello, World! #1
Post-processing is done here... {} ('Hello, World! #1',) {}
    >>> try:
    ...         f2( "Hello, World! #2" )
    ... except:
    ...         print( "error #2" )
    ...
Pre-processing is done here('mytest1',) {} ('Hello, World! #2',) {}
    Hello, World! #2
Post-processing is done here('mytest1',) {} ('Hello, World! #2',) {}
    >>> try:
    ...         f3( "Hello, World! #3" )
    ... except:
    ...         print( "error #3" )
    ...
Pre-processing is done here(<function f3 at 0x...>,) {} ('Hello, World! #3',) {}
    Hello, World! #3
    error #3
    >>> try:
    ...         f4( "Hello, World! #4" )
    ... except:
    ...         print( "error #4" )
    ...
Pre-processing is done here('mytest2',) {} ('Hello, World! #4',) {}
    Hello, World! #4
    error #4
    >>>
    """

    #Click here for decorators that clearly take no arguments
    # _my_Rename decorator and define globally
    def _my_decorator( func ):
        # _my_decorator_body()If there is any necessary processing before defining, write it here
        print( "_my_decorator_body()If there is any necessary processing before defining, write it here" )
        @wraps(func)
        def _my_decorator_body( *body_args, **body_kwargs ):
            #Pre-processing is done here
            print( "Pre-processing is done here", args, kwargs, body_args, body_kwargs )
            try:
                #Execution of the decorated body
                ret = func( *body_args, **body_kwargs )
            except:
                raise
            #Post-processing is done here
            print( "Post-processing is done here", args, kwargs, body_args, body_kwargs )
            return ret

        #Write here if processing is required when the decorator is listed#2
        print( "Write here if processing is required when the decorator is listed#2" )
        return _my_decorator_body

    #This is the end of the decorator that clearly takes no arguments

    #Write here if processing is required when the decorator is listed#1
    print( "Write here if processing is required when the decorator is listed#1" )

    if len(args) == 1 and callable( args[0] ):
        #If the decorator is called with no arguments, handle it here
        print( "No arguments" )
        return _my_decorator( args[0] )

    else:
        #If the decorator is called with arguments, handle it here
        print( "There are some arguments:", args )
        return _my_decorator


###########################################
#   unitttest
###########################################
import unittest
from io import StringIO
import sys

class Test_My_Decorator(unittest.TestCase):
    def setUp(self):
        self.saved_stdout = sys.stdout
        self.stdout = StringIO()
        sys.stdout = self.stdout

    def tearDown(self):
        sys.stdout = self.saved_stdout

    def test_decorator_noarg(self):
        @my_decorator
        def t1(arg0):
            print( arg0 )

        t1("test_decorator_noarg")

        import re

        s = re.sub('0x[0-9a-f]+', '0x', self.stdout.getvalue())

        self.assertEqual(s,
            "Write here if processing is required when the decorator is listed#1\n" +
            "No arguments\n" +
            "_my_decorator_body()If there is any necessary processing before defining, write it here\n" +
            "Write here if processing is required when the decorator is listed#2\n" +
            "Pre-processing is done here(<function Test_My_Decorator.test_decorator_noarg.<locals>.t1 at 0x>,) {} ('test_decorator_noarg',) {}\n" +
            "test_decorator_noarg\n" +
            "Post-processing is done here(<function Test_My_Decorator.test_decorator_noarg.<locals>.t1 at 0x>,) {} ('test_decorator_noarg',) {}\n"
            )

    def test_decorator_witharg(self):
        @my_decorator('with arg')
        def t1(arg0):
            print( arg0 )

        t1("test_decorator_witharg")

        self.assertEqual(self.stdout.getvalue(),
            "Write here if processing is required when the decorator is listed#1\n" +
            "There are some arguments: ('with arg',)\n" +
            "_my_decorator_body()If there is any necessary processing before defining, write it here\n" +
            "Write here if processing is required when the decorator is listed#2\n" +
            "Pre-processing is done here('with arg',) {} ('test_decorator_witharg',) {}\n" +
            "test_decorator_witharg\n" +
            "Post-processing is done here('with arg',) {} ('test_decorator_witharg',) {}\n"
            )

    def test_functionname(self):
        @my_decorator
        def t1():
            return t1.__name__

        f_name = t1()

        self.assertEqual( f_name, "t1" )

    def test_docattribute(self):
        @my_decorator
        def t1():
            """Test Document"""
            pass

        self.assertEqual( t1.__doc__, "Test Document" )


###########################################
#   main
###########################################
if __name__ == '__main__':

    @my_decorator
    def f1( arg1 ):
        print( arg1 )

    @my_decorator('mytest1')
    def f2( arg2 ):
        print( arg2 )

    @my_decorator
    def f3( arg1 ):
        print( arg1 )
        a = 1/0

    @my_decorator('mytest2')
    def f4( arg2 ):
        print( arg2 )
        a = 1/0

    try:
        f1( "Hello, World! #1" )
    except:
        print( "error #1" )

    try:
        f2( "Hello, World! #2" )
    except:
        print( "error #2" )

    try:
        f3( "Hello, World! #3" )
    except:
        print( "error #3" )

    try:
        f4( "Hello, World! #4" )
    except:
        print( "error #4" )

    import doctest
    doctest.testmod(optionflags=doctest.ELLIPSIS)

    unittest.main()

Sample execution


$ python decorator_framework.py
Write here if processing is required when the decorator is listed#1
No arguments
_my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
Write here if processing is required when the decorator is listed#1
There are some arguments: ('mytest1',)
_my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
Write here if processing is required when the decorator is listed#1
No arguments
_my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
Write here if processing is required when the decorator is listed#1
There are some arguments: ('mytest2',)
_my_decorator_body()If there is any necessary processing before defining, write it here
Write here if processing is required when the decorator is listed#2
Pre-processing is done here(<function f1 at 0x76973a08>,) {} ('Hello, World! #1',) {}
Hello, World! #1
Post-processing is done here(<function f1 at 0x76973a08>,) {} ('Hello, World! #1',) {}
Pre-processing is done here('mytest1',) {} ('Hello, World! #2',) {}
Hello, World! #2
Post-processing is done here('mytest1',) {} ('Hello, World! #2',) {}
Pre-processing is done here(<function f3 at 0x7685bd20>,) {} ('Hello, World! #3',) {}
Hello, World! #3
error #3
Pre-processing is done here('mytest2',) {} ('Hello, World! #4',) {}
Hello, World! #4
error #4
....
----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK
$

unittest


$ python -m unittest -v decorator_framework.py
test_decorator_noarg (decorator_framework.Test_My_Decorator) ... ok
test_decorator_witharg (decorator_framework.Test_My_Decorator) ... ok
test_docattribute (decorator_framework.Test_My_Decorator) ... ok
test_functionname (decorator_framework.Test_My_Decorator) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.006s

OK
$

Recommended Posts