1 08-Functions


Previous: 07-Containers.html

**If debugging is the process of removing software bugs, **
**then programming must be the process of putting them in.**
    - https://en.wikipedia.org/wiki/Edsger_W._Dijkstra

It’s time to start creating a lot more bugs!

1.1 Screencasts

2 Lecture 1:

2.1 Reading

Quick overview:
* http://scipy-lectures.org/intro/language/functions.html
* https://automatetheboringstuff.com/2e/chapter3/
* https://books.trinket.io/pfe/04-functions.html
* https://docs.python.org/3/tutorial/controlflow.html#defining-functions
* https://inventwithpython.com/invent4thed/chapter7.html
* https://inventwithpython.com/invent4thed/chapter8.html
* https://inventwithpython.com/invent4thed/chapter9.html
* https://inventwithpython.com/invent4thed/chapter10.html
* https://python.swaroopch.com/functions.html
* https://realpython.com/defining-your-own-python-function
* https://www.learnpython.org/en/Functions
* https://www.learnpython.org/en/Multiple_Function_Arguments
* https://www.python-course.eu/python3_functions.php
* https://www.python-course.eu/python3_global_vs_local_variables.php
* https://www.python-course.eu/python3_namespaces.php
* https://www.python-course.eu/python3_passing_arguments.php
* https://www.tutorialspoint.com/python3/python_functions.htm

2.2 Functions

https://en.wikipedia.org/wiki/Subroutine
* A function can be considered a named, callable, list of statements.
* A subroutine (a.k.a. function) is a sequence of program instructions that should ideally performs a specific task, packaged as a named unit.
* This unit can then be used (called) in programs wherever that particular task should be performed.

2.2.1 What is the function of a function?

https://en.wikipedia.org/wiki/Subroutine#Advantages
* Abstraction can improve readability. Does it always?
* Enable modularity and re-use?
* Reduce redundancy?
* Decomposing a complex programming task into simpler steps: this is one of the two main tools of structured programming, along with data structures.
* Reduce duplicate code?
* Simplify testing. Does it always?
* Enable reuse of code across multiple programs?
* Divide a large programming task among various programmers, or various stages of a project?
* Hide implementation details from users of the subroutine. Is this always good?
* Improve readability of code by replacing a block of code with a function call, where a descriptive function name serves to describe the block of code?
* This makes the calling code concise and readable, even if the function is not meant to be reused.
* Improve traceability (i.e. most languages offer ways to obtain the call trace which includes the names of the involved subroutines and perhaps even more information such as file names and line numbers); by not decomposing the code into subroutines, debugging could be impaired

https://en.wikipedia.org/wiki/Subroutine#Disadvantages
* Invoking a subroutine (versus using in-line code) imposes some computational overhead in the call mechanism.
* A subroutine typically requires standard housekeeping code both at entry to, and exit from, the function (function prologue and epilogue usually saving general purpose registers and return address as a minimum).
* Testing and debugging can also be more complicated.

2.2.2 Naming and defining

A function definition states:

  1. what the function is named,
  2. what parameters it can take (if any),
  3. what it does, and
  4. what it potentially returns (if anything).

Functions are typically defined in the most global level scope in a file, though can also be defined within functions.
Functions must be defined before they are called in the sequence of statements in a program!

2.2.3 Calling

2.2.4 Scope

https://en.wikipedia.org/wiki/Scope_(computer_science)
https://en.wikipedia.org/wiki/Variable_(computer_science)#Scope_and_extent
* In many languages, variables in a function only exist within that function, and within a function, the calling function’s variables are not accessible to the called function, and vice-versa.
* In python, it’s a bit more nuanced that that (more below).

2.2.5 Parameters versus Arguments

https://en.wikipedia.org/wiki/Parameter_(computer_programming)
Functions optionally take inputs:
* A parameter is a general specification for the unit of potential input to a function (in the declaration/definition).
* A parameter is a kind of variable, used in a subroutine to refer to one of the pieces of data provided as input to the subroutine.
* An argument is an actual value passed into function’s parameter, when the function is called (the actual call).
* The actual pieces of data are the values of the arguments (often called actual arguments or actual parameters) with which the subroutine is going to be called/invoked.
* A function may have no parameters, a single parameter, or multiple.
* Some languages allow default argument values to be specified for each parameter (python does).
* In python, functions themselves are objects, can be passed as arguments to other functions.
* Importantly, in python:
* All arguments are passed by assignment of the variable alias/reference.
* Mutable arguments and immutable arguments apparently differ in their behavior when being passed to a function (and yet do not actually differ).
* Immutable arguments can not be modified, and thus are re-assigned upon assignment to variables within functions, thus having no impact on the original object passed into the function, from the scope above the function call. This leads to the appearance of it having been passed by value, rather than by reference (a non-python C/C++concept), when in reality, it was just passed by assignment.
* Mutable arguments can be modified, and thus if they are modified within the function, can have an impact on the original object passed into the function, from the scope above the function call. This leads to the appearance of it having been passed by reference, rather than by value (a non-python C/C++ concept), when in reality, it was just passed by assignment.

2.2.6 Return values and statements

https://en.wikipedia.org/wiki/Return_statement
* In some languages, to return a value to main’s scope, a function uses a special return statement.
* In some languages, the return type or variable is part of the declaration/definition.
* This type-hint is optional but recommended in Python!
* There is often one return statement, but there can be multiple, as there can be in Python.
* However, only one return statement is ever executed, because after it executes, the function completes execution by returning execution to the calling function.

2.3 Functions in Python

2.3.1 General specification

Without type hints:

def func_name(arg1, arg2, defaulted_arg='somevalue'):
    print('statements here')
    some_local_var = 'someothervalue'
    return some_local_var

With optional type hints (highly recommended):

def func_name(arg1: type, arg2: type, defaulted_arg: type = 'somevalue') -> return_type:
    print('statements here')
    some_local_var: type = 'someothervalue'
    return some_local_var

Note: parameters and arguments in definitions and calls can be broken onto different lines (as above).

2.3.2 Options

2.3.3 Syntax

With parameters: parameters are specified by name, in a comma-separated list.
08-Functions/functions_00_convert.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def ft_inch_to_cm(num_ft, num_inch):
    num_cm = ((num_ft * 12) + num_inch) * 2.54
    return num_cm

result_cm = ft_inch_to_cm(5, 6)
print(result_cm)

Type hints: technically optional, a form of comment, but required in this class!
08-Functions/functions_01_convert_type.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def ft_inch_to_cm(num_ft: float, num_inch: float) -> float:
    num_cm = ((num_ft * 12) + num_inch) * 2.54
    return num_cm

result_cm: float = ft_inch_to_cm(5, 6)
print(result_cm)

Parameter and argument naming: arguments can be named variables, and variable arguments don’t need to be named the same as the parameter itself:
08-Functions/functions_02_convert_vars.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def ft_inch_to_cm(num_ft: float, num_inch: float) -> float:
    num_cm = ((num_ft * 12) + num_inch) * 2.54
    return num_cm

five: float = 5.0
# note argument name here vs. parameter name above
result_cm = ft_inch_to_cm(num_ft=five, num_inch=6.0)
print(result_cm)

NAME YOUR PARAMETERS UPON PASSING ARGUMENTS TO THEM in the actual function call itself (a.k.a, keyword arguments)!!
Explicit is better than implicit.
Doing it this way makes your assignment unambiguous, even when order is not as specified in the definition.
Named keyword arguments must come last, if you mix them with positional arguments.

Without parameters: A function without parameters has empty parentheses.
Note: this function also happens to return None, but that is not related to its lack of parameters.
08-Functions/functions_03_noparam.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def print_hello() -> None:
    print("Hello there.")

print_hello()

+++++++++++++++++++++++++
Cahoot-08.1
https://mst.instructure.com/courses/58101/quizzes/55735

