#!/usr/bin/env python # coding: utf-8 # # 3.4 Advanced Topics on Functions # # ## Topics # - handle variable length arguments # - lambda expressions # - higher-order functions # - nested functions # - functions as returned values # - currying # - function decorators # ## 3.4.1 Variable length arguments # - when not sure how many arguments will be passed to a function (e.g., print()) # - *args (non-keyworded variable length arguments) # - *kwargs (keyworded variable length arguments) # - e.g., built-in print function uses variable length arguments # # ### print(*object, sep=' ', end='\n', file=sys.stdout, flush=False) # In[1]: # variable length arguments demo def someFunction(a, b, c, *varargs, **kwargs): print('a = ', a) print('b = ', b) print('c = ', c) print('*args = ', varargs) print('type of args = ', type(varargs)) print('**kwargs = ', kwargs) print('type of kwargs = ', type(kwargs)) # In[2]: # call someFunction with some arguments someFunction(1, 'Apple', 4.5, 5, [2.5, 'b'], fname='Jake', num=1) # ## 3.4.2 Lambda Functions/Expressions # - anonymous functions (no name) # - typically used in conjunction with higher order functions such as: map(), reduce(), filter() # - Reference: http://www.secnetix.de/olli/Python/lambda_functions.hawk # ### lambda function properties and usage # - single line simple functions # - no explicit return keyword is used # - always contains an expression that is implictly returned # - can use a lambda definition anywere a function is expected without assigning to a variable # - syntax: **lambda argument(s): expression** # - see Ch08-2 Lists Advanced chapter for lambda applications on some higher order built-in functions # ### difference between lambda and regular function # In[5]: # regular function def func(x): return x**2 # In[6]: print(func(4)) # In[7]: g = lambda x: x**2 # no name, no parenthesis, and no return keyword # a function that takes x and returns x**2 # In[9]: print(g) # In[10]: g(4) # ## 3.4.3 Higher-order functions # https://composingprograms.com/pages/16-higher-order-functions.html # - functions that manipulate other functions are called higher order functions # - functions take function(s) as argument(s) # - typically lambda expressions are passed # - functions can return a function # In[30]: # computer summations of n natural numbers # func is a function applied to all the natural numbers between 1 and n inclusive def sum_naturals(func, n): total, k = 0, 1 while k <= n: total += func(k) k += 1 return total # In[31]: n = 100 print(f'sum of first {n} natural numbers = {sum_naturals(lambda x: x, n)}') # In[32]: # of course you can pass regular function def even(n): return n if n%2 == 0 else 0 # In[33]: print(f'sum of even numbers from 1 to {n} = {sum_naturals(even, n)}') # In[49]: # sum of odd numbers from 1 to 100 print(f'sum of odd numbers from 1 to {n} = {sum_naturals(lambda x: x if x%2==1 else 0, n)}') # ## 3.4.4 Nested definitions # - functions can be defined inside a function with local scope # - locally defined functions also have access to the names in which they are defined # - this technique is called lexical scoping # - helps keep the global frame clean and less cluter with functions that are only used inside some functions # - let's redefine sum_natural function again with local functions # # ### Visualize using [PythonTutor.com](http://pythontutor.com/visualize.html#code=def%20sum_naturals1%28n,%20number_type%3D%22all%22%29%3A%0A%20%20%20%20def%20even%28x%29%3A%0A%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%200%20else%200%0A%20%20%20%20%0A%20%20%20%20def%20odd%28x%29%3A%0A%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%201%20else%200%0A%20%20%20%20%0A%20%20%20%20def%20func%28x%29%3A%0A%20%20%20%20%20%20%20%20%23%20local%20function%20has%20access%20to%20global%20variables%20as%20well%20as%20parent%20frames%0A%20%20%20%20%20%20%20%20if%20number_type%20%3D%3D%20'even'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%200%20else%200%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%201%20else%200%0A%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20total,%20k%20%3D%200,%201%0A%20%20%20%20while%20k%20%3C%3D%20n%3A%0A%20%20%20%20%20%20%20%20if%20number_type%20!%3D%20'all'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20total%20%2B%3D%20func%28k%29%0A%20%20%20%20%20%20%20%20%23elif%20number_type%20%3D%3D%20'odd'%3A%0A%20%20%20%20%20%20%20%20%23%20%20%20%20total%20%2B%3D%20odd%28k%29%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20total%20%2B%3D%20k%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20return%20total%0A%20%20%20%20%0An%20%3D%2010%20%20%0Aprint%28f'sum%20of%20even%20numbers%20from%201%20to%20%7Bn%7D%20%3D%20%7Bsum_naturals1%28n,%20%22even%22%29%7D'%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) # In[102]: # compute summations of n natural numbers # by default sum_natural1 finds sum of all the natural numbers between 1 and n inclusive def sum_naturals1(n, number_type="all"): def even(x): return x if x%2 == 0 else 0 def odd(x): return x if x%2 == 1 else 0 def func(x): # local function has access to global variables as well as parent frames if number_type == 'even': return x if x%2 == 0 else 0 else: return x if x%2 == 1 else 0 total, k = 0, 1 while k <= n: if number_type != 'all': total += func(k) #elif number_type == 'odd': # total += odd(k) else: total += k k += 1 return total # In[45]: n = 100 print(f'sum of first {n} natural numbers = {sum_naturals1(n)}') # In[46]: print(f'sum of even numbers from 1 to {n} = {sum_naturals1(n, "even")}') # In[48]: # sum of odd numbers from 1 to 100 print(f'sum of odd numbers from 1 to {n} = {sum_naturals1(n, "odd")}') # ## 3.4.5 Functions as returned values # - functions can return functions # - locally defined functions maintain their parent environment when they are returned # In[52]: def number_type(ntype='all'): def even(x): return x if x%2 == 0 else 0 def odd(x): return x if x%2 == 1 else 0 def _(x): # function to return x as it is; any() return x if ntype == 'all': return _ elif ntype == 'even': return even else: return odd # In[55]: n = 100 print(f'sum of first {n} natural numbers = {sum_naturals(number_type("all"), n)}') # In[58]: print(f'sum of even numbers from 1 to {n} = {sum_naturals(number_type("even"), n)}') # In[59]: # sum of odd numbers from 1 to 100 print(f'sum of odd numbers from 1 to {n} = {sum_naturals(number_type("odd"), n)}') # ## 3.4.6 Currying # - functions that take multiple arguments can be converted into a chain of functions that each take a single argument using higher-order function # - e.g., given a function **f(x, y)**, we can define a function **g(x)(y)** equivalent to **f(x, y)** # - **g** is a higher-order function that takes in a single argument **x** and returns another function that takes in a single argument **y** # - this transformation is called **currying** # In[86]: def curried_pow(x): def g(y): return pow(x, y) return g # In[87]: # same as 2**3 curried_pow(2)(3) # In[88]: # let's create a list of integers and map each to a different value nums = list(range(1, 11)) # In[89]: nums # In[90]: def my_map(alist, func): for i in range(len(alist)): alist[i] = func(alist[i]) # In[91]: my_map(nums, curried_pow(2)) # In[92]: nums # ## 3.4.7 Function Decorators # - https://realpython.com/primer-on-python-decorators/ # - decorators are higher order functions # - decorators take another function and extends the behavior of the latter function without explictly modifying it # - if the func being decorated takes arguments, provide arguments to wrapper # - if the func being decorated returns a value call it with return statement # - many frameworks such as Flask, Django provide lots of decorators # - e.g. @login_required; @app.route("/route_name"), etc. # In[118]: # a simple decorator example # my_decorator decorates func def my_decorator(func): def wrapper(): print("Before the function is called...") # call the original function func() print("After the function is called.") return wrapper def say_hello(): print("Hello there!") # In[119]: # say_hello is decorated now, without modifying the original function # just the behavior is modified by added extra print() before and after say_hello say_hello = my_decorator(say_hello) # In[120]: say_hello() # In[100]: # Python provides better syntax! # use @decorting_function @my_decorator def say_hi(): print("Hi there!") # In[101]: say_hi() # In[107]: # a simple count down function def countDown(from_number): if from_number <= 0: print('Blast off!') else: print(from_number) countDown(from_number-1) # In[112]: # Doesn't slow down the countdown! countDown(10) # In[109]: # let's write a slow_down wrapper import time def slow_down(func): """Sleep 1 second before calling the function""" def wrapper_slow_down(*args, **kwargs): time.sleep(1) # sleep for a second return func(*args, **kwargs) # call and return the result from the func return wrapper_slow_down # In[113]: countDownSlow = slow_down(countDown) # In[114]: countDownSlow(10) # In[ ]: