DEV Community

Apcelent
Apcelent

Posted on • Edited on

Python Decorator with Arguments Python Decorator Tutorial with Example

If you want to take a really deep dive, you should read these exhaustive articles by Graham Dumpleton. However, if you intend to get started and getting better at reading/writing python decorators, this article should suffice.

Everything is an object in python, even functions. A function can be assigned to a variable, passed to another function and can be returned from another function. Take a look at the example below:

[1]: def outer_function():
   ...:     print "1. This is outer function!"
   ...:     def inner_function():
   ...:         print "2. This is inner function, inside outer function!"
   ...:     print "3. This is outside inner function, inside outer function!"
   ...:     return inner_function()
   ...:

[2]: func_assign = outer_function()
1. This is outer function!
3. This is outside inner function, inside outer function!
2. This is inner function, inside outer function!
Enter fullscreen mode Exit fullscreen mode

Mark in the above execution, how the statement inside the inner function is printed at the last, consequential to inner_function being returned, at the end of outer_function, and the execution could be seen during the assignment.

Python decorator are the function that receive a function as an argument and return another function as return value. The assumption for a decorator is that we will pass a function as argument and the signature of the inner function in the decorator must match the function to decorate.

Function Decorator

Let us now, write a simple function decorator for ourselves. We will write a decorator that would measure the execution time of the function passed to it.

import time

def timetest(input_func):

    def timed(*args, **kwargs):

    start_time = time.time()
    result = input_func(*args, **kwargs)
    end_time = time.time()
    print "Method Name - {0}, Args - {1}, Kwargs - {2}, Execution Time - {3}".format(
        input_func.__name__,
        args,
        kwargs,
        end_time - start_time
    )
    return result
    return timed


@timetest
def foobar(*args, **kwargs):
    time.sleep(0.3)
    print "inside foobar"
    print args, kwargs

foobar(["hello, world"], foo=2, bar=5)

inside foobar
(['hello, world'],) {'foo': 2, 'bar': 5}
Method Name - foobar, Args - (['hello, world'],), Kwargs - {'foo': 2, 'bar': 5}, Execution Time - 0.30296087265
Enter fullscreen mode Exit fullscreen mode

We passed the function foobar to decorator named timetest. Inside decorator, function foobar is referenced as variable input_func. The result, post execution of input_func is referred as result.

Prepending @ to the name of the decorator, and writing the same above a function calls the decorator, and passes the function to the decorator(decorates).

Method Decorator

Method decorators allow overriding class properties by decorating, without having to find the calling function.

def method_decorator(method):

    def inner(city_instance):
    if city_instance.name == "SFO":
        print "Its a cool place to live in."
    else:
        method(city_instance)
    return inner


class City(object):

    def __init__(self, name):
    self.name = name

    @method_decorator
    def print_test(self):
    print self.name

p1 = City("SFO")

p1.print_test()

Its a cool place to live in.
Enter fullscreen mode Exit fullscreen mode

In the snippet shown above, we decorate the class method print_test. The method_decorator prints the name of the city, if the name of city instance is not SFO.

Class Decorators

If you want to create a callable returning another callable, the function decorator approach is easier. If you want the return to be a function, function decorators should be preferred, however if you want the decorator to return a custom object that does something different to what a function does, in that case a class decorator should be used.

With a class, you can add methods and properties to the decorated callable object, or implement operations on them. You can create descriptors that act in a special way when placed in classes (e.g. classmethod, property)

class decoclass(object):

    def __init__(self, f):
    self.f = f

    def __call__(self, *args, **kwargs):
    # before f actions
    print 'decorator initialised'
    self.f(*args, **kwargs)
    print 'decorator terminated'
    # after f actions

@decoclass
def klass():
    print 'class'

klass()
Enter fullscreen mode Exit fullscreen mode

Chaining Decorators

The chaining of decorator is similar to how multiple inheritance can be used to construct classes We can write as many decorator as we want and include them one by one in decoration line with decoration syntax before the definition of function to be decorated.

def makebold(f):
    return lambda: "<b>" + f() + "</b>"
def makeitalic(f):
    return lambda: "<i>" + f() + "</i>"

@makebold
@makeitalic
def say():
    return "Hello"

print say()
Enter fullscreen mode Exit fullscreen mode

One thing should be kept in mind that the order of decorators we set matters. When you chain decorators, the order in which they are stacked is bottom to top.

Functools and Wraps

When we use a decorator, we are replacing one functions with another.

def decorator(func):
    """decorator docstring"""
    def inner_function(*args, **kwargs):
    """inner function docstring """
    print func.__name__ + "was called"
    return func(*args, **kwargs)
    return inner_function


@decorator
def foobar(x):
    """foobar docstring"""
    return x**2
Enter fullscreen mode Exit fullscreen mode

If we try printing the name and docstring we see the following

print foobar.__name__
print foobar.__doc__

inner_function
inner function docstring
Enter fullscreen mode Exit fullscreen mode

The above observation leads us to conclude that the function foobar is being replaced by inner_function. This means that we are losing information about the function which is being passed. functools.wraps comes to our rescue. It takes the function used in the decorator and adds the functionality of copying over the function name, docstring, arguemnets etc. Lets decorate without losing information:

from functools import wraps

def wrapped_decorator(func):
    """wrapped decorator docstring"""
    @wraps(func)
    def inner_function(*args, **kwargs):
    """inner function docstring """
    print func.__name__ + "was called"
    return func(*args, **kwargs)
    return inner_function


@wrapped_decorator
def foobar(x):
    """foobar docstring"""
    return x**2

print foobar.__name__
print foobar.__doc__

foobar
foobar docstring
Enter fullscreen mode Exit fullscreen mode

The above implementation preserves the information about the funciton being passed to the decorator.

How would you go about caching information inside a class based decorator?

One of the ways of doing it, is listed here , would love to see more implementation, in comments.

Decorators with Arguments

Function Decorator with Arguments

from functools import wraps

def decorator(arg1, arg2):

    def inner_function(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        print "Arguements passed to decorator %s and %s" % (arg1, arg2)
        function(*args, **kwargs)
    return wrapper
    return inner_function


@decorator("arg1", "arg2")
def print_args(*args):
    for arg in args:
    print arg

 print print_args(1, 2, 3)

 Arguements passed to decorator arg1 and arg2
 1
 2
 3
Enter fullscreen mode Exit fullscreen mode

Class Based Decorators with Arguments

class ClassDecorator(object):

    def __init__(self, arg1, arg2):
    print "Arguements passed to decorator %s and %s" % (arg1, arg2)
    self.arg1 = arg1
    self.arg2 = arg2

    def __call__(self, foo, *args, **kwargs):

    def inner_func(*args, **kwargs):
        print "Args passed inside decorated function .%s and %s" % (self.arg1, self.arg2)
        return foo(*args, **kwargs)
    return inner_func


@ClassDecorator("arg1", "arg2")
def print_args(*args):
    for arg in args:
    print arg


 print_args(1, 2, 3)
 Arguements passed to decorator arg1 and arg2
 Args passed inside decorated function .arg1 and arg2
 1
 2
 3
Enter fullscreen mode Exit fullscreen mode

How would you go about implementing decorator with optional arguments?

Try following this SO Post.

You might want to further explore the Wrapt Library.

The article originally appeared on Apcelent Tech Blog.

Top comments (5)

Collapse
 
otacilion profile image
Otacilio Saraiva Maia Neto

In this piece of code:

    def inner_function(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        print "Arguements passed to decorator %s and %s" % (arg1, arg2)
        function(*args, **kwargs)
    return wrapper
    return inner_function
Enter fullscreen mode Exit fullscreen mode


`
is it missing some identation?

Collapse
 
sp1thas profile image
Panagiotis Simakis

Good catch! I'm pretty sure that should look like:

def decorator(arg1, arg2):

    def inner_function(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            print "Arguements passed to decorator %s and %s" % (arg1, arg2)
            function(*args, **kwargs)
        return wrapper
    return inner_function
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vberlier profile image
Valentin Berlier • Edited

There's also the pattern with functools.partial that allows you to call decorators with optional arguments without the parenthesis if you want to use the default values.

from functools import partial, wraps

def print_result(func=None, *, prefix=''):
    if func is None:
        return partial(print_result, prefix=prefix)

    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'{prefix}{result}')
        return result
    return wrapper

Now you can apply the decorator without the parenthesis if you want to use the default value.

@print_result
def add(a, b):
    return a + b

add(2, 3)  # outputs '5'

@print_result()
def add(a, b):
    return a + b

add(2, 3)  # outputs '5'

@print_result(prefix='The return value is ')
def add(a, b):
    return a + b

add(2, 3)  # outputs 'The return value is 5'

There's no way of getting it wrong when you apply the decorator. IMO it's the simplest and most readable pattern. I use it all the time.

Collapse
 
djangotricks profile image
Aidas Bendoraitis

Looks like a useful article. But can you please correct the indentation of the Python code?

Collapse
 
andrejhatzi profile image
Andrej Hatzi

timed does not exist, maybe thou should test the code before posting it!