<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Introduction to Python and Natural Language Technologies
Lecture 5
Decorators and packaging
March 7, 2018
Let's create a greeter function
takes another function as a parameter
greets the caller before calling the function
Step1:  Functions are first class objects
they can be passed as arguments
they can be returned from other functions (example later)
Let's create a count_predicate function
takes a iterable and a predicate (yes-no function)
calls the predicate on each element
counts how many times it returns True
same as std
Step2:  Q. Can you write this function in fewer lines?
Step3:  The predicate parameter
it can be anything 'callable'
1. function
Step4:  2. instance of a class that implements __call__ (functor)
Step5:  3. lambda expression
Step6:  Functions can be nested
Step7:  the nested function is only accessible from the parent
Step8:  Functions can be return values
Step9:  Nested functions have access to the parent's scope
closure
Step10:  Function factory
Step11:  Wrapper function factory
let's create a function that takes a function return an almost identical function
the returned function adds some logging
Step12:  Wrapping a function
The function we are going to wrap
Step13:  now add some noise
Step14:  Bound the original reference to the wrapped function
i.e. greeter should refer to the wrapped function
we don't need the original function
Step15:  this turns out to be a frequent operation
Step16:  Decorator syntax
a decorator is a function
that takes a function as an argument
returns a wrapped version of the function
the decorator syntax is just syntactic sugar (shorthand) for
Step17:  Pie syntax
introduced in PEP318 in Python 2.4
various syntax proposals were suggested, summarized here
Problem 1. Function metadata is lost
Step19:  Solution 1. Copy manually
Step20:  What about other metadata such as the docstring?
Step22:  Solution 2. functools.wraps
Step23:  Problem 2. Function arguments
so far we have only decorated functions without parameters
to wrap arbitrary functions, we need to capture a variable number of arguments
remember args and kwargs
Step24:  the same mechanism can be used in decorators
Step25:  the decorator has only one parameter
Step26:  Decorators can take parameters too
they have to return a decorator without parameters - decorator factory
Step27:  Decorators can be implemented as classes
__call__ implements the wrapped function
Step28:  See also
Decorator overview with some advanced techniques
Step29:  Filter
filter creates a list of elements for which a function returns true
Step30:  Most comprehensions can be rewritten using map and filter
Step31:  Reduce
reduce applies a rolling computation on a sequence
the first argument of reduce is two-argument function
the second argument is the sequence
the result is accumulated in an accumulator
Step32:  an initial value for the accumulator may be supplied
Step33:  Modules and imports
import statement combines two operations
it searches for the named module, 
then it binds the results of that search to a name in the local scope -- official documentation (emphasis mine)
several formats
importing full modules
Step34:  importing submodules
Step35:  the as keyword binds the module to a different name
Step36:  importing more than one module/submodule
Step37:  importing functions or classes
Step38:  importing everything from a module
NOT recommended because we have no way of knowing where names come from
<ASSISTANT_TASK:>
Python Code:
def greeter(func):
    print("Hello")
    func()
    
def say_something():
    print("Let's learn some Python.")
    
greeter(say_something)
# greeter(12)
Explanation: Introduction to Python and Natural Language Technologies
Lecture 5
Decorators and packaging
March 7, 2018
Let's create a greeter function
takes another function as a parameter
greets the caller before calling the function
End of explanation
def count_predicate(predicate, iterable):
    true_count = 0
    for element in iterable:
        if predicate(element) is True:
            true_count += 1
    return true_count
Explanation: Functions are first class objects
they can be passed as arguments
they can be returned from other functions (example later)
Let's create a count_predicate function
takes a iterable and a predicate (yes-no function)
calls the predicate on each element
counts how many times it returns True
same as std::count in C++
End of explanation
def count_predicate(predicate, iterable):
    return sum(predicate(e) for e in iterable)
Explanation: Q. Can you write this function in fewer lines?
End of explanation
def is_even(number):
    return number % 2 == 0
numbers = [1, 3, 2, -5, 0, 0]
count_predicate(is_even, numbers)
Explanation: The predicate parameter
it can be anything 'callable'
1. function
End of explanation
class IsEven(object):
    def __call__(self, number):
        return number % 2 == 0
    
print(count_predicate(IsEven(), numbers))
IsEven()(123)
i = IsEven()
i(11)
Explanation: 2. instance of a class that implements __call__ (functor)
End of explanation
count_predicate(lambda x: x % 2 == 0, numbers)
Explanation: 3. lambda expression
End of explanation
def parent():
    print("I'm the parent function")
    
    def child():
        print("I'm the child function")
        
