Hi! I am currently looking for some freelance projects. If you have a project idea in mind please feel free to send me an email or check out my personal website. If you want to get to know about my journey, you can read this article: How I got into programming

Decorators are a significant part of Python. In simple words: they are
functions which modify the functionality of another function. They help
to make our code shorter and more Pythonic. Most of the beginners do not
know where to use them so I am going to share some areas where
decorators can make your code more concise.

First, let’s discuss how to write your own decorator.

It is perhaps one of the most difficult concepts to grasp. We will take
it one step at a time so that you can fully understand it.

defhi(name="yasoob"):return"hi "+nameprint(hi())# output: 'hi yasoob'# We can even assign a function to a variable likegreet=hi# We are not using parentheses here because we are not calling the function hi# instead we are just putting it into the greet variable. Let's try to run thisprint(greet())# output: 'hi yasoob'# Let's see what happens if we delete the old hi function!delhiprint(hi())#outputs: NameErrorprint(greet())#outputs: 'hi yasoob'

So those are the basics when it comes to functions. Let’s take your
knowledge one step further. In Python we can define functions inside
other functions:

defhi(name="yasoob"):print("now you are inside the hi() function")defgreet():return"now you are in the greet() function"defwelcome():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# This shows that whenever you call hi(), greet() and welcome()# are also called. However the greet() and welcome() functions# are not available outside the hi() function e.g:greet()#outputs: NameError: name 'greet' is not defined

So now we know that we can define functions in other functions. In
other words: we can make nested functions. Now you need to learn one
more thing, that functions can return functions too.

It is not necessary to execute a function within another function, we
can return it as an output as well:

defhi(name="yasoob"):defgreet():return"now you are in the greet() function"defwelcome():return"now you are in the welcome() function"ifname=="yasoob":returngreetelse:returnwelcomea=hi()print(a)#outputs: <function greet at 0x7f2143c01500>#This clearly shows that `a` now points to the greet() function in hi()#Now try thisprint(a())#outputs: now you are in the greet() function

Just take a look at the code again. In the if/else clause we are
returning 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; whereas if you don’t put parenthesis after it,
then it can be passed around and can be assigned to other variables
without executing it. Did you get it? Let me explain it in a little bit
more detail. When we write a=hi(), hi() gets executed and
because the name is yasoob by default, the function greet is returned.
If we change the statement to a=hi(name="ali") then the welcome
function will be returned. We can also do print hi()() which outputs
now you are in the greet() function.

In the last example we actually made a decorator! Let’s modify the
previous decorator and make a little bit more usable program:

defa_new_decorator(a_func):defwrapTheFunction():print("I am doing some boring work before executing a_func()")a_func()print("I am doing some boring work after executing a_func()")returnwrapTheFunctiondefa_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()

Did you get it? We just applied the previously learned principles. This
is exactly what the decorators do in Python! They wrap a function and
modify its behaviour in one way or the another. Now you might be
wondering that we did not use the @ anywhere in our code? That is just a
short way of making up a decorated function. Here is how we could have
run the previous code sample using @.

@a_new_decoratordefa_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)

I hope you now have a basic understanding of how decorators work in
Python. Now there is one problem with our code. If we run:

That’s not what we expected! Its name is
“a_function_requiring_decoration”. Well our function was replaced by
wrapTheFunction. It overrode the name and docstring of our function.
Luckily Python provides us a simple function to solve this problem and
that is functools.wraps. Let’s modify our previous example to use
functools.wraps:

Now that is much better. Let’s move on and learn some use-cases of
decorators.

Blueprint:

fromfunctoolsimportwrapsdefdecorator_name(f):@wraps(f)defdecorated(*args,**kwargs):ifnotcan_run:return"Function will not run"returnf(*args,**kwargs)returndecorated@decorator_namedeffunc():return("Function is running")can_run=Trueprint(func())# Output: Function is runningcan_run=Falseprint(func())# Output: Function will not run

Note: @wraps takes a function to be decorated and adds the
functionality of copying over the function name, docstring, arguments
list, etc. This allows to access the pre-decorated function’s properties
in the decorator.

Decorators can help to check whether someone is authorized to use an
endpoint in a web application. They are extensively used in Flask web
framework and Django. Here is an example to employ decorator based
authentication:

Logging is another area where the decorators shine. Here is an example:

fromfunctoolsimportwrapsdeflogit(func):@wraps(func)defwith_logging(*args,**kwargs):print(func.__name__+" was called")returnfunc(*args,**kwargs)returnwith_logging@logitdefaddition_func(x):"""Do some math."""returnx+xresult=addition_func(4)# Output: addition_func was called

I am sure you are already thinking about some clever uses of decorators.

Come to think of it, isn’t @wraps also a decorator? But, it takes an
argument like any normal function can do. So, why can’t we do that too?

This is because when you use the @my_decorator syntax, you are
applying a wrapper function with a single function as a parameter
Remember, everything in Python is an object, and this includes
functions! With that in mind, we can write a function that returns
a wrapper function.

Let’s go back to our logging example, and create a wrapper which lets
us specify a logfile to output to.

fromfunctoolsimportwrapsdeflogit(logfile='out.log'):deflogging_decorator(func):@wraps(func)defwrapped_function(*args,**kwargs):log_string=func.__name__+" was called"print(log_string)# Open the logfile and appendwithopen(logfile,'a')asopened_file:# Now we log to the specified logfileopened_file.write(log_string+'\n')returnwrapped_functionreturnlogging_decorator@logit()defmyfunc1():passmyfunc1()# Output: myfunc1 was called# A file called out.log now exists, with the above string@logit(logfile='func2.log')defmyfunc2():passmyfunc2()# Output: myfunc2 was called# A file called func2.log now exists, with the above string

Now we have our logit decorator in production, but when some parts
of our application are considered critical, failure might be
something that needs more immediate attention. Let’s say sometimes
you want to just log to a file. Other times you want an email sent,
so the problem is brought to your attention, and still keep a log
for your own records. This is a case for using inheritence, but
so far we’ve only seen functions being used to build decorators.

Luckily, classes can also be used to build decorators. So, let’s
rebuild logit as a class instead of a function.

classlogit(object):def__init__(self,logfile='out.log'):self.logfile=logfiledef__call__(self,func):log_string=func.__name__+" was called"print(log_string)# Open the logfile and appendwithopen(self.logfile,'a')asopened_file:# Now we log to the specified logfileopened_file.write(log_string+'\n')# Now, send a notificationself.notify()defnotify(self):# logit only logs, no morepass

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

@logit()defmyfunc1():pass

Now, let’s subclass logit to add email functionality (though this topic
will not be covered here).

classemail_logit(logit):''' A logit implementation for sending emails to admins when the function is called. '''def__init__(self,email='admin@myproject.com',*args,**kwargs):self.email=emailsuper(email_logit,self).__init__(*args,**kwargs)defnotify(self):# Send an email to self.email# Will not be implemented herepass

From here, @email_logit works just like @logit but sends an email
to the admin in addition to logging.