Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
3 views

06_functions

The document provides an overview of functions in programming, explaining their definition, invocation, and the importance of parameters and return values. It covers concepts such as local and global variables, function scope, default parameter values, and the use of docstrings for documentation. Additionally, it introduces recursive functions and stack diagrams to illustrate function execution and variable tracking.

Uploaded by

899141
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views

06_functions

The document provides an overview of functions in programming, explaining their definition, invocation, and the importance of parameters and return values. It covers concepts such as local and global variables, function scope, default parameter values, and the use of docstrings for documentation. Additionally, it introduces recursive functions and stack diagrams to illustrate function execution and variable tracking.

Uploaded by

899141
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 12

# -*- coding: utf-8 -*-

"""06_functions.ipynb

Automatically generated by Colab.

Original file is located at


https://colab.research.google.com/drive/159-
uBndGvawJZdxrSiA80oMFePBYSDD4

# Functions

*\"Programming\"* can be seen as the **process of breaking** a


**large and complex computational task** into smaller and smaller
**subtasks**, until the subtasks are simple enough to be performed
by combining basic instructions. To improve *code readability* and
promote *code reuse*, it’s convenient to associate a **name** with
these subtasks, which becomes **callable functions**.

In the context of programming, a **function** is thus a **named


group of statements** that performs a computation.
Each function must be before **“defined”** and then **“called”** by
its name. Optionally, we can specify **parameters** to supply so as
to call a function.

We already used a lot of **pre-defined functions**, by calling them


with suitable **arguments**:
```python
int('32') # transform string into integer
type(1.3) # return the type of the
parameter
len("Hello") # return the length of a string
str(25) # transform the parameter into
string
```
We also used **composition of functions**, where the **return of a
function** is used in turn as a argument of another, and so on:
```python
input_num = "44"
print(str(int(input_num) + 1))
```
Since expressions returns objects, these expressions can be used as
function arguments (see the expression: `int(input_num) + 1` used as
a parameter of `str()`).
"""

#v = int(input("give me a number > "))


#print(v)

input_num = "44"
print(str(int(input_num) + 2))
# the use of str() is not compulsory in print, since all arguments
are automatically transformed into string before visualizing on the
console
print(int(input_num) + 2)

"""# Defining and calling a function

To **define** and then **call/invoke** a function, you have to use


the following **syntax**:

```python
def name_of_function([<list of comma-separated
parameters>]): # the list can be empty
< indented block of statements >

....

name_of_function([<list of comma-separated arguments>])


# function call
```

Therefore, if **function arguments** are needed, when you define the


function you have to introduce one or more variables called
**parameters** between a pair of parentheses. For example:
```python
def print_twice(param1, param2):
print(param1, param2)
print(param1, param2)
```
This function assigns the arguments, passed at calling time, to the
two parameters `param1` and `param1`. Indeed, when the function
`print_twice` is called, it prints the value of the two parameters
twice. For example, after the definition, you can call the function
as follows:
```python
print_twice("Thanks", 1000)
```
This function works with any argument that can be printed by the
build-in function `print`.

You can also pass **expressions as arguments**, which are first


evaluated before being assigned to the parameter variables.
For example, look at the code below, where we also defined a
function without parameters:
"""

def print_twice(param1, param2):


print(param1, param2)
print(param1, param2)

def print_my_strings():
msg = "Hello!"
print(msg)
print("How are you from 0 to 10?")

#print_twice("Thanks") # run-time error


print_twice('Thanks', 1000)
print()
print_my_strings() # no arguments must be passed in this case

# Commented out IPython magic to ensure Python compatibility.


# There are so-called "magic methods" in Jupyther notebook
# For example, %who shows the list of all variables defined within
the current notebook
n = 10

# %who

print_twice("aa", "bb")
print(n)

# This is a Cheat Sheet of magic methods


# https://www.kdnuggets.com/wp-content/uploads/
Jupyter_Notebook_Magic_Methods_Cheat_Sheet_KDnuggets.pdf

# %lsmagic