parent()
Explanation: Functions can be nested
End of explanation
def parent():
    print("I'm the parent function")
    
    def child():
        print("I'm the child function")
    
    print("Calling the nested function")
    child()
        
parent()
# parent.child  # raises AttributeError
Explanation: the nested function is only accessible from the parent
End of explanation
def parent():
    print("I'm the parent function")
    
    def child():
        print("I'm the child function")
        
    return child
child_func = parent()
print("Calling child")
child_func()
print("\nUsing parent's return value right away")
parent()()
Explanation: Functions can be return values
End of explanation
def parent(value):
    
    def child():
        print("I'm the nested function. "
              "The parent's value is {}".format(value))
        
    return child
        
child_func = parent(42)
print("Calling child_func")
child_func()
f1 = parent("abc")
f2 = parent(123)
f1()
f2()
f1 is f2
Explanation: Nested functions have access to the parent's scope
closure
End of explanation
def make_func(param):
    value = param
    
    def func():
        print("I'm the nested function. "
              "The parent's value is {}".format(value))
        
    return func
func_11 = make_func(11)
func_abc = make_func("abc")
func_11()
func_abc()
Explanation: Function factory
End of explanation
def add_noise(func):
    
    def wrapped_with_noise():
        print("Calling function {}".format(func.__name__))
        func()
        print("{} finished.".format(func.__name__))
        
    return wrapped_with_noise
Explanation: Wrapper function factory
let's create a function that takes a function return an almost identical function
the returned function adds some logging
End of explanation
def noiseless_function():
    print("This is not noise")
    
noiseless_function()
Explanation: Wrapping a function
The function we are going to wrap:
End of explanation
noisy_function = add_noise(noiseless_function)
noisy_function()
Explanation: now add some noise
End of explanation
def greeter():
    print("Hello")
    
print(id(greeter))
   
greeter = add_noise(greeter)
greeter()
print(id(greeter))
Explanation: Bound the original reference to the wrapped function
i.e. greeter should refer to the wrapped function
we don't need the original function
End of explanation
def friendly_greeter():
    print("Hello friend")
    
def rude_greeter():
    print("Hey you")
    
friendly_greeter = add_noise(friendly_greeter)
rude_greeter = add_noise(rude_greeter)
friendly_greeter()
rude_greeter()
Explanation: this turns out to be a frequent operation
End of explanation
@add_noise
def informal_greeter():
    print("Yo")
    
# informal_greeter = add_noise(informal_greeter)
    
informal_greeter()
Explanation: Decorator syntax
a decorator is a function
that takes a function as an argument
returns a wrapped version of the function
the decorator syntax is just syntactic sugar (shorthand) for:
python
func = decorator(func)
End of explanation
informal_greeter.__name__
Explanation: Pie syntax
introduced in PEP318 in Python 2.4
various syntax proposals were suggested, summarized here
Problem 1. Function metadata is lost
End of explanation
def add_noise(func):
    
    def wrapped_with_noise():
        print("Calling {}...".format(func.__name__))
        func()
        print("{} finished.".format(func.__name__))
        
    wrapped_with_noise.__name__ = func.__name__
    return wrapped_with_noise
@add_noise
def greeter():
    meaningful documentation
    print("Hello")
    
print(greeter.__name__)
Explanation: Solution 1. Copy manually
End of explanation
print(greeter.__doc__)
Explanation: What about other metadata such as the docstring?
End of explanation
from functools import wraps
def add_noise(func):
    
    @wraps(func)
    def wrapped_with_noise():
        print("Calling {}...".format(func.__name__))
        func()
        print("{} finished.".format(func.__name__))
        
    wrapped_with_noise.__name__ = func.__name__
    return wrapped_with_noise
@add_noise
def greeter():
    function that says hello
    print("Hello")
    
print(greeter.__name__)
print(greeter.__doc__)
Explanation: Solution 2. functools.wraps
End of explanation
def function_with_variable_arguments(*args, **kwargs):
    print(args)
    print(kwargs)
    
function_with_variable_arguments(1, "apple", tree="peach")
Explanation: Problem 2. Function arguments
so far we have only decorated functions without parameters
to wrap arbitrary functions, we need to capture a variable number of arguments
remember args and kwargs
End of explanation
def add_noise(func):
    
    @wraps(func)
    def wrapped_with_noise(*args, **kwargs):
        print("Calling {}...".format(func.__name__))
        func(*args, **kwargs)
        print("{} finished.".format(func.__name__))
        
    return wrapped_with_noise
