<jupyter_start><jupyter_text>Passing multiple values to functions So far we have seen several ways to pass data to a function. One way is to use a series of values seperated by a comma<jupyter_code>def passComma(x,y,z):
    print(x,y,z)
passComma(1,2,3)<jupyter_output><empty_output><jupyter_text>The problem is that it only works with the exact number of items<jupyter_code>passComma(1,2)
def passComma(x=0,y=0,z=0):
    print(x,y,z)
passComma(1,2,3)
passComma(1,2)<jupyter_output><empty_output><jupyter_text>We have seen ways of passing arguments though that can take any number of arguments. look at print. It can take any number of arguments<jupyter_code>print(1,2)
print(1,2,3,4,5)

def passComma(x=0,y=0,z=0):
    if( z==0):print(x,y)
    elif(y==0 and z==0): print(x)
    print(x,y,z)
passComma(1,2,3)
passComma(1,2)
passComma(1)<jupyter_output><empty_output><jupyter_text>We can even add keywords arugments that are optional<jupyter_code>print(1,2,3,4,5,sep=",")<jupyter_output><empty_output><jupyter_text>How do we do this in our own code?<jupyter_code>def testfunc(*args,**kwargs):
    print(type(args))
    print(type(kwargs))
    if 'keyword' in kwargs.keys():
        print(kwargs['keyword']*"!")
grep | lsusb -f -r -12356526
testfunc(1,2,3,4,5,otherword="test",keyword = 4)
def average(*args):
    x = len(args)
    y = sum(args)
    return y/x

average(1,2,3,4,5)
    
def tupleAverage(tup):
    x = len(tup)
    y = sum(tup)
    return y/x

tupleAverage((1,2,3,4,5))<jupyter_output><empty_output><jupyter_text>Here we see that we are making a tuple or a dictionary. We know how to handle those! So what are those ***** doing?\\Its called unpacking, ***** is for tuples and list, ****** is for dictionaries<jupyter_code>def my_sum(a, b, c):
    print(a)
    print(b)
    print(c)
    print(a + b + c)

my_list = [1, 2, 3]
my_sum(*my_list)
my_first_dict = {"A": 1, "B": 2}
my_second_dict = {"C": 3, "D": 4}
my_merged_dict = {**my_first_dict, **my_second_dict}

print(my_merged_dict)
list1 = list(range(0,10))

list2 = list(range(0,10))

*zip(list1,list2)<jupyter_output><empty_output><jupyter_text>Pure vs Impure functionsWhat make a function "pure"<jupyter_code>def pureFunction(x):
    y = []
    for i in range(len(x)):
        y.append( x[i]**2)
    return y

x = [1,2,3,4]
y = pureFunction(x)
print(x)
print(y)
y = pureFunction(x)
print(x)
print(y)
def impureFunction(x):
    for i in range(len(x)):
        x[i] = x[i]**2
x = [1,2,3,4]

y = impureFunction(x)
print(x)
print(y)<jupyter_output>[1, 65536, 43046721, 4294967296]
None<jupyter_text>Here the main difference between a pure function and an impure one is that the impure one chages the value of something passed to it. It does something other than return a value. both are doing $X->X^2$ but the pure function returns a value, and the impure one just relies on mutationChanging something without returning a value is known as a "side-effect" What else is a side effect?\\\\\\\\\\\ Globals<jupyter_code>x = 0

def setx(n):
    global x
    x = n

print(x)
y = setx(5)
print(y)
print(x)<jupyter_output>0
None
5<jupyter_text>Global variables let you change something in the code without seeing it immediately. \what else generates side effects?\\\\\\\\\\\\\\\\\\\ file writing<jupyter_code>def addToFile():
    with open("myfile.txt","a") as file:
        file.write("I need to add something\n")
        

def readFromFile():
    contents = ''
    with open("myfile.txt","r") as f:
        contents =f.read()
        return contents
    return contents