"""# Functions and Flow of control/execution

Recall that program execution always begins at the first statement


of the program. Statements are run one at a
time, in order from top to bottom.

**Function definitions** _do not alter the flow of execution of the


program_, and the statements inside the function definition
**don't** run until the function is called.

A **function call** is like a **detour in the flow of execution**.


Instead of going to the next statement,
the **flow jumps** to the body of the function, and runs the
statements there, and then **comes
back** to resume from where it left off.

This execution scheme works well even if the called function calls
another, and so on.
Python is good at keeping track of where the flow of execution is,
moving forward and backward between different functions.

In summary, when you read a program, you don't always want to read
from top to bottom. Sometimes it makes more sense if you follow the
flow of execution. Use [Python tutor](https://pythontutor.com/) to
visualize the execution of a program with function definitions and
function calls.

# Variables and parameters within functions are local


When you create a **variable** inside a function definition, it is
**local**, and its **lifetime** starts when the function is called
and ends when the execution of the called function ends.

*Parameters* are also local. For example, outside `print_twice`,


there is no known variables `param1` or `param2`. See the example
below, and discuss because they generate run-time errors.
"""

def foo(val):
incr = 12
a = 10
val = val + incr + a + 10
print("local variable:", val)

a = 20
incr = 1000

foo(8)

print(a) # the variable is **not** the one defined in the body of


foo()
print(incr) # the variable is **not** defined in this environment
# print(val)

# try to create a variable incr and val outside the function


# run by using pythontutor to visualize the execution

"""## Scope of variables and functions

Functions can access variables in two different scopes: *global* and


*local*.

The *local namespace* is created when the function is called and


immediately populated by the function's arguments. After the
function is finished, the local namespace is *destroyed* (with some
exceptions).

A variable defined in the *global namespace* (defined outside, and


before the function call), can be read and used from within the
function.

In the following example `a` is in global namespece, while `c` is in


the local namespace of the function.

"""

def foo1():
c = a + 1 # access varial a, located in the global space
# a = c + 1 # in general, you cannot change variables in the
global namespaces
print("local variable: ", c)
a = 15
foo1()
print(a)

# Commented out IPython magic to ensure Python compatibility.


# %who

"""When the name of variable, already defined in the global space,


is reused within the function to assign (create) a new variable,
this new variable in local namespace is distinct from the variable
in the global namaspace.

In the following example the same name `a` is used in the local and
global namespace. Within the function, we access to the variable in
the local namespace, whereas the variable in the global namespace is
left unmodified.

```python
def b():
a = c + 1
print('local namespace', a)

a = 12
c = 13
b()
print('global namespace', a)
```

"""

def b():
#global a
a = c + 1
print('global namespace', c)
print('local namespace', a)
#print('global namespace', a)

a = 12
c = 13
b()
print('global namespace', a)

"""## Default values of parameter

When you define a function you can specify a default value for one
or more arguments. This creates a function that can be called with
fewer arguments.
"""

