Functions

A function is a block of code which only runs when it is called, you can pass data, known as parameters, into a function, a function can return data as a result. A function will perform a specific task and is generally named to hightlight this task. Python uses the def keyword to start a function and uses the normal Python indentation for the block (no braces)

Arguments are passed to functions in by object reference. The parameter becomes a new reference to the object. For immutable objects (such as tuples, strings, and numbers), what is done with a parameter has no effect outside the function. But if you pass in a mutable object (such as a list, dictionary, or class instance), any change made to the object changes what the argument is referencing outside the function

Basic function
def function1():                                  # notice the semi-colon at the end
    print("Hello World")

function1()
Passing Parameters
def function2(str1, str2):
    print(str1 + " " + str2)

function2("Hello", "World")                                   # you can pass parameters by position
function2(str2="World", str1="Hello")                         # you can pass parameters by name
Default values
def function3(str1, str2="world"):                # using a default value
    return str1 + " " + str2

print(function3("Hello", "Paul"))                 # the function will use both passed values
print(function3("Hello"))                         # the function will use the default value
Variable number of parameters
def function4(*strings):                                           # variable number of arguments (mixed data types)
    print(strings[0] + " " + strings[1] + " " + str(strings[2]))   # you could use a for loop here

function4("Hello", "World", 100)                                   # you can send mixed data types

Note: If the final parameter in the parameter list is prefixed with **, it collects all excess keyword-passed arguments into a dictionary. 
The key for each entry in the dictionary is the keyword (parameter name) for the excess argument. The value of that entry is the argument
itself. An argument passed by keyword is excess in this context if the keyword by which it was passed doesn’t match one of the parameter
names in the function definition.
Using Local variables
def function5():
    i = 101                                                        # you can create local variables
    y = "Hello World"
    print(str(i) + " " + y)

function5()
Using Global variables
g_var = 10
nl_var = 5

def function5():

    def inner_function():
        global g_var                              # use the global variable, 10
        nonlocal nl_var                           # bind to closest scoped nl_var variable, 25

        print(str(g_var) + " " + str(nl_var))

    g_var = 20
    nl_var = 25
    print(str(g_var) + " " + str(nl_var))       # we are using the local variables, 20 and 25

    inner_function()


function5()

Note: You can explicitly make a variable global by declaring it so before the variable is used, using the global statement. Global
variables can be accessed and changed by the function. They exist outside the function and can also be accessed and changed by
other functions that declare them global or by code that’s not within a function.
Assigning functions to variables
def function2(str1, str2):
    print(str1 + " " + str2)
          
funcVariable = function2                                           # you can assign a function to a variable
funcVariable("Hello", "World")                                     # then use the variable like a normal function

Lambda Expressions

Short functions can alos defined by using lambda expressions

t2 = {'FtoK': lambda deg_f: 273.15 + (deg_f - 32) * 5 / 9, 
      'CtoK': lambda deg_c: 273.15 + deg_c}
      
print(t2['FtoK'](32))

Generator Functions

Generator function is a special kind of function that you can use to define your own iterator. When you define a generator function, you return each iteration’s value using the yield keyword. The generator will stop returning values when there are no more iterations, or it encounters either an empty return statement or the end of the function. Local variables in a generator function are saved from one call to the next, unlike in normal functions.

Generator Function
def four():
    x = 0
    while x < 4:
        print("in generator, x =", x)
        yield x                                 # notice the yield keyword
        x += 1


for i in four():
    print(i)

Decorators

Functions can also be passed as arguments to other functions and passed back as return values from other functions. It’s possible, for example, to write a Python function that takes another function as its parameter, wraps it in another function that does something related, and then returns the new function. This new combination can be used instead of the original function.

A decorator is syntactic sugar for this process and lets you wrap one function inside another with a one-line addition. It still gives you exactly the same effect as the previous code, but the resulting code is much cleaner and easier to read. Very simply, using a decorator involves two parts: defining the function that will be wrapping or “decorating” other functions and then using an @ followed by the decorator immediately before the wrapped function is defined. The decorator function should take a function as a parameter and return a function

Decorator
def decorate(func):
    print("in decorate function, decorating", func.__name__)

    def wrapper_func(*args):
        print("Executing", func.__name__)
        return func(*args)

    return wrapper_func                         # return the wrapped function


def myfunction(parameter):
    print(parameter)


myfunction = decorate(myfunction)               # The wrapped function is called after the decorator function has completed,
                                                # results in 'in decorate function, decorating myfunction'

myfunction("hello")                             # results in 'Executing myfunction'
                                                # 'hello'