addToFile()
x = readFromFile()
print(x)
addToFile()
addToFile()
addToFile()
y = readFromFile()
print(y)<jupyter_output>I need to add something

I need to add something
I need to add something
I need to add something
I need to add something<jupyter_text>Print Statements<jupyter_code>x = print("This is a side effect")
print(x)<jupyter_output>This is a side effect
None<jupyter_text>Try to write pure functionsIts better to write pure functions because it increases readability and it make it possible to track down bugs and test your code. In genernal the only times you should deal with side-effects is when you are doing I/O operations like talking to a port, reading an writing files, printing, etc.  \\\\\\\\\\\\\\ Function Composition ![image.png](attachment:image.png)<jupyter_code>def f(x):
    if x==1: return 1
    elif x==2: return 3
    elif x==3: return 1
    elif x==4: return 2
    return 0

def g(x):
    if x==1: return 2
    elif x==2: return 3
    elif x==3: return 1
    elif x==4: return 2
    return 0

print(f(1))
print(g(1))
h = lambda x:g(f(x))
print (h(1))
print (h(2))
print (h(3))
print (h(4))
def double(x):
     return x * 2

def inc(x):
     return x + 1

inc_and_double = lambda x: double(inc(x)) # 2(x+1)
inc_and_double(10)
double_and_inc = lambda x: inc(double(x)) # 2x+1
double_and_inc(10)<jupyter_output><empty_output><jupyter_text>You can combine functions together to make new functions! An important thing to note is that **ORDER MATTERS**\$f \circ g$ may not be the same as $g \circ f$ **What if you have a whole bunch of functions you want to compost together?**<jupyter_code>from functools import reduce

def compose(*funcs):
    """Compose a group of functions (f(g(h(...)))) into a single composite func."""
    return reduce(lambda f, g: lambda x: f(g(x)), funcs)

# Example
f = lambda x: x + 1
g = lambda x: x * 2
h = lambda x: x - 3

# Call the function x=10 : ((x-3)*2)+1 = 15
print(compose(f, g, h)(10))#((x-3)*2)+1
print(compose(g,f, h)(10)) #((x-3)+1)*2 
print(compose(g,h,f)(10))  # ((x+1)-3)*2 - The same as above because order doenst matter on + or -<jupyter_output><empty_output><jupyter_text>What! thats crazy! you can put functions in a list? or tuple? What about a dictionary or set?<jupyter_code>func_set = set((double,inc))
print(func_set)
func_dict = {"dub":double,"plus1":inc}
print(func_dict)<jupyter_output><empty_output><jupyter_text>OK so we can store functions as variables, in lists, can we pass and return functions?\\\\\\\\\\\\\\\\\We already have passed functions to function! we did it with **map**, **reduce**, and **filter**<jupyter_code>list1 = [1,2,3,4,5,6,7,8,9]
eg = map(double, list1)
print (eg)
print(list(eg))<jupyter_output><empty_output><jupyter_text>Here we pass double into map. Where in math do we commonly pass functions to function?\\\\\\\\\\\\\\\\\\\![image.png](attachment:image.png) These are called higher order functions. They are functions that take in and return functions.when is it good to write a function that takes in another function? when you see a pattern in your code that is replicated across instances. Forinstance lets look at our old friend sumations<jupyter_code>def summation(n, term):
    total, k = 0, 1
    while k <= n:
        total, k = total + term(k), k + 1
    return total

def cube(k):
    return pow(k,3)

def oneOver(k):
    if k==0: return 0
    return 1/k

summation(10,cube)
def summationCube(n, term):
    total, k = 0, 1
    while k <= n:
        total, k = total + pow(k,3), k + 1
    return total

def summation(n, term):
    total, k = 0, 1
    while k <= n:
        if k>0
            total, k = total + 1/k, k + 1
        else
            total, k = total + 0, k + 1
    return total

summation(10,oneOver)<jupyter_output><empty_output><jupyter_text>Another reason to pass functions is to eliminate case statementsLets say you need to write a report about data from several samples<jupyter_code>import random
stock = {"steel": random.randrange(100,10000),
         "aluminum": random.randrange(100,10000),
         "brass": random.randrange(100,10000),
        }
def writeReport(data):
    align = max(map(len,data))
    for material,count in data.items():
        print('{material:<{align}}  {count:>10,}'.format(material=material, count=count, align=align))
writeReport(stock)<jupyter_output><empty_output><jupyter_text>Lets say someone comes and says write to a file<jupyter_code>def writeReport(data,filename = None):
    align = max(map(len,data))
    for material,count in data.items():
        line = '{material:<{align}}  {count:>10,}'.format(material=material, 
                                                          count=count, align=align)
        if filename:
            with open(filename,'a') as f:
                f.write(line)
        print (line)
writeReport(stock,filename = "MyReport.txt")<jupyter_output><empty_output><jupyter_text>Then someone comes and says, can we write it and email at the same time<jupyter_code>def writeReport(data,filename = None, to_email=None):
    align = max(map(len,data))
    if to_email:
        lines = []
    
    for material,count in data.items():
        line = '{material:<{align}}  {count:>10,}'.format(material=material, 
                                                          count=count, align=align)
        if filename:
            with open(filename,'a') as f:
                f.write(line)
        
        if to_email: lines.append(line)
        print (line)
        
    if to_email: 
        email = "\n".join(lines)
        return email<jupyter_output><empty_output><jupyter_text>instead of true false values we can use a function called write<jupyter_code>template1 = '{material:<{align}}  {count:>10,}'.format # REquires material, align, count
def writeReport(data,write = print, template=template1):
    align = max(map(len,data))
    for material1,count1 in data.items():
        line = template(material=material1, count=count1, align=align1)
        write(line)
        
writeReport(stock)
def toFile(line):
    filename = "log.txt"
    with open(filename,'a') as f:
        f.write(line)
        
writeReport(stock,write=toFile)
cvstemplate = '{material},{count}\n'.format
writeReport(stock,write=toFile,template=cvstemplate)<jupyter_output><empty_output><jupyter_text>What about returning a function? When do we want to return functions?A funciton that makes a common type of function with a value set<jupyter_code>def incrementBy(n):
    def adder(k):
        return k+n
    return adder

i2 = incrementBy(2)
list(map(i2,range(0,10)))
i2(7)
def incrementListBy(n):
    def adder(*args):
        y=[]
        for a in args:
            y.append(a+n)
        return y
    return adder
iL1 = incrementListBy(3)
iL1(1,2,3,4,5)<jupyter_output><empty_output><jupyter_text>\\\\\\\\\\\\\\\\\\\\\\\\\\\\Wrapping a function is useful for debugging and logging<jupyter_code>def wrap(pre, post):
    def decorate(func):
        def call(*args, **kwargs):
            pre(func, *args, **kwargs)
            result = func(*args, **kwargs)
            post(func, *args, **kwargs)
            return result
        return call
    return decorate



def trace_in(func, *args, **kwargs):
    print ("Entering function",  func.__name__)

def trace_out(func, *args, **kwargs):
    print ("Leaving function", func.__name__)

@wrap(trace_in, trace_out)
def calc(x, y):
    return x + y

print (calc(1,4))
wrap(trace_in,trace_out)(i2)(1)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1356194287793342, 'fraction_numerical': 0.02446789157722394, 'mean_word_length': 3.7007268063274905, 'pattern_counts': {'":': 9, '<': 88, '<?xml version=': 0, '>': 87, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '21', 'dir': 'jupyter-structured-clean-dedup', 'id': '141450', 'license': 'MIT', 'n_tokens_mistral': 3931, 'n_tokens_neox': 3771, 'n_words': 1130, 'path': 'JupyterNotebooks/Fun with Functions.ipynb', 'repo_name': 'JeffreyILipton/ME480', 'size': '10994'}