<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Youtube videos
Step1:  Closures
A closure closes over free variables from their environment.
Step2:  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.
Step3:  Some practical applications of decorators
Step4:  Chaining of Decorators
Step5:  Let's see if switching the order of decorators helps
Step6:  Decorators with Arguments
<ASSISTANT_TASK:>
Python Code:
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)
Explanation: 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.
End of explanation
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!')
Explanation: Closures
A closure closes over free variables from their environment.
End of explanation
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()
# 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()
# 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)
# 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)
Explanation: 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.
End of explanation
#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)
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))
Explanation: Some practical applications of decorators
End of explanation
@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"
Explanation: Chaining of Decorators
End of explanation
@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.
# 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__)
# 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)
Explanation: Let's see if switching the order of decorators helps
End of explanation
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)
Explanation: Decorators with Arguments
End of explanation
<END_TASK>