Functions which modify the functionality of other functions.
Everything in Python is an object
First of all let’s understand functions in Python:
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!del hiprint(hi())#outputs: NameErrorprint(greet())#outputs: 'hi yasoob'
Defining functions within functions
In Python we can define functions inside other functions, ie. nested 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
Returning functions from within functions
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"if name =="yasoob":return greetelse:return welcomea =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
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.
Giving a function as an argument to another function
defhi():return"hi yasoob!"defdoSomethingBeforeHi(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!
Decorators let you execute code before and after a function.
Writing our first decorator
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()")return wrapTheFunctiondefa_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()
Decorators wrap a function and modify its behavior in one way or the another.
The @ syntax 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)
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:
from functools import wrapsdefa_new_decorator(a_func):@wraps(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()")return wrapTheFunction@a_new_decoratordefa_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
Blueprint:
from functools import wrapsdefdecorator_name(f):@wraps(f)defdecorated(*args,**kwargs):# Here goes the extra behaviorifnot can_run:return"Function will not run"returnf(*args, **kwargs)return decorated@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.
Use cases
Authorization
Check whether someone is authorized to use an endpoint in a web applicatio
from functools import wrapsdeflogit(func):@wraps(func)defwith_logging(*args,**kwargs):print(func.__name__+" was called")returnfunc(*args, **kwargs)return with_logging@logitdefaddition_func(x):"""Do some math."""return x + xresult =addition_func(4)# Output: addition_func was called
Decorators with Arguments
@wraps is also a decorator. But, it takes an argument like any normal function can do.
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! We can write a function that returns a wrapper function.
Nesting a Decorator Within a Function
Create a wrapper which lets us specify a logfile to output to:
from functools import wrapsdeflogit(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')as opened_file:# Now we log to the specified logfile opened_file.write(log_string +'\n')return wrapped_functionreturn logging_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
Decorator Classes
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 inheritance, but so far we’ve only seen functions being used to build decorators.
Classes can also be used to build decorators.
Rebuild logit as a class instead of a function.
classlogit(object): _logfile ='out.log'def__init__(self,func): self.func = funcdef__call__(self,*args): log_string = self.func.__name__+" was called"print(log_string)# Open the logfile and appendwithopen(self._logfile, 'a')as opened_file:# Now we log to the specified logfile opened_file.write(log_string +'\n')# Now, send a notification self.notify()# return base funcreturn self.func(*args)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._logfile ='out2.log'# if change log file@logitdefmyfunc1():passmyfunc1()# Output: myfunc1 was called
Now, let’s subclass logit to add email functionality:
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.