Skip to content
Snippets Groups Projects
Commit 287f490d authored by Ulrich Kerzel's avatar Ulrich Kerzel
Browse files

add comments to account solution whre to improve more

parent c7cbbf75
No related branches found
No related tags found
No related merge requests found
......@@ -42,7 +42,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
......@@ -69,7 +69,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 3,
"metadata": {},
"outputs": [
{
......@@ -362,7 +362,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.10.12"
},
"orig_nbformat": 4,
"vscode": {
......
......@@ -150,7 +150,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.10.12"
},
"orig_nbformat": 4,
"vscode": {
......
......@@ -169,7 +169,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.10.12"
},
"orig_nbformat": 4,
"vscode": {
......
%% Cell type:markdown id: tags:
# Functions
We could - in principle - write code by adding more and more statements after one another. This might well work and do what we want, but would have quite some disadvantages.
* The code becomes very long very quickly ("Spaghetti - Code")
* We would need to keep track of *a lot* of variables as each variable is valid all the time.
* The only way to re-use code is to copy/paste it.
It works... but it is neither nice, efficient, readable or maintainable.
Functions help us to structure code. Essentially, they assign a block of code to a call of this function.
The general structure is:
```
def my_func(<arguments>):
....
my code goes here
....
return <return value>
```
We notice the following:
* The function is defined using the keyword ```def```
* The name of the function is given by ```my_func```
* We can pass ```<arguments>``` arguments to the function, This is optional.
* Functions can have one or multiple return value(s), indicated by the keyword ```return```. This is optional.
Some programming languages distinguish between functions that return a value ("functions") or not ("procedures"). Python does not and we call them all "functions".
Therefore, the simplest function is something like the following:
%% Cell type:code id: tags:
``` python
# define the function
def say_hello():
print("This is a small function that says Hello.")
# call the function
say_hello()
```
%% Output
This is a small function that says Hello.
%% Cell type:markdown id: tags:
This is probably not the most useful function we can think of - but it allows us to see what we can do:
* We can assign a descriptive name to a block of code. The name should give a short indication about what the function does so that when reading the code we can follow what happens.
* We can re-use the code. Whenever we want to execute the same block of code, we call the function. This makes it much easier to write understandable and efficient code - and also helps us to maintain the code. Whenever we find a bug, we need to fix it only in this function.
***Best practice***
Functions should only do one thing. This makes them as short, concise and efficient as possible. It also makes them testable (we know what to expect as output), and more maintainable.
While there is an overhead in calling a function, it is *very* small compared to writing inefficient and lengthy code. Do not hesitate to put even a single line of code into a separate function if you think that this will help to make the code easier to read, debug and maintain.
## Function arguments
Functions can have one or more arguments that we pass when calling the function.
%% Cell type:code id: tags:
``` python
def my_sum(x, y):
return x + y
print(type(x), type(y))
return x +y, x, y
sum = my_sum(3,4)
print('The sum is: {}'.format(sum))
sum, a ,b = my_sum(3,4)
print('The sum is: {} {} {}'.format(sum,a,b))
```
%% Output
The sum is: 7.1
<class 'int'> <class 'int'>
The sum is: 7 3 4
%% Cell type:markdown id: tags:
We can make use of the arguments in several ways: Above we pass two numbers and naturally assume that $x=3, y=4$ when we compute the sum. Here, the order of the variables and the arguments matter. The variables are assigned in the order we pass them.
We can also specify this explicity.
%% Cell type:code id: tags:
``` python
def my_sum(x, y):
print('x={}, y={}'.format(x,y))
return x + y
sum = my_sum(y=4, x=3)
print('The sum is: {}'.format(sum))
```
%% Output
x=3, y=4
The sum is: 7
%% Cell type:markdown id: tags:
Here, we specify the variables directly and can choose any order.
We can also define default arguments, i.e. a value, that is taken when we do not specify the variable.
%% Cell type:code id: tags:
``` python
def my_sum(x=3, y=4):
print('x={}, y={}'.format(x,y))
return x + y
sum = my_sum()
print('The sum is: {}'.format(sum))
sum = my_sum(1)
print('The sum is: {}'.format(sum))
sum = my_sum(2,6)
print('The sum is: {}'.format(sum))
sum = my_sum(x=2)
print('The sum is: {}'.format(sum))
sum = my_sum(x=2, y=1)
print('The sum is: {}'.format(sum))
```
%% Output
x=3, y=4
The sum is: 7
x=1, y=4
The sum is: 5
x=2, y=6
The sum is: 8
x=2, y=4
The sum is: 6
x=2, y=1
The sum is: 3
%% Cell type:markdown id: tags:
There are a (very) few use-cases where you do not know how many arguments a user will pass. We can do this by preceeding the variable name in the argument list with a single ```*```.
***Note***
These arguments are often abbreviated as ```*args```, as we do not know what they will be.
%% Cell type:code id: tags:
``` python
def print_pets(*args):
print(args)
print('dog')
print('dog', 'cat')
print('dog', 'cat', 'hamster')
```
%% Output
dog
dog cat
dog cat hamster
%% Cell type:markdown id: tags:
Sometimes, we might want to use a function with an arbitrary number of arguments and we also do not know what kind of information is going to be passed. We can use the ```**``` construct to accept as many key-value pairs as are provided.
An example could be:
%% Cell type:code id: tags:
``` python
def update_userprofile(name, **profile):
profile['name'] = name
return profile
user_profile = update_userprofile('Reynolds',
first_name = 'Malcom',
rank = 'Captain',
gender = 'male')
print(user_profile)
```
%% Output
{'first_name': 'Malcom', 'rank': 'Captain', 'gender': 'male', 'name': 'Reynolds'}
%% Cell type:markdown id: tags:
As we can see, the ```**profile``` creates a dictionary into which we (implicitly) add all the key-value pairs we pass as arguments to the function.
Inside the function, we can access and change the dictionary as we normally would (e.g. add a new field with key ```name```).
As we can also see, this makes the code less readable and - because we do not know what the user will pass to the function - we also would not know how to check whether this was intentional, etc.
One good use-case for such a construct is that we want to use a generic way to pass options to our code, such as keyword arguments.
In general, when we find ourselves in the situation to need such constructs with ```*``` or ```**```, we should take the opportunity to reflect if we really need this and if we can use simpler approaches to make our code more readable and maintainable.
## Local variables
%% Cell type:code id: tags:
``` python
def my_function(x, y):
i = 0
print('The value of i is {}'.format(i))
return x + y
sum = my_function(3, 4)
# uncommenting this will fail.
# print('The value of i is {}'.format(i))
```
%% Output
The value of i is 0
%% Cell type:markdown id: tags:
So far, all variables we have used in our code, inside or outside loops are always available: Once we have used a variable, it is defined and the Python interpreter keeps it in memory. This is our ***global*** scope.
As our example above shows, variables that we use inside a function are only valid within this function, a ***local*** scope.
This has the benefit that we can define the variables we need inside the function and we do not need to clean up afterwards. Again, this makes the code more readable and maintainable, as we can move blocks of code *together with the relevant variables* into a function.
***Best practice***
When defining a function, do not rely on global variables defined outside the scope of this function. Instead, all variables that you need inside the function should be passed as arguments, all variables that you need back, should be part of the ```return``` statement.
The following works but is clearly not a good idea:
%% Cell type:code id: tags:
``` python
sum = 0
x = 3
y = 7
def stupid_sum():
global sum
sum = x + y
stupid_sum()
print('The sum is {}'.format(sum))
```
%% Output
The sum is 10
%% Cell type:markdown id: tags:
Here, we use the keyword ```global``` to force Python to make use of the variable ```sum``` defined outside the function such that it modifies this variable (otherwise, a local variable would be created with the same name, but not returned. ---try it by commenting out line with ```global sum```).
Obviously, the keyword exists because there may be some legitimate use-case for it.
Generally, avoid such a pattern and have a clearly defined list of input and output values. Otherwise you loose all benefits to writing a function in the place.
%% Cell type:markdown id: tags:
### Exercise
Rewrite the Fibonacci series as a function. The input value should be the number of Fibonacci numbers generated, the output should be a list containing the Fibonacci numbers.
Call your function and have it return 10 Fibonacci numbers.
The output should be like: ```The Fibonacci numbers are: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]```
%% Cell type:code id: tags:
``` python
# ... your code here ....
```
%% Cell type:markdown id: tags:
## Functions within functions
We can also define functions within functions, these are called *inner functions*. This can be helpful if we need to define a function that makes sense only within the context of the function we currently define.
The general syntax is:
```
def my_function(argument_1, argument_2,....):
def inner_function_1(argument_X, argument_Y,...):
# code for inner_function_1
def inner_function_2(argument_A, argument_B, ...):
# code for inner_function_2
# code for outer function my_function
```
%% Cell type:markdown id: tags:
# Lambda functions
Lambda functions are small anonymous functions. They can take any number of arguments but can have only one expression.
The term "anonymous" function means that lambda construct effectively defines a function (as we did above) --- but we do not assign a name it to but call it directly.
The general syntax is:
```
lambda arguments : expression
````
In some cases, they can help to write code quite concise and are especially helpful if we use them inside other functions.
We could write our example calculating the sum as:
%% Cell type:code id: tags:
``` python
sum = lambda x, y : x + y
print('The sum is: {}'.format(sum(3,4)))
```
%% Output
The sum is: 7
%% Cell type:markdown id: tags:
## Recursive Functions
So far, the functions we have encountered were called once from the outside or from a loop. We can also call other functions from a function. Or, we can call the same function from within itself - we call this a ***recursive*** function.
The key thing about recursive functions is to define a suitable stopping point at which the function no longer calls itself. Otherwise, we would call the functions repeatedly in an endless loop.
#### Demo
Remember the definition of the Fibonacci numbers we have encountered earlier: \
The [Fibonacci Numbers](https://en.wikipedia.org/wiki/Fibonacci_number) are a sequence where the current number is derived from the sum of the two preceeding ones, i.e. $F_n = F_{n-1} + F_{n-2}$. The first two numbers are $F_1 = 0$ and $F_2 = 1$.
This already includes a recursive definition: $F_n = F_{n-1} + F_{n-2}$, apart from $F_1 = 0$ and $F_2 = 1$.
Write a recursive functions to calculate the $F_n$ Fibonacci number and compute the first 10 numbers.
%% Cell type:code id: tags:
``` python
def Fibonacci(n):
if n < 0: # not defined for negative numbers
return None
if n <= 1: # first two numbers are 0, 1, stopping criterion
return n
else: # recursive call
return Fibonacci(n-1) + Fibonacci(n-2)
for i in range(0,10):
fib = Fibonacci(i)
print('the Fibonacci number is: {}'.format(fib))
```
%% Output
the Fibonacci number is: 0
the Fibonacci number is: 1
the Fibonacci number is: 1
the Fibonacci number is: 2
the Fibonacci number is: 3
the Fibonacci number is: 5
the Fibonacci number is: 8
the Fibonacci number is: 13
the Fibonacci number is: 21
the Fibonacci number is: 34
%% Cell type:markdown id: tags:
#### Demo
We can rewrite this recursive function as a one-line Lambda function.
%% Cell type:code id: tags:
``` python
Fibonacci = lambda n: n if n <= 1 else Fibonacci(n-1) + Fibonacci(n-2)
for i in range(0,10):
for i in range(-3,10):
fib = Fibonacci(i)
print('the Fibonacci number is: {}'.format(fib))
```
%% Output
the Fibonacci number is: -3
the Fibonacci number is: -2
the Fibonacci number is: -1
the Fibonacci number is: 0
the Fibonacci number is: 1
the Fibonacci number is: 1
the Fibonacci number is: 2
the Fibonacci number is: 3
the Fibonacci number is: 5
the Fibonacci number is: 8
the Fibonacci number is: 13
the Fibonacci number is: 21
the Fibonacci number is: 34
......
%% Cell type:markdown id: tags:
---
### Exercise
Imagine we are a bank and need to establish a way for us to track the transactions with customers.
In a very simple scenario, we need an account for each customer, that has the following properties:
* It is associated with a customer with their name.
* For simplicity, we only consider euros (i.e. no cents, just integer values).
* Customers can put money into the account, withdraw money and ask for the balance.
* The bank does not offer credit or overdraft.
Implement this as a class.
First, think about why the above requirements are ambiguous and not sufficient (and then come up with a way to mitigate this.)
Then, think about which cases you need to consider in your implementation.
We need to consider:
* "name" is not detailed enough: Do we mean the full name, just the surname or the given name, any middle names? Also, depending on the culture, the concept of a name may be different from what we expect in our western world.
For simplicity we just go with the surname as a single identifier. Obviously, this will create problems down the line, as many people share the same surname.
* We do not have any specification for gender assignments. Ignore for now.
* When a withdrawal is made, we need to check that:
* There are sufficient funds in the account
* The withdrawal is a positive integer number
* We can only withdraw the full value of the balance at most, since there is no overdraft/credit
* When depositing money, we need to check that:
* The deposit is a positive integer number.
%% Cell type:code id: tags:
``` python
class Account():
def __init__(self, name, balance = 0):
self._name = name
# what can we improve in the following line?
self._balance = int(balance)
if self._balance < 0:
self._balance = 0
def balance(self):
return self._balance
def name(self):
return self._name
def deposit(self, amount):
if type(amount) == int and amount > 0:
self._balance = self._balance + amount
else:
print('Invalid deposit')
def withdrawal(self, amount):
if type(amount) == int and amount > 0 and amount < self._balance:
self._balance = self._balance - amount
return self._balance
else:
print('Invalid withdrawal')
return None
```
%% Cell type:code id: tags:
``` python
account = Account('Smith')
print('Opened account for {}'.format(account.name()))
print('Current balance: {}'.format(account.balance()))
# make a deposit
account.deposit(10)
print('Current balance: {}'.format(account.balance()))
# try to make an invalid deposit -- should produce an error message
account.deposit(-10)
# try to make an invalid deposit -- should produce an error message
account.deposit(10.5)
# try to make an invalid deposit -- should produce an error message
account.deposit('ten')
# withdraw some money
my_money = account.withdrawal(5)
print('I have now {} euros in my hand'.format(my_money))
print('Current balance: {}'.format(account.balance()))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal(-10)
print('I have now {} euros in my hand'.format(my_money))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal(10.5)
print('I have now {} euros in my hand'.format(my_money))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal('ten')
print('I have now {} euros in my hand'.format(my_money))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal(500)
print('I have now {} euros in my hand'.format(my_money))
```
%% Output
Opened account for Smith
Current balance: 0
Current balance: 10
Invalid deposit
Invalid deposit
Invalid deposit
I have now 5 euros in my hand
Current balance: 5
Invalid withdrawal
I have now None euros in my hand
Invalid withdrawal
I have now None euros in my hand
Invalid withdrawal
I have now None euros in my hand
Invalid withdrawal
I have now None euros in my hand
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment