<jupyter_start><jupyter_text>Python Functions- Context: what are functions? why are they helpful?     - They are reusable peices of code     - Takes an input to produce and output.     - Abstractions: (Print function) allow us to abstract away.   Using Functions             Vocab                Run/invoke/call  Start of function        Argument  Value passed to the function        Return Value  Result of evaluating the function call expression.<jupyter_code>1 + 2
int(123) #Int = call #Argument = string 123 and #Return of 123<jupyter_output><empty_output><jupyter_text>We've already used built-in functions             Mini Exercise -- Using Functions                            Take a look at this code snippet:            max([1, 2, 3])            What is the function name?             Max             Where is the function invocation?             On [1, 2, 3]            What is the return value?             3                            Take a look at this code snippet:            type(max([1, 2, 3]))            What will the output be? Why?                            Take a look at this code snippet:            type(max)            What will the output be? Why?                            What is the difference between the two code blocks below?            print            print()                What other built in functions do you know?<jupyter_code>max([1,2,3])
type(max([1,2,3]))
type(max)
print # refrences the print function
print() #invoking the function/ calling th function<jupyter_output><jupyter_text>Function signiture e.g.max(l: list(int))-> int What are the signitures for - print()    -  print(x) -> none    - can not store a value in print- range()    - takes in two arguments, both int, and returns a list(range) of ints    - range(start: int, stop: int[, step: int]) -> list[int]  Other types of built-in functions- max()- min()- range()- print()- return()  Defining Functions             Vocab                Function Definition        Function Name        Argument        Parameter        Function Body<jupyter_code>def increment(n):
    return n + 1

increment(2) # calling the function  
# 2 isthe argument to the invocation of the incremented function.<jupyter_output><empty_output><jupyter_text>Mini Exercise -- Defining Functions                What is the difference between calling and defining a function?                    What is the difference between the two code blocks below?            def increment(n):    return n + 1            def increment(n):    print(n + 1)                Create a function named nonzero. It should accept a number and return true if the number is anythong other than zero, false otherwise.        Use your nonzero function in combination with the built-in input function and an if statement to prompt the user for a number and print a message displaying whether or not the number is zero.        Transfer the work you have done into a function named explain_nonzero. Calling this function whould prompt the user and display the message as before.<jupyter_code>def increment(n):
    print(n + 1)
increment(3)
def increment(n):
    return n + 1
increment(3)
def nonezero(n):
    if n != 0:
        return True
    else: 
        return False
nonezero(2)
while True:    
    n = int(input("Please enter a number: "))

    if n != 0:
        print(f"{n} is not equal to 0")
    elif n == 0: 
        print(f"{n} is equal to 0")
    else: 
        print("Not a number")
    Continue = input("More numbers? ")
    if not Continue.lower().startswith("y"):
        break
def explain_nonezero():    
    n = int(input("Please enter a number: "))

    if n != 0:
        print(f"{n} is not equal to 0")
    elif n == 0: 
        print(f"{n} is equal to 0")
    else: 
        print("Not a number")
explain_nonezero() # must use this to call a def funtion 
                    #rather than just running it as before
def increment(n):
    return n + 1
assert increment(3) == 4
def increment(n):
    print(n + 1)
assert increment(3) == 4 
# We see the 4 printed but the function did not return the value 4 so we get an assertion error<jupyter_output>4<jupyter_text>- what happens if we omit the return keyword?    - the function does not return a value.    - the function call expression evaluates to None- when is this useful?- for side effects     - 'square_and_double(x)': Produces a Value     - 'insert_book_into_database(book)': Has a side effect   Default Parameter Values and Keyword Arguments<jupyter_code># sayhello(name: str) -> str  *(function signiture)*
def sayhello(name="Easley"):
    return f"Hello, {name}!"

# The name parameter has a default value of "Easley"
sayhello() # Calls the function so it prints
sayhello("class") # can update the name value
# More customization 

def sayhello(name="Easley", greeting="Hello"):
    return f"{greeting}, {name}!"
