# Decomposition, Abstraction, and Functions
## Programming Ethnics
- more code is not necessarily a good thing
- measure good programmers by the amount of functionality
- functions
- decomposition and abstraction
exp: **projector**
### ABSTRACTION IDEA:
#### **Do not need to know how projector works to use it**
- a projector is a black box
- don’t know how it works
- know the interface: input/output
- connect any electronic to it that can communicate
with that input
- black box somehow converts image from input source to a wall, magnifying it
### DECOMPOSITION IDEA:
#### **Different devices work together to achieve an end goal**
- projecting large image for Olympics decomposed into
separate tasks for separate projectors
- each projector takes input and produces separate
output
- all projectors work together to produce larger image
*Applying these concepts is crucial to become a good programmer*
## Decomposition (Create Structure)
In programming, divide code into modules
- are self-contained
- used to break up code
- intended to be reusable
- keep code organized
- keep code coherent
*Decomposition can be achieved with functions and classes (advanced)*
## Abstraction (Supress Details)
In programming, think of a piece of code as a black box
- cannot see details
- do not need to see details
- do not want to see details
- hide tedious coding details
*Abstraction can be achieved with function specifications or docstrings*
## Functions
Functions are not run in a program until they are *called* or *invoked*.
Characteristics:
- has a name
- has parameters (0 or more)
- has a docstring (optional but recommended)
- has a body
- returns something
### Refer function structure in slide 12
```python
#def function_name(parameters of arguments):
def is_even( i ):
"""
Input: i, a positive int,
Returns True if i is even
"""
#what happens in """here""" will be the docstring
print("inside is even")
return i%2 == 0
#this is the body
is_even(3)
#calling the function using it's name and values for parameters
```
Parameters are the inputs of the function. Docstring is the specification of the abstraction. It's a multi-line text for explanation purposes. Last line is to call the function.
Function body is a mini program that does things.
```return i%2 == 0``` is a crucial keyword to evaluate value.
## Variable Scope
```python
#the first parentheses will be the formal parameters, this doesn't have value yet, ur assuming x has value later
#below will be ur function definition
def f(x):
x = x + 1
print("in f(x): x=",x)
return x
#beyond this will be the ur main program code
x = 3
z = f(x) #here is ur function call
```
Function definitions will be stored in the computer **until u call it with the values into the function.** The following action will be the **actual parentheses** in which u mapped values into x for the function to work. In this case ur mapping `x = 3`. Return of function will be assigned to variable z.
### Visualising function operations is in slide 15-18
## No `return` Warning
If the function definition is missing a return statement, python automatically returns `None` value, a special type of value that represents the absence of the value.
*Note: `None` is not a string*
## `return` vs `print`
| `return` | `print` |
| - | - |
| return only has meaning **inside** a function | print can be used **outside** functions
| only one `return` can be executed inside a function | many `print` statemtents can be executed
| code after `return` statement will not be executed | can be executed after `print`
| has a value associated with it, given to **function caller** | has a value associated with it but outputted to console
### With `return`
Here we have a function that can identify even positive integers:
```python
def is_even_with_return( i ):
"""
Input: i, a positive int
Returns True if i is even, otherwise False
"""
print('with return')
remainder = i % 2
return remainder == 0
is_even_with_return(3)
print(is_even_with_return(3) )
```
Notice the statement `return remainder == 0`, that is to say if there are no remainders after division, `True` is returned to the function caller, otherwise print `False`.
However, as previously discussed, the boolean values that are returned will not be showed in console unless a `print` statement is carried out, like `print(is_even_with_return(3) )`.
[Visualization](https://pythontutor.com/render.html#code=def%20is_even_with_return%28%20i%20%29%3A%0A%20%20%20%20%22%22%22%20%0A%20%20%20%20Input%3A%20i,%20a%20positive%20int%0A%20%20%20%20Returns%20True%20if%20i%20is%20even,%20otherwise%20False%0A%20%20%20%20%22%22%22%0A%20%20%20%20print%28'with%20return'%29%0A%20%20%20%20remainder%20%3D%20i%20%25%202%0A%20%20%20%20return%20remainder%20%3D%3D%200%0A%0Ais_even_with_return%283%29%20%0Aprint%28is_even_with_return%283%29%20%29&cumulative=false&curInstr=13&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
### Without `return`
```python
def is_even_without_return( i ):
"""
Input: i, a positive int
Does not return anything
"""
print('without return')
remainder = i % 2
is_even_without_return(3)
print(is_even_without_return(3) )
```
This function has the same goal, however it doesn't work like it's supposed to be. This is because the function doesn't have a `return` statement.
As previously discussed, since this function is missing a `return` statement, there are no operations to get back to the function caller.
[Visualization](https://pythontutor.com/render.html#code=def%20is_even_without_return%28%20i%20%29%3A%0A%20%20%20%20%22%22%22%20%0A%20%20%20%20Input%3A%20i,%20a%20positive%20int%0A%20%20%20%20Does%20not%20return%20anything%0A%20%20%20%20%22%22%22%0A%20%20%20%20print%28'without%20return'%29%0A%20%20%20%20remainder%20%3D%20i%20%25%202%0A%0Ais_even_without_return%283%29%0Aprint%28is_even_without_return%283%29%20%29&cumulative=false&curInstr=10&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
### Thus, `None` will be returned to address this issue, the following `print` statement will also print `None`.
## Function as Arguments
Arguments can take on any type, even functions: [Visualization](https://pythontutor.com/render.html#code=def%20func_a%28%29%3A%0A%20%20%20%20print%28'inside%20func_a'%29%0Adef%20func_b%28y%29%3A%0A%20%20%20%20print%28'inside%20func_b'%29%0A%20%20%20%20return%20y%0Adef%20func_c%28z%29%3A%0A%20%20%20%20print%28'inside%20func_c'%29%0A%20%20%20%20return%20z%28%29%0A%20%20%20%20%0Aprint%28func_a%28%29%29%0Aprint%285%20%2B%20func_b%282%29%29%0Aprint%28func_c%28func_a%29%29&cumulative=false&curInstr=14&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
```python
def func_a():
print('inside func_a')
def func_b(y):
print('inside func_b')
return y
def func_c(z):
print('inside func_c')
return z()
print(func_a())
print(5 + func_b(2))
print(func_c(func_a))
```
### `print(func_a())`
When `func_a()` is called, it takes no parameters since there's the parantheses of the caller is left blank. And given there's no return argument, this prints `None`.
### `print(5 + func_b(2))`
`func_b()` takes one parameter overall, in this case it's 2. Notice when it returns 2, the print method adds it up with 5, so 7 will be printed in output.
### `print(func_c(func_a))`
This is a nested function call, where it calls one function that's inside of the other. Just like functions, it goes from the outer to inner layer, so `func_c()` is considered first.
In `func_c()`, value `z()` is returned but it stays on hold while waiting for `func_a` to return something. Note that because `z()` is still waiting for `func_a` value, `z()` **is not the final return value to the function caller.**
Going to `func_a`,as previously discussed, this returns `None`. `None` goes back to `z()`, however knowing that inside `z()` parameter is `None`, `func_c` **finally returns** `None`.
## Scope Example
- inside functions, variable that's defined outside can be accessible
- however, variable defined outside cannot be modified inside a function
- workaround: use global variables, but not recommended
```python
def f(y):
x = 1 #x is re-defined in scope of f
x += 1
print(x)
return x
x = 5 #outside function
f(x)
print(x)
```
[Visualization](https://pythontutor.com/render.html#code=def%20f%28y%29%3A%0A%20%20%20%20x%20%3D%201%20%23x%20is%20re-defined%20in%20scope%20of%20f%0A%20%20%20%20x%20%2B%3D%201%0A%20%20%20%20print%28x%29%0A%20%20%20%20return%20x%0A%0Ax%20%3D%205%20%23outside%20function%0Af%28x%29%20%0Aprint%28x%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
`f(x)` and x that's a global variable are two different x objects. One major mistake in this code is that it uses variable names that are completely the same.
x is global scope is defined as 5, when `f(x)` is called, 5 is mapped into function, so y in scope f is 5. x is re-defined in scope of f as 1, thus the print method in the function prints 2.
However, when x is returned as 2, the last print method in the global scope still prints 5. Why? This is because both variables are in different scopes. **Different print methods resort to variables in their respective scopes.**
The solution to this is replace the global print method `print(x)` to `print(f(x))`.
```python
def g(y):
print(x) #x from outside g (global scope)
print(x + 1)
return x
x = 5
g(x)
print(x) #x inside g is picked up from scope g
```
In this case, function `g(y)` grabs x from the global scope, thus `print(x)` prints 5, and `print(x + 1)` prints 6. This is due to the function not having a re-defined x variable. x will still be returned 5.
The last print method in global scope will still print 5, as previously mentioned.
```python
def h(y):
x += 1
return x
x = 5
h(x)
print(x)
```
[Visualization](https://pythontutor.com/render.html#code=def%20h%28y%29%3A%0A%20%20%20%20x%20%2B%3D%201%0A%20%20%20%20return%20x%0A%0Ax%20%3D%205%0Ah%28x%29%0Aprint%28x%29&cumulative=false&curInstr=5&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
This may be the most common mistake done when starting to learn variable binding in functions. This code will show:
```
UnboundLocalError: local variable 'x' referenced before assignment
```
This basically says since x is not defined in the function, python cannot find x to add 1 to. Remember that when mapping x = 5 to `h(x)`, ur essentially mapping y to 5 in scope f. So y is 5 instead. The proper solution to this is to change `x += 1` to `y += 1` and return y instead.
## Scope details
We will be looking at nested functions: [Visualization](https://pythontutor.com/render.html#code=def%20g%28x%29%3A%0A%20%20%20%20def%20h%28%29%3A%0A%20%20%20%20%20%20%20%20x%20%3D%20'abc'%0A%20%20%20%20x%20%3D%20x%20%2B%201%0A%20%20%20%20print%20%28'g%3A%20x%20%3D',%20x%29%0A%20%20%20%20h%28%29%0A%20%20%20%20return%20x%0A%0Ax%20%3D%203%0Az%20%3D%20g%28x%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
```python
def g(x):
def h():
x = 'abc'
return x
x = x + 1
print ('g: x =', x)
print(h())
return x
x = 3
z = g(x)
print(x)
```
`g(x)` is called, mapping 3 to the function. x is then re-defined as 4 in scope g, thus the print method after prints `g: x = 4`. **Through this whole process** `h()` **is ignored until it is called.**
Once `h()` is called, it looks back to it's definition, where x is re-defined as a string `abc` in scope h. `abc` is printed afterwards. However, instead of `abc`, 4 is returned instead.
Again, the last print method still prints what's x is defined in the global scope, which is 3.