def print_twice(param1, param2 = 'Salvatore!', param3 = 'How are


you?'):
print(param1, param2, param3)
print(param1, param2, param3)

print_twice('Hello')
print_twice('Ciao')
print_twice('Hello', param2="Giovanni!", param3 = 'How old are
you?')
print_twice('Hello', param2="Giovanni!")
print_twice('Ciao', param3="Come stai?")

"""# Docstrings

To document a function, a *docstring* should be used.

It is a text put at the beginning of a function to explains the


interface (*doc* is
short for *documentation*). Here is an example:

```python
def fib(n):
'''
Print a Fibonacci series up to n, n>=0.
example for n=35: 0,1,1,2,3,5,8,13,21,34

Args:
n: limit of the value to include in the series, n>=0.
Returns:
No return value, but print the list.
'''
if n == 0:
print(0) # first Fibonacci value
elif n >= 1:
print(0, 1, 1, end=' ') # first three Fibonacci
values
next = 2 # new Fib value (to print)
previous = 1 # previous Fib value (already printed)
while next <= n:
print(next, end=' ')
new_next = next + previous # store in "new_next"
the next Fib value (to print in the next iteration)

previous = next # assign to "previous" the


penultimate Fib value
next = new_next # assign to "next" the next
value to print

print()
```

By convention, all docstrings are triple-quoted strings, also known


as multiline string
because the triple quotes allow the string to span more than one
line.

Docstring are treated by Python as *multiline comments*.


There are tools which use docstrings to automatically produce online
or printed documentation, or to let the user interactively browse
through code.
"""

def fib(n):
'''
Print a Fibonacci series up to n, n>=0.
example for n=35: 0,1,1,2,3,5,8,13,21,34

Args:
n: limit of the value to include in the series (n >= 0).
Returns:
No return value, but print the list.
'''
if n == 0:
print(0) # first Fibonacci value
elif n >= 1:
print(0, 1, 1, end=' ') # first three Fibonacci values
next = 2 # new Fib value (to print)
previous = 1 # previous Fib value (already printed)
while next <= n:
print(next, end=' ')
new_next = next + previous # store in "new_next" the next Fib
value (to print in the next iteration)

previous = next # assign to "previous" the penultimate Fib


value
next = new_next # assign to "next" the next value to print
print()

fib(300) # call the function

# fib
fib(1000)

"""# Fruitful functions

Many of the Python functions we have used produce return


values.

But the above functions are all **void**: they have an effect, as
they print values, but they do not have a return value. How can we
write **fruitful** functions?

Look at this example, a function that returns the area of a circle


with the given radius:

```python
def area(radius):
a = 3.14 * radius**2
return a
```

We can use the `return` statement with or without arguments.


If the function is *void*, the `return` statement forces the exit
from function.
If the function is *fruitful*, the `return` statement must be
followed by an expression, as in the example above.

You can use fruitful functions within expressions, or assign the


function, indeed the returned value, to a variable.

"""

import math

def area_square(side):
a = side**2
return a

def area_circle(radius):
a = math.pi * area_square(radius) #radius**2
return a

total = area_circle(10) + area_square(3) # usage of fruitful


functions within expressions, eventually assigned to a variable
print(area_circle(10))
print(total)

"""# Advanced topic: recursive functions

A **recursive definition** is similar to


a circular definition, in the sense that the definition contains a
reference to the thing being
defined.

It is very common in mathematics. For example, the **factorial** $n!


= 1 \cdot 2 \cdot \ldots \cdot n$ can be expressed as follows:

$$
0! = 1\\
n! = n \cdot (n - 1)!
$$

This definition says that the factorial of $0$ is $1$, and the
factorial of any other value, $n$, is $n$
multiplied by the factorial of $n - 1$.

So $3!$ is $3$ times $2!$, which is $2$ times $1!$, which is $1$
times $0!$. $0!$ is the base case, where the definition is known.

Putting it all together, $3!$ is equal to $3 \cdot 2 \cdot 1 = 6$.


If you can write a recursive definition of something, you can write
a Python program to
evaluate it.

The first step is to decide what the parameters should be. In this
case it should
be clear that factorial takes an integer:
```python
def factorial(n):
```
If the argument happens to be 0, all we have to do is to return 1,
otherwise we have to make a **recursive call** to find the
factorial of $n - 1$ and then multiply it by $n$:

```python
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result
```

Since $3$ is not $0$, we take the second branch and calculate the
factorial of $n-1$ (which is $2$)...<br/>
&nbsp;&nbsp;&nbsp;&nbsp;Since $2$ is not $0$, we take the second
branch and calculate the factorial of $n-1$ (which is $1$)...<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Since $1$ is not
$0$, we take the second branch and calculate the factorial
of $n-1$ (which is $0$)...<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&n
bsp;Since $0$ equals $0$, we take the first branch and return $1$
without making any more recursive calls.<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;The return value,
$1$, is multiplied by $n$, which is $1$, and the result is
returned.<br/>
&nbsp;&nbsp;&nbsp;&nbsp;The return value, $1$, is multiplied by $n$,
which is $2$, and the result is returned.<br/>
The return value ($2$) is multiplied by $n$, which is $3$, and the
result, $6$, becomes the return
value of the function call that started the whole process.

Try with [Python Tutor](http://pythontutor.com/


visualize.html#mode=edit) by defining and then calling the function:

```python
result = factorial(3)
```
"""

def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result

print(factorial(4))

"""# Advanced topic: Stack diagrams

To keep track of which variables can be used where, it is sometimes


useful to draw a **stack
diagram**, which shows the value of each variable but they
also show *the function each variable belongs to*.

*Each function* is represented by a **frame**, indeed a box with the


name of a function
beside it and the parameters and variables of the function inside
it.

The *frames* are arranged in a **stack** that indicates which


function called which, and so
on.

The following is the stack diagram of the call of


```python
result = factorial(3)
```

[Stack diagram image](https://drive.google.com/file/d/1UN-


S_nNGcu5m8MqN7qWfOzI_HfCBLH0Z/view?usp=sharing)

The return values are shown being passed back up the stack. In each
frame, the return value is the value of `result`, which is the
product of $n$ and `recurse`.

In the last frame, the local variables `recurse` and `result` do not
exist, because the branch that creates them does not run.

Note how many different instances of variable `result` are generated


during the recursive call.

# Exercises

1. Write a function ```check_prime``` that takes an integer and


returns *True* if the number is prime, and *False* otherwise.

2. Write a recursive function that realizes *fibonacci*, defined


recursively as follows:
$$
fib(0) = 0\\
fib(1) = 1\\
fib(n) = fib(n-1) + fib(n-2)
$$

2. Write a recursive function that computes the sum of all integer


values $x$ in the interval: *from* $\le x \le$ *to*:
$$
sum\_list(l, l) = l\\
sum\_list(l, u) = l + sum\_list(l+1, u)
$$
"""

# Write a function check_prime(n) that takes an integer and


# returns True if the number is prime, and False otherwise.

def check_prime(n):
'''
Return True if the input argument n is prime, False otherwise
Args:
n: integer to be checked.
Returns:
Boolean value.
'''
is_prime = True

for div in range(2,n):


if n % div == 0: # if n is divisible by div
is_prime = False

return is_prime

print(10, ":", check_prime(10))


print(1, ":", check_prime(1))
print(13, ":", check_prime(13))

# 𝑓𝑖𝑏(0) = 0
# 𝑓𝑖𝑏(1) = 1
# 𝑓𝑖𝑏(𝑛) = 𝑓𝑖𝑏(𝑛−1)+𝑓𝑖𝑏(𝑛−2)

# recursive
def recfib(n):
'''
Print the n-th element of the Fibonacci series (counting from 0)

Args:
n: n-th position in the Fibonacci series.
Returns:
Value of the item in the series at position n.
'''
if n == 0:
return 0
if n == 1:
return 1
return recfib(n-2) + recfib(n-1)

for i in range(20):
print(recfib(i), end=' ')

# 𝑠𝑢𝑚_𝑙𝑖𝑠𝑡(𝑙,𝑙) = 𝑙
# 𝑠𝑢𝑚_𝑙𝑖𝑠𝑡(𝑙,𝑢) = 𝑙 + 𝑠𝑢𝑚_𝑙𝑖𝑠𝑡(𝑙+1,𝑢)

def sum_list(l,u):
'''
Compute and return the sum of the integers x, where l <= x <= u.

Args:
l: lower bound of the series
u: upper bound of the series
requirement: l <= u
Returns:
Sum of the subseries.
'''
if l > u:
s = 0
elif l == u:
s = l
else:
s = l + sum_list(l+1,u)
return s

def sum_list_norec(l,u):
if l > u:
return 0
s = 0
for item in range(l,u+1):
print("-->", item)
s = s + item
return s

print(sum_list(6,5))
print(sum_list(5,5))
print(sum_list(5,10), 10*11/2 - 4*5/2)

print("\n", 6, 5, "*****")
print(sum_list_norec(6,5))

print("\n", 5, 5, "*****")
print(sum_list_norec(5,5))

print("\n", 5, 10, "*****")


print(sum_list_norec(5,10), 10*11/2 - 4*5/2)

You might also like