import math ''' This is an example of a 2 level closure that checks if we are passing the right type of parameters to a function. We use this closure to decorate other functions, so that we can check for correct parameter types The accpets function takes a variable size list of argument types, and returns a pointer to the inner function, argCheck The argCheck function tskes the function being checked as a parameter and returns a pointer tp its inner function, newFunc The newFunc function takes the original parameters to the original function as a variabl;e size parameter list. We go through the list and make sure the parameters are the right types. ''' def accepts(*argTypes): def argCheck(func): def newFunc(*args): for arg, aType in zip(args, argTypes): if type(arg) != aType: print (arg, "is not a ", aType) break #Python lets loops have an else #The else is executed if the loop exited naturally #and did not break out else: func(*args) return newFunc return argCheck ''' This function accepts a string parameter ''' @accepts(str) def printStuff(name): print ("No! This is",name) ''' this function accepts a complex parameter ''' @accepts(complex) def complex_magnitude(z): print (math.sqrt (z.real**2 + z.imag**2)) ''' This function accepts 2 parameters - a dictionary and an integer ''' @accepts(dict, int) def avgTemp(records, year): for month, temp in records.items(): print("The average temp for", month, year, "was",temp) ''' This function is not decorated. So, it does not check for right types ''' def printHello(name): print("Hello",name) ''' This is a generator. We use generators when a function is used to generate a series of numbers, but we don't want the overhead of memory management every time the function is called. A generator is a function that stays in memory when control returns from the function. This happens when we "yield" control from the function instead of "return"ing from it. A generator is called the first time, we store its address in a function pointer. Then we call it using next(function pointer). Control picks up where we left off the last time, and gets the next number in the series. Generators typically have an infinte loop and rely on being called to control the number of items generated. ''' def fibo(): a = 0 b = 1 while True: c = a+b yield c a=b b=c ''' Create the function pointer to the generator. Creating a second pointer will result in a econd series of numbers, starting at the beginiing ''' fibogen = fibo() fibo2 = fibo() if __name__ == "__main__": #We call each decorated function twice, once correctly and once wrongly complex_magnitude(5+2j) complex_magnitude("Squidward") printStuff("Patrick") printStuff(1+3) avgTemp( [ ("Jan",60), ("Jun",90)], 2017) avgTemp( {"Jan":60, "Jun":90}, 2018) #This function wasn't decorated, so it doesn't check for type matching printHello("Spongebob") printHello(24) #Using the generator for printing Fibonacci numbers count = 1 while count <= 15: print(next(fibogen)) print(next(fibo2)) count += 1 ''' Iterators create an iterable list that we can iterate through. While we can randomly access items in a list, we can get the previous and next items on an iterable list. It also comes with the other iterator functions like first and end.''' i1 = iter(range(0,21)) for item in i1: print(item)