By Md. Sabuj Sarker | 12/29/2017 | General |Intermediate

Understanding Advanced Decorators With Parameters In Python

Understanding Advanced Decorators With Parameters In Python

In a previous guide I introduced you to the concept of decorators in Python. We could pass parameters to the decorated functions in the same way we did for the original functions. And it served our purpose very well. But we the humans have insatiable taste, never-to-be-satisfied desires and infinite demands. If you are building a library, framework, or API, you may want to make things simple with the help of decorators. To make their life easier you can let them pass parameters to not only the functions but also the decorators. It may make the user’s (or other programmer’s) life easier but will surely make your life harder—harder to code these things.

In this guide I will show you how to customize the behavior of our decorators and functions by passing extra parameters to the decorators.

Before You Begin

This is an advanced level guide, so beginners are not welcome here, though they can easily read this and practice the code without problems. Make sure you satisfy the following list before you begin.

  • You must have read through and practiced all the code in the previous guide on decorators in Python.
  • You must have Python 3+ installed on your system.
  • You should have familiarity working with the command line on your system.

Preparing the Environment

To go along with this guide you should choose a directory where you want to keep the Python script files. You can also create a new directory for this. Create a Python file called dec2.py in that directory. As the code shown in this guide does not depend on any external library, you do not need to create a virtual environment.

You can choose any plain text editor, code editor, or Python supported IDE to write and edit code shown in this guide. We will run our Python script with the help of following command:

$ python3 dec2.py

Remember to start your command line in the chosen directory or you may cd into it.

Calling a Decorator During Decorating a Function

In the previous guide we decorated say_something() function like below:

@print_decorator
def say_something(what):
   print(what)

Notice, print_decorator is a function and we are not calling it. Then how on earth it is wrapping say_something function, returning another function and replacing the function by another function keeping the same name in the scope?

All of these are being done by Python itself. To see the proof, let's try to call the decorator during decoration.

def print_decorator(a_fun):
   def inner_fun(*args, **kwargs):
       print(a_fun.__name__ + ":")
       a_fun(*args, **kwargs)
       print()
   return inner_fun

@print_decorator(say_something)
def say_something(what):
   print(what)

Ah! A horrible mistake. It throws errors and throws the following exception.

NameError: name 'say_something' is not defined

What if we just called it without arguments?

@print_decorator()
def say_something(what):
   print(what)

Exception thrown is:

TypeError: print_decorator() missing 1 required positional argument: 'a_fun'

If we cannot call the decorator in any convenient way then how on earth can we pass some extra arguments to customize the behavior further?

Let's try to decorate the function name after the function declaration.

def say_something(what):
   print(what)

@print_decorator(say_something)

Exception thrown is:

SyntaxError: invalid syntax

But we could easily do this by calling print_decorator as a function with and assigning the return value to the function name say_something.

def say_something(what):
   print(what)

say_something = print_decorator(say_something)

Now all is well. But wait! We are back to where we started in the first guide. If we need to do it like that then we did not need to use cute syntax of decorators. We need to have the decorator syntax and need to pass extra parameters in that way, period.

The Customization We Want

We need to move forward to certain goals. What do we want to customize? Let's say that we want to show a different name in the output than the original function name and we want to pass the secondary name through a decorator.

Inner Functions Inside Inner Functions

Creating a decorator needs you to create an inner function. Inner functions seem like a complex concept to many people when it comes to resolving to scope of parameters of different inner functions and their propagation.

To make things worse we need to create another inner function inside the inner function we already have. In this way it may be hard for you but will be easier for your function users.

In this way the outermost function (print_decorator) will act as a normal function, first the nested function will work as the decorator function and the second nested function is what we will return.

So, print_statement will be the parameter acceptor and will not accept functions any more. So, my code looks like the following:

def print_decorator(name=None):
   def the_decorator(a_fun):
       def wrapper_fun(*args, **kwargs):
           custom_name = name
           if not custom_name:
               custom_name = a_fun.__name__
           print(custom_name + ":")
           a_fun(*args, **kwargs)
           print()
       return wrapper_fun
   return the_decorator

@print_decorator("the_speaker")
def say_something(what):
   print(what)


say_something("I want to stay silent")

This will output:

the_speaker:
I want to stay silent

Try with passing no values to the decorator call.

So, now your users of the decorator know how to customize the function name to be printed on the console. It's very normal that when they do not want to customize the name they will code it like below:

@print_decorator
def say_something(what):
   print(what)

Notice: we are not putting () after @print_decorator.

They will hit the following output.

<function __main__.print_decorator.<locals>.the_decorator.<locals>.wrapper_fun>

This is because this time the outermost function was not invoked. To solve the problem and make your function/api users' life a bed of rose you have to code like below.

def print_decorator(*args, **kwargs):
   if len(args) == 1:
       if callable(args[0]):
           a_fun = args[0]
           name = kwargs.get('name', None)
       else:
           a_fun = None
           name = args[0]
   elif len(args) == 0:
       a_fun = None
       name = None
   else:
       raise Exception("Invalid number of arguments provided")



   if a_fun:
       # The user used decorator as "@print_decorator"
       def wrapper_fun(*args, **kwargs):
           custom_name = kwargs.get('name', None)
           if not custom_name:
               custom_name = a_fun.__name__
           print(custom_name + ":")
           a_fun(*args, **kwargs)
           print()
       return wrapper_fun
   else:
       # The user used decorator as "@print_decorator()" or "@print_decorator("a name")" or "@print_decorator(name="a name")"

       def the_decorator(a_fun):
           def wrapper_fun(*args, **kwargs):
               custom_name = name
               if not custom_name:
                   custom_name = a_fun.__name__
               print(custom_name + ":")
               a_fun(*args, **kwargs)
               print()
           return wrapper_fun
       return the_decorator

@print_decorator("speaker")
def say_something(what):
   print(what)

say_something("I want to stay silent")

Now try this with:

@print_decorator
def say_something(what):
   print(what)

And with:

@print_decorator()
def say_something(what):
   print(what)

Every time you will see your expected output.

Note: We have code duplication for wrapper_fun, I did that to make things less complicated for you. You can create wrapper_fun once and provide proper name variable from the right scope to avoid this duplication. Try this, it is not that hard if you know how to work with the scope and the **kwargs.

In our code we first tried to find out whether the print_decorator was used as "@print_decorator" or by calling it. In that way we did set a_fun to the function or to None. If "@print_decorator" were used then Python is calling it with the function below that. If it is being called by us then we are returning the decorator from inside it and Python calling that decorator function with the function object declared below it. That's how it works.

Conclusion

This guide may seem hard to many of you. To have the proper understanding, you need to practice all the code by yourself and also try different combinations. I also recommend you try to understand how scope works in Python and how to use *args and **kwargs in different situations. In a later guide I will talk about class decorators in Python.

By Md. Sabuj Sarker | 12/29/2017 | General

{{CommentsModel.TotalCount}} Comments

Your Comment

{{CommentsModel.Message}}

Recent Stories

Top DiscoverSDK Experts

User photo
3355
Ashton Torrence
Web and Windows developer
GUI | Web and 11 more
View Profile
User photo
3220
Mendy Bennett
Experienced with Ad network & Ad servers.
Mobile | Ad Networks and 1 more
View Profile
User photo
3060
Karen Fitzgerald
7 years in Cross-Platform development.
Mobile | Cross Platform Frameworks
View Profile
Show All
X

Compare Products

Select up to three two products to compare by clicking on the compare icon () of each product.

{{compareToolModel.Error}}

Now comparing:

{{product.ProductName | createSubstring:25}} X
Compare Now