Explanation: the same mechanism can be used in decorators
End of explanation
@add_noise
def personal_greeter(name):
    print("Hello {}".format(name))
    
personal_greeter("John")
Explanation: the decorator has only one parameter: func, the function to wrap
the returned function (wrapped_with_noise) takes arbitrary parameters: args, kwargs
it calls func, the decorator's argument with arbitrary parameters
End of explanation
def decorator_with_param(param1, param2=None):
    print("Creating a new decorator: {0}, {1}".format(
        param1, param2))
    def actual_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("Wrapper function {}".format(
                func.__name__))
            print("Params: {0}, {1}".format(param1, param2))
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator
@decorator_with_param(42, "abc")
def personal_greeter(name):
    print("Hello {}".format(name))
    
@decorator_with_param(4)
def personal_greeter2(name):
    print("Hello {}".format(name))
    
print("\nCalling personal_greeter")
personal_greeter("Mary")
def hello(name):
    print("Hello {}".format(name))
    
hello = decorator_with_param(1, 2)(hello)
hello("john")
Explanation: Decorators can take parameters too
they have to return a decorator without parameters - decorator factory
End of explanation
class MyDecorator(object):
    def __init__(self, func):
        self.func_to_wrap = func
        wraps(func)(self)
        
    def __call__(self, *args, **kwargs):
        print("before func {}".format(self.func_to_wrap.__name__))
        res = self.func_to_wrap(*args, **kwargs)
        print("after func {}".format(self.func_to_wrap.__name__))
        return res
    
@MyDecorator
def foo():
    print("bar")
foo()
Explanation: Decorators can be implemented as classes
__call__ implements the wrapped function
End of explanation
def double(e):
    return e * 2
l = [2, 3, "abc"]
list(map(double, l))
map(double, l)
list(map(lambda x: x * 2, [2, 3, "abc"]))
Explanation: See also
Decorator overview with some advanced techniques: https://www.youtube.com/watch?v=9oyr0mocZTg
A very deep dive into decorators: https://www.youtube.com/watch?v=7jGtDGxgwEY
Functional Python: map, filter and reduce
Python has 3 built-in functions that originate from functional programming.
Map
map applies a function on each element of a sequence
End of explanation
def is_even(n):
    return n % 2 == 0
l = [2, 3, -1, 0, 2]
list(filter(is_even, l))
list(filter(lambda x: x % 2 == 0, range(8)))
Explanation: Filter
filter creates a list of elements for which a function returns true
End of explanation
l = [2, 3, 0, -1, 2, 0, 1]
signum = [x / abs(x) if x != 0 else x for x in l]
print(signum)
list(map(lambda x: x / abs(x) if x != 0 else 0, l))
even = [x for x in l if x % 2 == 0]
print(even)
print(list(filter(lambda x: x % 2 == 0, l)))
Explanation: Most comprehensions can be rewritten using map and filter
End of explanation
from functools import reduce
l = [1, 2, -1, 4]
reduce(lambda x, y: x*y, l)
Explanation: Reduce
reduce applies a rolling computation on a sequence
the first argument of reduce is two-argument function
the second argument is the sequence
the result is accumulated in an accumulator
End of explanation
reduce(lambda x, y: x*y, l, 10)
reduce(lambda x, y: max(x, y), l)
reduce(max, l)
reduce(lambda x, y: x + int(y % 2 == 0) * y, l, 0)
Explanation: an initial value for the accumulator may be supplied
End of explanation
import sys
", ".join(dir(sys))
Explanation: Modules and imports
import statement combines two operations
it searches for the named module, 
then it binds the results of that search to a name in the local scope -- official documentation (emphasis mine)
several formats
importing full modules
End of explanation
from os import path
try:
    os
except NameError:
    print("os does not seem to be defined")
    
try:
    path
    print("path found")
except NameError:
    print("path does not seem to be defined")
Explanation: importing submodules
End of explanation
import os as os_module
try:
    os
except NameError:
    print("os does not seem to be defined")
    
try:
    os_module
    print("os_module found")
except NameError:
    print("os_module does not seem to be defined")
Explanation: the as keyword binds the module to a different name:
End of explanation
# import os, sys
from sys import stdin, stderr, stdout
Explanation: importing more than one module/submodule
End of explanation
from argparse import ArgumentParser
import inspect
inspect.isclass(ArgumentParser)
Explanation: importing functions or classes
End of explanation
from os import *
try:
    makedirs
    stat
    print("everything found")
except NameError:
    print("Something not found")
Explanation: importing everything from a module
NOT recommended because we have no way of knowing where names come from
End of explanation
<END_TASK>