Decorators are an important part of Python. In simple terms: they are functions that modify the functionality of other functions. They help make our code shorter and more Pythonic. Most beginners don't know where to use them, so I'm going to share some areas where decorators can make your code cleaner. First, let's discuss how to write your own decorators.

This is probably one of the hardest concepts to master. We will discuss one step at a time so that you can fully understand it.

Everything is an object

First, let's understand functions in Python:

def hi(name="yasoob"): return "hi " + name print(hi()) # output: 'hi yasoob' # We can even assign a function to a variable, for example greet = hi # We are not using parentheses here, because we are not calling the hi function # but rather putting it into the greet variable. Let's try running this print(greet()) # output: 'hi yasoob' # If we delete the old hi function, see what happens! del hi print(hi()) #outputs: NameError print(greet()) #outputs: 'hi yasoob'

Define functions inside functions

Those were the basics of functions. Let's take your knowledge a step further. In Python, we can define one function inside another function:

def hi(name="yasoob"): print("now you are inside the hi() function") def greet(): return "now you are in the greet() function" def welcome(): return "now you are in the welcome() function" print(greet()) print(welcome()) print("now you are back in the hi() function") hi() #output:now you are inside the hi() function # now you are in the greet() function # now you are in the welcome() function # now you are back in the hi() function # The above shows that whenever you call hi(), greet() and welcome() will be called at the same time. # Then greet() and welcome() functions are not accessible outside the hi() function, for example: greet() #outputs: NameError: name 'greet' is not defined

Now we know that we can define other functions inside a function. That is to say: we can create nested functions. Now you need to learn a bit more: functions can also return functions.

Returning functions from functions

Actually, there's no need to execute another function within a function; we can also return it as output:

def hi(name="yasoob"): def greet(): return "now you are in the greet() function" def welcome(): return "now you are in the welcome() function" if name == "yasoob": return greet else: return welcome a = hi() print(a) #outputs: <function greet at 0x7f2143c01500> # The above clearly shows that `a` now points to the greet() function inside the hi() function # Now try this print(a()) #outputs: now you are in the greet() function

Look at this code again. In the if/else statement we return greet and welcome, not greet() and welcome(). Why is that? It's because when you put a pair of parentheses after it, the function gets executed; however, if you don't put parentheses after it, it can be passed around and assigned to other variables without executing it. Do you understand? Let me explain a bit more in detail.

  • When we writea = hi(),hi()it gets executed, and since the name parameter defaults to yasoob, the functiongreet()is returned.

  • We can also print outhi()(), which will output:now you are in the greet() function。

  • If we change the statement toa = hi(name = "ali"), thenwelcome()the function will be returned.

Passing a function as an argument to another function

def hi(): return "hi yasoob!" def doSomethingBeforeHi(func): print("I am doing some boring work before executing hi()") print(func()) doSomethingBeforeHi(hi) #outputs:I am doing some boring work before executing hi() # hi yasoob!

Now you have all the necessary knowledge to learn what decorators really are. Decorators let you execute code before and after a function.

Your first decorator

In the previous example, we actually already created a decorator! Now let's modify the previous decorator and write a slightly more useful program:

def a_new_decorator(a_func): def wrapTheFunction(): print("I am doing some boring work before executing a_func()") a_func() print("I am doing some boring work after executing a_func()") return wrapTheFunction def a_function_requiring_decoration(): print("I am the function which needs some decoration to remove my foul smell") a_function_requiring_decoration() #outputs: "I am the function which needs some decoration to remove my foul smell" a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration) #now a_function_requiring_decoration is wrapped by wrapTheFunction() a_function_requiring_decoration() #outputs:I am doing some boring work before executing a_func() # I am the function which needs some decoration to remove my foul smell # I am doing some boring work after executing a_func()

Do you see it? We just applied the principles we learned earlier. This is exactly what decorators do in Python! They wrap a function and modify its behavior in one way or another. Now you might be wondering, in our code we didn't use the@symbol? That is just a shorthand way to generate a decorated function. Here is how we use@to run the previous code:

@a_new_decorator def a_function_requiring_decoration(): """Hey you! Decorate me!""" print("I am the function which needs some decoration to " "remove my foul smell") a_function_requiring_decoration() #outputs: I am doing some boring work before executing a_func() # I am the function which needs some decoration to remove my foul smell # I am doing some boring work after executing a_func() #the @a_new_decorator is just a short way of saying: a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

Hopefully you now have a basic understanding of how Python decorators work. There is a problem if we run the following code:

print(a_function_requiring_decoration.__name__)
# Output: wrapTheFunction