What about order?
08-Functions/functions_04_print.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def print_hello() -> int:
    four: int = 4
    print("Hello there.")
    return four

print(print_hello())

With return value: A function may print AND return something
08-Functions/functions_05_return.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def print_hello() -> int:
    four: int = 4
    print("Hello there.")
    return four

my_int = print_hello()  # prints and returns value
print(my_int)

print and return are different!
Get the vocabulary straight!
print() prints to the screen!
return just returns a value from a function!

Without return value: A function that doesn’t return a value uses “-> None”.
08-Functions/functions_06_noreturn.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def print_four() -> None:
    four: int = 4
    print(four)

print_four()  # prints alone, but return value of None
var = print_four()  # prints, and saves return value of None
print(print_four())  # prints and then prints returned value (None)

With array parameters:
08-Functions/functions_07_inarrays.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List, Any

def print_array(my_array: List[Any]) -> None:
    for item in my_array:
        print(item)

some_array: List[int] = [4, 3, 1]
print_array(some_array)

another_array: List[str] = ["hey", "you"]
print_array(another_array)

The easy promiscuity of functions like this, operating on any iterable container, in python is one of python’s more pleasant benefits!

+++++++++++++++++++++++++
Cahoot-08.2
https://mst.instructure.com/courses/58101/quizzes/55736

Returning arrays: What about returning an array from a function??
08-Functions/functions_08_outarrays.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List

def mod_array(some_array: List[int]) -> None:
    some_array[1] = 1

some_array: List[int] = [0, 0, 0]
mod_array(some_array)
print(some_array)

Does this help?
08-Functions/functions_09_outarrays.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List

def mod_array(my_array: List[int]) -> List[int]:
    my_return_array = my_array
    my_return_array[1] = 1
    return my_return_array

some_array: List[int] = [0, 0, 0]
another_array = mod_array(some_array)
print(some_array)
print(another_array)

This actually prevents modification of the global list object.
08-Functions/functions_10_outarrays.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List

def mod_array(my_array: List[int]) -> List[int]:
    my_return_array = my_array.copy()
    # This would also work:
    # my_return_array = my_array[:]
    my_return_array[1] = 1
    return my_return_array

some_array: List[int] = [0, 0, 0]
another_array = mod_array(some_array)
print(some_array)
print(another_array)

+++++++++++++++++++++++++
Cahoot-08.3
https://mst.instructure.com/courses/58101/quizzes/55737

Scoping
08-Functions/functions_11_scope.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def print_what() -> None:
    g: int = 3
    i: int = 5
    print(g, i)

g: int = 4
print_what()
print(g)
print(i)

Uses: functions can be used as expressions
* A function call’s arguments can be expressions. Ex: z = ft_inch_to_cm(x, y + 1)
* A function call may appear in an expression. Ex: z = 1.0 + ft_inch_to_cm(5, 6)
* A print() statement’s item may be an expression, so a call may appear there, as below.

08-Functions/functions_12_directprint.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def ft_inch_to_cm(num_ft: float, num_inch: float) -> float:
    num_cm = ((num_ft * 12) + num_inch) * 2.54
    return num_cm

# Function returns walue as the result of evaluating
# the expression containing a function
print(ft_inch_to_cm(5, 6))

2.4 Built in functions (review)

Built-in math and functions
08-Functions/functions_13_math.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import math

x: float = float(input())
y: float = float(input())

# python's math import:
print(math.sqrt(x))
print(math.pow(x, y))

# built-in:
print(abs(x))

Built-in random functions
08-Functions/functions_14_random.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import random

random.seed(5)
print(random.randint(0, 5))

2.5 Example programs

2.5.1 Square root

08-Functions/functions_15_sqrt.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import math
import numpy

def sqare_rt(my_val: float) -> float:
    diff: float = 1.0
    root: float = 1.0

    while (diff > 0.0001) or (diff < -0.0001):
        root = (root + (my_val / root)) / 2.0
        diff = (root * root) - my_val
    return root