sayhello()
sayhello("Class", "Good Afternoon") # defined by their position
sayhello(greeting="Salutations") # defined as keyword
sayhello(greeting="Salutations", name="Class")<jupyter_output><empty_output><jupyter_text>- postitional arguments parameter defined by position or order- keyword arguments parameter defined by keyword  Function Scope- defining variables inside/outside of functions- defines where a variable can be referenced             Vocab                Scope        Global        Local<jupyter_code># NB. function names and variables are very generic here because the concept is very generic
def f():
    x = 123 # variable x has a local scope (b/c its inside the body of the function)

f()    
print(x) # does not print because x is not in outside world
x = 123 # (x variable is golbaly defined variable)

def f():
    print(x)

f()<jupyter_output>123<jupyter_text>Why would yo use a global scope vs local scope?  Which is preferd?- short answer: prefer local scope, ise global sparingly when a variable needs to be refrenced from within multiple functions.<jupyter_code>x = 123

def f(x):
    return x + 1

print(f(12)) # only prints f() (Only calling from the local scope)
x = 123

def f(x):
    return x + 1

print(f(12), x) # Also prints x (calling from local scope and global scope)<jupyter_output>13 123<jupyter_text>Mini Exercise -- Function Scope                What is the difference between local and global scope? Which is preferred?        Take a look at the cell below this one. Before running it, think about what you would expect to happen. Explain step by step how the python code is executing.<jupyter_code>def changeit(x):
    x = x + 1

x = 42
print(x)
changeit(x) # there is not chnage for changeit() because x is local to the function while the golbal x is unchnaged.
print(x)
def changeit(x):
    return x + 1

x = 42
print(x)
print(changeit(x)) #This is how you would get the changeit(x) to print
print(x)<jupyter_output>42
43
42<jupyter_text>- perfere local scope - avoid re-assigning global variables   Function Scope Example```pythondef fill_nulls(df):    return df.fillna(0)    def drop_outliers(df):    outlier_cutoff = 3    return df[df.zscore().abs() < 3]    def prep_dataframe(df):    df = fill_nulls(df)    df = drop_outliers(df)    return ``` [Data Prep example](https://github.com/CodeupClassroom/darden-nlp-exercises/blob/main/nlp_prepare.py). The specifics here aren't important right now, just pay attention to the overall shape of functions and how local scope is used.  Lambda Functions- A function as an expression- used for "throw away", or one-off, functions<jupyter_code># Useful in pandas and dataframes

def increment(n):
    return n + 1

# same as

increment = lambda n: n + 1<jupyter_output><empty_output><jupyter_text>**Use case**: sorting (min, max too)Python doesn't know how to compare dictionaries, but it does know how to compare strings or numbers<jupyter_code>students = [
    {"name": "<NAME>", "grade": 87},
    {"name": "<NAME>", "grade": 89},
    {"name": "<NAME>", "grade": 99},
    {"name": "<NAME>", "grade": 94},
    {"name": "<NAME>", "grade": 97},
]
sorted([3, 1, 5, 100, -4])
# sort by name
sorted(students, key=lambda s: s["name"])
# sort by grade
sorted(students, key=lambda s: s["grade"]) # defaults low to high 
sorted(students, key=lambda s: -s["grade"]) # sort from high to low<jupyter_output><empty_output><jupyter_text>Mini Exercise -- Lambda Functions &amp; Sorting        Write the code necessary to sort the list of student dictionaries by student last name.    Hints:            You will need to write a function that takes in a student dictionary and returns just the last name.        You can use the .split string method to seperate the first name and the last name.<jupyter_code>student = {'name': '<NAME>', 'grade': 87}

student['name'].split(' ')[-1]

sorted(students, key=lambda s: s["name"].split(" ")[-1])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09395285275025624, 'fraction_numerical': 0.013551987245188475, 'mean_word_length': 3.0884543761638734, 'pattern_counts': {'":': 10, '<': 47, '<?xml version=': 0, '>': 50, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '445655', 'license': 'MIT', 'n_tokens_mistral': 2637, 'n_tokens_neox': 2476, 'n_words': 1065, 'path': 'functions_lesson.ipynb', 'repo_name': 'ljackson707/python-exercises', 'size': '8820'}