This is not what we wanted! The output should be "a_function_requiring_decoration". Here the function was replaced by warpTheFunction. It overwrote our function's name and docstring. Fortunately, Python provides us with a simple function to solve this problem, namely functools.wraps. Let's modify the previous example to use functools.wraps:

from functools import wraps def a_new_decorator(a_func): @wraps(a_func) def wrapTheFunction(): print("I am doing some boring work before executing a_func()") a_func() print("I am doing some boring work after executing a_func()") return wrapTheFunction @a_new_decorator def a_function_requiring_decoration(): """Hey yo! Decorate me!""" print("I am the function which needs some decoration to " "remove my foul smell") print(a_function_requiring_decoration.__name__) # Output: a_function_requiring_decoration

That's much better now. Next, let's learn some common use cases of decorators.

Blueprint specification:

from functools import wraps def decorator_name(f): @wraps(f) def decorated(*args, **kwargs): if not can_run: return "Function will not run" return f(*args, **kwargs) return decorated @decorator_name def func(): return("Function is running") can_run = True print(func()) # Output: Function is running can_run = False print(func()) # Output: Function will not run

Note:@wrapsIt accepts a function to decorate, and adds functionality to copy the function name, docstring, argument list, and so on. This allows us to access the attributes of the function before decoration inside the decorator.


Use cases

Now let's look at where decorators really shine, and how using them makes managing some things easier.

Authorization

Decorators can help check whether someone is authorized to use an endpoint of a web application. They are heavily used in Flask and Django web frameworks. Here is an example of using decorator-based authorization:

from functools import wraps def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): authenticate() return f(*args, **kwargs) return decorated

Logging

Logging is another highlight of decorator usage. Here is an example:

from functools import wraps def logit(func): @wraps(func) def with_logging(*args, **kwargs): print(func.__name__ + " was called") return func(*args, **kwargs) return with_logging @logit def addition_func(x): """Do some math.""" return x + x result = addition_func(4) # Output: addition_func was called

I'm sure you're already thinking of another clever use of decorators.


Decorators with parameters

Think about this: isn't @wraps also a decorator? But it takes a parameter, just like any ordinary function can. So why don't we do that too? That's because when you use the @my_decorator syntax, you are applying a wrapper function that takes a single function as an argument. Remember, everything in Python is an object, and that includes functions! With that in mind, we can write a function that returns a wrapper function.

Embedding decorators in functions

Let's go back to the logging example and create a wrapper function that allows us to specify a log file for output.

from functools import wraps def logit(logfile='out.log'): def logging_decorator(func): @wraps(func) def wrapped_function(*args, **kwargs): log_string = func.__name__ + " was called" print(log_string) # Open the logfile and write content with open(logfile, 'a') as opened_file: # Now log to the specified logfile opened_file.write(log_string + '\n') return func(*args, **kwargs) return wrapped_function return logging_decorator @logit() def myfunc1(): pass myfunc1() # Output: myfunc1 was called # Now a file called out.log appears, and its content is the string above @logit(logfile='func2.log') def myfunc2(): pass myfunc2() # Output: myfunc2 was called # Now a file called func2.log appears, and its content is the string above

Decorator classes

Now we have a logit decorator suitable for production, but when certain parts of our application are still fragile, exceptions may require more urgent attention. For example, sometimes you just want to log to a file. Other times, you want to send a problem that catches your attention to an email while also keeping a log for the record. This is a scenario for using inheritance, but so far we have only seen functions used to build decorators.

Fortunately, classes can also be used to build decorators. So now let's rebuild logit using a class rather than a function.

from functools import wraps class logit(object): def __init__(self, logfile='out.log'): self.logfile = logfile def __call__(self, func): @wraps(func) def wrapped_function(*args, **kwargs): log_string = func.__name__ + " was called" print(log_string) # Open the logfile and write with open(self.logfile, 'a') as opened_file: # Now log to the specified file opened_file.write(log_string + '\n') # Now, send a notification self.notify() return func(*args, **kwargs) return wrapped_function def notify(self): # logit only logs, does nothing else pass

This implementation has the additional advantage of being cleaner than the nested function approach, and wrapping a function still uses the same syntax as before:

@logit()
def myfunc1():
    pass

Now, let's create a subclass of logit to add email functionality (although the email topic won't be expanded here).

class email_logit(logit): '''A logit implementation version that can send an email to the administrator when the function is called''' def __init__(self, email='[email protected]', *args, **kwargs): self.email = email super(email_logit, self).__init__(*args, **kwargs) def notify(self): # Send an email to self.email # We won't implement this here pass

From now on, @email_logit will have the same effect as @logit, but in addition to logging, it will also send an email to the administrator.

Original article address: https://eastlakeside.gitbooks.io/interpy-zh/content/decorators/