twenty: float = 20.0

# Our version:
print(sqare_rt(20.0))

# Versus python's:
print(math.sqrt(20.0))

# Versus numpy's
print(numpy.sqrt(20.0))
08-Functions/functions_15_sqrt_cfg.svg

2.5.2 Dragon realm

http://inventwithpython.com/invent4thed/chapter5.html

08-Functions/functions_16_dragon.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import random
import time

def displayIntro() -> None:
    print(
        """You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.\n"""
    )

def chooseCave() -> str:
    cave = ""

    while cave != "1" and cave != "2":
        cave = input("Which cave will you go into? (1 or 2)")

    return cave

def checkCave(chosenCave: str) -> None:
    print("You approach the cave...")
    time.sleep(2)
    print("It is dark and spooky...")
    time.sleep(2)
    print("A large dragon jumps out in front of you! He opens his jaws and...")
    print()
    time.sleep(2)
    friendlyCave = random.randint(1, 2)

    if chosenCave == str(friendlyCave):
        print("Gives you his treasure!")
    else:
        print("Gobbles you down in one bite!")

def main() -> None:
    playAgain = "yes"
    while playAgain == "yes" or playAgain == "y":
        displayIntro()
        caveNumber = chooseCave()
        checkCave(caveNumber)
        print("Do you want to play again? (yes or no)")
        playAgain = input()

if _name_ == "_main_":
    main()
08-Functions/functions_16_dragon_cfg.svg

2.5.3 Caesar extended

http://inventwithpython.com/invent4thed/chapter14.html
* We have covered variables, branching, loops, and arrays.
* What else could be left?
* The code we had was one big mass, left for you to decipher, with no neat labels, or convenient abstractions.
* Parts of it could be made re-usable in other programs!

2.5.3.1 Key generation, Encryption, and decryption

08-Functions/functions_17_caesar.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from typing import List
import random

caesar_encoding: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "

def key_gen() -> int:
    """
    Generates one Caesar key
    """
    key = random.randint(1, 25)
    return key

def str_to_num_arr(message: str) -> List[int]:
    """
    Translates a string into a Caesar-encoded List
    """
    arr: List[int] = []
    for character in message:
        arr.append(caesar_encoding.find(character.upper()))
    return arr

def num_arr_to_str(encoded_arr: List[int]) -> str:
    """
    Translates a Caesar encoded list back into a string
    """
    plaintext: List[str] = []
    for encoded_char in encoded_arr:
        plaintext.append(caesar_encoding[encoded_char])
    plaintext_string = "".join(plaintext)
    return plaintext_string

def translate(encoded_arr: List[int], mode: int, key: int) -> List[int]:
    """
    Encrypts or decryps a Caesar-encoded List of ints
    """
    translated: List[int] = []
    for encoded_char in encoded_arr:
        if mode == 1:
            # 27 is the symbol set size (# letters in alphabet)
            # Note the space added above (bug from last time)!
            translated.append((encoded_char + key) % 27)
        else:
            translated.append((encoded_char - key) % 27)
    return translated

message: str = input("\nEnter your message, in English:\n")
gen_key: str = input("Want to generate a key? (y/n)")

if gen_key == "y":
    ok: int = 0
    while ok == 0:
        key = key_gen()
        print("Your key is: ", key)
        print("Is the key it ok with you (1-yes, 0-no, make another): ")
        ok = int(input())
else:
    key = int(input("What is your key (0-25)?"))

print("\nYour Caesar key is: ")
print(key)
print("\n Share this with your partner. Don't tell anyone else\n")

print("\nEnter 1 for encryption, and 0 for decryption: ")
mode: int = int(input())
message_arr = str_to_num_arr(message)
message_arr = translate(message_arr, mode, key)
message = num_arr_to_str(message_arr)
print(message)
08-Functions/functions_17_caesar_cfg.svg

3 Lecture 2:

3.1 Language focus

To be stepped through in the python3-spyder IDE and/or python3-pudb debugger:
08-Functions/functions_18_overview.py

