Python is a great programming language , you can write procedural, functional and object oriented code and develop almost anything. While writing code and using some infrastructures, you sometimes need to extend code without touching the original. The object oriented solution for that is inheritance but what if you are writing a procedural code? the answer is function decorator.
Function is an object
One important thing to note about python is that function is an object – you can create functions on the fly, send function to another function and return function from another function. Using the lambda expressions it is also easy to write.
For example, creating a function using lambda expression and send it to another function:
def do_something(fn): print("=======") fn() print("=======") f1 = lambda : print( "simple function" ); do_something( f1 )
output:
======= simple function =======
Inner functions
One way to understand that a function is an object is to declare an inner functions and return a function dynamically.
For example, we want to create a fibonacci series, we can do that iteratively or recursively and we want to choose on runtime the method. We can also use an inner function without expose it outside (checkn in this example)
def getfib(i): def checkn( n ): return n == 0 or n == 1 def iterfib( n ): if checkn( n ): return n a, b = 0, 1 for item in range(0, n-1): b = a + b a = b - a return b def recfib( n ): if checkn( n ): return n return recfib( n-1 ) + recfib( n-2 ) if i == 0: return iterfib return recfib fib = getfib( 1 ) print( fib(8) ) # 21
In this example we can use getfib with parameter to decide which implementation we want.
Closure
We can return an inner function dynamically and based on the parameters we got on the outer function. For example:
def getmulby( m ): def op( n ): return m * n return op f1=getmulby(10) f2=getmulby(5) print( f1(2) ) # 20 print( f2(2) ) # 10
This is useful to generate a function on the fly. For example if we have set of points (x,y) and we want to use interpolation to calculate y values for points we don’t have:
from scipy.interpolate import interp1d from pylab import plot, axis, legend, scatter import numpy as np x = np.array( [10,13,16,22,28,30,32,35,39,44] ) y = np.array( [90,120,170,210,290,300,330,370,390,410] ) scatter(x, y)
Now we can use interp1d function to generate a function for us :
interfn = interp1d(x, y, kind = 3) # returns a function # use the function with any value in the range yy = interfn(25) print(yy) # 253.42
Decorator
Decorator is a design pattern – function that takes another function as a parameter and extends its behavior without modifying it. This is another good example of closure
def add_stars( some_function ): def wrapper(): print("********************") some_function() print("********************") return wrapper def my_function(): print( "Hello!!!" ) my_function = add_stars( my_function ) my_function() # ******************** # Hello!!! # ********************
Using the @ symbol
You can use the @ symbol as a syntactic sugar to create a decorator:
just remove the function assignment and add the decorator using the @ syntax
def add_stars( some_function ): def wrapper(): print("********************") some_function() print("********************") return wrapper @add_stars def my_function(): print( "Hello!!!" ) my_function() # ******************** # Hello!!! # ********************
You can use any number of decorators you want and even use one more than once:
def add_header( some_function ): def wrapper(): print("====================") some_function() print("====================") return wrapper def add_stars( some_function ): def wrapper(): print("********************") some_function() print("********************") return wrapper @add_header @add_stars @add_header def my_function(): print("Hello!!!") my_function() # ==================== # ******************** # ==================== # Hello!!! # ==================== # ******************** # ====================
Nested Decorators
We can call one decorator from another like any other function. For example:
def add_header( some_function ): def wrapper(dec): print("=======HEADER=======") return some_function(dec) return wrapper def add_stars( some_function ): def wrapper(): print("********************") some_function() print("********************") return wrapper def my_function(): print("Hello!!!") # add_header takes and returns a decorator function add_stars=add_header(add_stars) my_function = add_stars(my_function) my_function() # Output: # =======HEADER======= # ******************** # Hello!!! # ********************
And with the @ syntax :
def add_header(some_function): def wrapper(dec): print("=======HEADER=======") return some_function(dec) return wrapper @add_header def add_stars(some_function): def wrapper(): print("********************") some_function() print("********************") return wrapper @add_stars def my_function(): print("Hello!!!") my_function()
Decorator With Parameter
While developing frameworks and infrastructures it is very useful so provide features with decorators, for example in Django web framework, we can validate that the user is logged and if not, redirect it to the login page :
@login_required(login_url = '/accounts/login/') def my_view(request):
Working with decorators is a good pattern when you want to write a function and add functionality that is not related to the implementation like the above example – a view return something only for logged in user
Because a decorator is a function, it can take any parameters you want , so for example, if we want to control the number of header lines in the above example its is easy:
def add_stars(some_function, lines): def wrapper(): for i in range(lines): print("********************") some_function() for i in range(lines): print("********************") return wrapper def my_function(): print("Hello!!!") my_function = add_stars(my_function, 3) my_function() # output: # ******************** # ******************** # ******************** # Hello!!! # ******************** # ******************** # ********************
But what about the @ syntax ?
@add_stars(3) def my_function(): print("Hello!!!")
If we try to send the decorator parameter in the “pie” syntax we will get an error:
... TypeError: add_stars() missing 1 required positional argument: 'lines'
The problem is that @add_stars(3) is not converted to add_stars(my_function,3). The parameter is ignored
To solve this, we need to do something tricky
We need to convert the parameters to optional arguments and named arguments – to do that we add another helper function:
def add_params(dec): def wrapper(*args, **kwargs): def fn(f): return dec(f, *args, **kwargs) return fn return wrapper
The function is actually a decorator that returns another decorator with any arguments you want , the returned decorator return a function that will call the original decorator with the optional arguments.
Now we can use that general purpose decorator function to send any parameter we want:
def add_params(dec): def wrapper(*args, **kwargs): def fn(f): return dec(f, *args, **kwargs) return fn return wrapper @add_params def add_stars(some_function, lines, msg, footer): def wrapper(): print(msg) for i in range(lines): print("********************") some_function() if footer: for i in range(lines): print("********************") return wrapper @add_stars(2, 'Good day', footer = False) def my_function(): print("Hello!!!") my_function() # Output: # Good day # ******************** # ******************** # Hello!!!
1 thought on “Python Closure and Function Decorators”
Comments are closed.
[…] lets write a function to generate linear regression model based on 2 vectors use a closure to generate a function […]