Youtube videos:

* [https://www.youtube.com/watch?v=kr0mpwqttM0&t=34s]
* [https://www.youtube.com/watch?v=swU3c34d2NQ]
* [https://www.youtube.com/watch?v=FsAPt_9Bf3U&t=35s]
* [https://www.youtube.com/watch?v=KlBPCzcQNU8]

First class functions

We can treat functions just like anyother object or variable.


In [2]:
def square(x):
    return x*x

def cube(x):
    return x*x*x

# This is custom-built map function which is going to behave like in-bulit map function.
def my_map(func, arg_list):
    result = []
    for i in arg_list:
        result.append(func(i))
    return result

squares = my_map(square, [1,2,3,4])

print(squares)

cubes = my_map(cube, [1,2,3,4])

print(cubes)


[1, 4, 9, 16]
[1, 8, 27, 64]

Closures

A closure closes over free variables from their environment.


In [3]:
def html_tag(tag):
    def wrap_text(msg):
        print('<{0}>{1}<{0}>'.format(tag, msg))
    return wrap_text

print_h1 = html_tag('h1')
print_h1('Test Headline')
print_h1('Another Headline')

print_p = html_tag('p')
print_p('Test Paragraph!')


<h1>Test Headline<h1>
<h1>Another Headline<h1>
<p>Test Paragraph!<p>

Decorators

Decorators are a way to dynamically alter the functionality of your functions. So for example, if you wanted to log information when a function is run, you could use a decorator to add this functionality without modifying the source code of your original function.


In [5]:
def decorator_function(original_function):
    def wrapper_function():
        print("wrapper executed this before {}".format(original_function.__name__))
        return original_function()
    return wrapper_function

def display():
    print("display function ran!")

decorated_display = decorator_function(display)
decorated_display()


wrapper executed this before display
display function ran!

In [6]:
# The above code is functionally the same as below:

def decorator_function(original_function):
    def wrapper_function():
        print("wrapper executed this before {}".format(original_function.__name__))
        return original_function()
    return wrapper_function

@decorator_function
def display():
    print("display function ran!")

display()


wrapper executed this before display
display function ran!

In [7]:
# Lets make our decorator function to work with functions with different number of arguments
# For this we use, *args (arguments) and **kwargs (keyword arguments). 
# args and kwargs are convention, you can use any other name you want like *myargs, **yourkeywordargs

def decorator_function(original_function):
    def wrapper_function(*args, **kwargs):
        print("wrapper executed this before {}".format(original_function.__name__))
        return original_function(*args, **kwargs)
    return wrapper_function

@decorator_function
def display():
    print("display function ran!")

@decorator_function
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display()
display_info('John', 25)


wrapper executed this before display
display function ran!
wrapper executed this before display_info
display_info ran with arguments (John, 25)

In [8]:
# Now let's use a class as a decorator instead of a function

class decorator_class(object):
    def __init__(self, original_function):
        self.original_function = original_function
    
    def __call__(self, *args, **kwargs): # This method is going to behave just like our wrapper function behaved
        print('call method executed this before {}'.format(self.original_function.__name__))
        return self.original_function(*args, **kwargs)

@decorator_class
def display():
    print("display function ran!")

@decorator_class
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display()
display_info('John', 25)


call method executed this before display
display function ran!
call method executed this before display_info
display_info ran with arguments (John, 25)

Some practical applications of decorators


In [9]:
#Let's say we want to keep track of how many times a specific function was run and what argument were passed to that function
def my_logger(orig_func):
    import logging
    logging.basicConfig(filename='{}.log'.format(orig_func.__name__), level=logging.INFO) #Generates a log file in current directory with the name of original funcion
    
    def wrapper(*args, **kwargs):
        logging.info(
            'Ran with args: {}, and kwargs: {}'.format(args, kwargs))
        return orig_func(*args, **kwargs)
    
    return wrapper

@my_logger
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display_info('John', 25) 
# Note: 
# Since display_info is decorated with my_logger, the above call is equivalent to:
# decorated_display = my_logger(display_info)
# decorated_display('John', 25)


display_info ran with arguments (John, 25)

In [10]:
def my_timer(orig_func):
    import time
    
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = orig_func(*args, **kwargs)
        t2 = time.time() - t1
        print('{} ran in: {} sec'.format(orig_func.__name__, t2))
        return result
    
    return wrapper

@my_timer
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display_info('John', 25)
# Note: 
# Since display_info is decorated with my_timer, the above call is equivalent to:
# decorated_display = my_timer(display_info)
# decorated_display('John', 25)
# (or simply put)
# my_timer(display_info('John', 25))


display_info ran with arguments (John, 25)
display_info ran in: 0.00019598007202148438 sec

Chaining of Decorators


In [11]:
@my_timer
@my_logger
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display_info('John', 25) # This is equivalent to my_timer(my_logger(display_info('John', 25)))
# The above code will give us some unexpected results.
# Instead of printing "display_info ran in: ---- sec" it prints "wrapper ran in: ---- sec"


display_info ran with arguments (John, 25)
wrapper ran in: 0.002919912338256836 sec

Let's see if switching the order of decorators helps


In [12]:
@my_logger
@my_timer
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display_info('John', 25) # This is equivalent to my_logger(my_timer(display_info('John', 25)))
# Now this would create wrapper.log instead of display_info.log like we expected.


display_info ran with arguments (John, 25)
display_info ran in: 0.0002319812774658203 sec

In [15]:
# To understand why wrapper.log is generated instead of display_info.log let's look at the following code.
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display_info = my_timer(display_info('John',25))
print(display_info.__name__)


display_info ran with arguments (John, 25)
wrapper

In [17]:
# So how do we solve this problem
# The answer is by using the wraps decorator
# For this we need to import wraps from functools module
from functools import wraps

def my_logger(orig_func):
    import logging
    logging.basicConfig(filename='{}.log'.format(orig_func.__name__), level=logging.INFO) #Generates a log file in current directory with the name of original funcion
    @wraps(orig_func)
    def wrapper(*args, **kwargs):
        logging.info(
            'Ran with args: {}, and kwargs: {}'.format(args, kwargs))
        return orig_func(*args, **kwargs)
    return wrapper

def my_timer(orig_func):
    import time
    @wraps(orig_func)
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = orig_func(*args, **kwargs)
        t2 = time.time() - t1
        print('{} ran in: {} sec'.format(orig_func.__name__, t2))
        return result
    return wrapper

@my_logger
@my_timer
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display_info('Hank', 22)


display_info
display_info ran with arguments (Hank, 22)
display_info ran in: 6.699562072753906e-05 sec

Decorators with Arguments


In [1]:
def prefix_decorator(prefix):
    def decorator_function(original_function):
        def wrapper_function(*args, **kwargs):
            print(prefix, "Executed before {}".format(original_function.__name__))
            result = original_function(*args, **kwargs)
            print(prefix, "Executed after {}".format(original_function.__name__), '\n')
            return result
        return wrapper_function
    return decorator_function

@prefix_decorator('LOG:')
def display_info(name, age):
    print('display_info ran with arguments ({}, {})'.format(name, age))

display_info('John', 25)


LOG: Executed before display_info
display_info ran with arguments (John, 25)
LOG: Executed after display_info