+++++++++++++++++++++++++
Cahoot-08.4

+++++++++++++++++++++++++
Cahoot-08.5

+++++++++++++++++++++++++
Cahoot-08.6

+++++++++++++++++++++++++
Cahoot-08.7

+++++++++++++++++++++++++
Cahoot-08.8

3.2 A deep point…

When you nest function calls, you can check and set the limit of the number of possible function calls on the stack in python:
* https://docs.python.org/3/library/sys.html#sys.getrecursionlimit
* https://docs.python.org/3/library/sys.html#sys.setrecursionlimit
* https://docs.python.org/3/library/inspect.html#the-interpreter-stack

08-Functions/functions_19_depth.py (While tracing, not the stack window in pudb3!)

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
import inspect

# defaults to 1000 in python3, and 3000 in ipython3?
print(sys.getrecursionlimit())
sys.setrecursionlimit(1500)
print(sys.getrecursionlimit())

# 1 in python3
# 12 in ipython3
# Overhead of extras in ipython3
print("\nIn global/main")
print(len(inspect.stack()))

def func1() -> None:
    print("\nIn func1")
    print(len(inspect.stack()))
    func2()

def func2() -> None:
    print("\nIn func2")
    print(len(inspect.stack()))
    func3()

def func3() -> None:
    print("\nIn func3")
    print(len(inspect.stack()))

func1()

08-Functions/functions_19_depth_cfg.svg
The sys. functions are not aptly named, since it applies generally to function call depth.
It is however typically only relevant when doing recursion, because you might actually reach that depth.

4 Lecture 3:

4.1 Nested function definitions, namespaces, and scope

4.1.1 General

Namespaces and scopes are related constructs:

4.1.1.1 Namespace

https://en.wikipedia.org/wiki/Namespace
* In computing, a namespace is a set of signs or symbols (names) that are used to identify and refer to objects of various kinds.
* A namespace ensures that all of a given set of objects have unique names, so that they can be easily identified, and there are often multiple namespaces within a program.
* Namespaces are often structured as hierarchies to allow reuse of names in different contexts.
* As a rule, names in a namespace do not generally have more than one meaning; that is, different meanings cannot share the same name in the same namespace (unless one masks the other in some cases).
* A namespace is also called a context, because the same name in different namespaces can have different meanings, each one appropriate for its namespace.
* A namespace (sometimes also called a name scope), is an abstract container or environment created to hold a logical grouping of unique identifiers or symbols (i.e. names).
* An identifier defined in a namespace is associated only with that namespace.
* An equally named identifier can be independently defined in multiple namespaces.
* That is, an identifier defined in one namespace may or may not have the same meaning as the same identifier defined in another namespace.
* Languages that support namespaces specify the rules that determine to which namespace an identifier (not its definition) belongs.

4.1.1.2 Scope

https://en.wikipedia.org/wiki/Scope_(computer_science)
https://en.wikipedia.org/wiki/Variable_(computer_science)#Scope_and_extent
* The scope of a name binding, an association of a name to an entity, such as a variable, is the part of a program where the name binding is valid, that is, where the name can be used to refer to the entity.
* In other parts of the program the name may refer to a different entity (it may have a different binding), or to nothing at all (it may be unbound).
* The scope of a name binding is also known as the visibility of an entity, particularly in older or more technical literature; this is from the perspective of the referenced entity, not the referencing name.
* The term “scope” is also used to refer to the set of all name bindings that are valid within a part of a program or at a given point in a program, which is more correctly referred to as context or environment

4.1.1.3 Python

+++++++++++++++++++++++++
Cahoot-08.9
https://mst.instructure.com/courses/58101/quizzes/55858

+++++++++++++++++++++++++
Cahoot-08.10
https://mst.instructure.com/courses/58101/quizzes/55859

4.2 Brief preview: import and main

https://realpython.com/python-main-function/
More to come on main in 09-ModulesPackages.html

Next: 09-ModulesPackages.html