Skip to content
Snippets Groups Projects

Corrections - Tom

Merged Tom Reclik requested to merge tom into master
1 file
+ 2
2
Compare changes
  • Side-by-side
  • Inline
%% Cell type:markdown id: tags:
# Iterators
We have previously seen that we can iterate over items in, for example, a dictionary or a list.
For example:
%% Cell type:code id: tags:
``` python
my_list = ['cat', 'dog', 'bird']
for item in my_list:
print(item)
```
%% Output
cat
dog
bird
%% Cell type:markdown id: tags:
This loop iterates over all elements in the list, gives us access to each element in turn and then stops once we reach the end of the list.
We could do this manually by creating an *iterator* and then use this to traverse the list, until no more elements are left over and an ```StopIteration``` exception is raised.
We define the iterator with the keyword ```my_iter = iter(my_object)``` and then proceed to the next item with ```next(my_iter)```.
%% Cell type:code id: tags:
``` python
my_list = ['cat', 'dog', 'bird']
my_iter = iter(my_list)
try:
while True:
print(next(my_iter))
except StopIteration as e:
print('Reached the end of the list {}'.format(e))
```
%% Output
cat
dog
bird
Reached the end of the list
%% Cell type:markdown id: tags:
Obviously, this is quite a horrible way to loop over a list (though some programming languages are not far off from doing this as the default way...)
One situation where we need to think about iterators is when we need to define a class that we can iterate over.
Then wen need to implement the "magic" (or dunder) functions ```__iter__()``` and ```__next__()```. Remember that the double underscores before and after the keywords indicate that we should not call these methods ourselves. \
Then we need to implement the "magic" (or dunder) functions ```__iter__()``` and ```__next__()```. Remember that the double underscores before and after the keywords indicate that we should not call these methods ourselves. \
For example, you may need to develop a more fancy list, a counter, an even more powerful dictionary, ...
%% Cell type:code id: tags:
``` python
class Counter:
def __init__(self, start, stop):
# number: our counter we want to iterate over
self.number = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
# check if we have reached the largest number we want to run over
if self.number > self.stop:
raise StopIteration
else:
current_number = self.number
self.number = self.number + 1
return current_number
my_counter = Counter(0, 5)
for counter in my_counter:
print('Value of the counter is now: {}'.format(counter))
```
%% Output
Value of the counter is now: 0
Value of the counter is now: 1
Value of the counter is now: 2
Value of the counter is now: 3
Value of the counter is now: 4
Value of the counter is now: 5
%% Cell type:markdown id: tags:
# Generators
Generators in python are special functions that remember their state each time you call them. Remeber that with the "normal" function we have seen earlier, we call the function, do our computations or other actions, maybe define local variables, etc. We may return the result of what the function does - but then the fuction is "forgotten".
In some cases, we may want to remember the state of the function. For example, we could read from a very large file: If it's very large, we cannot keep it in memory, but need to parse the content one line at the time. However, it would be very cumbersome to open a file, read one line, remember which line we have read, close the file, open the file, jump to the appropriate place, etc. \
Another use-case is we want to compute a long list of elements - but we do not know how many to start with. We could do this with a ```for``` or ```while``` loop (as we have done previously) and define the start and stop conditions.
Generators provide a neat way to do this without having to decide on start or stop condition in the function and, instead, focus on the function itself.
Generators are defined very similarly to normal functions, the main difference is the keyword ```yield```. When we encounter the ```yield``` statement, the execution of the function is stopped, we return a generator object (instead of the value), and the current state of the function is kept
**Example** \
Infinite sum - we want to obtain the sum of all integers until we decide to stop.
%% Cell type:code id: tags:
``` python
def my_infinite_sum():
sum = 0
while True:
yield sum
sum = sum + 1
# Then we can use this and print a few elements
my_generator = my_infinite_sum()
print(next(my_generator))
print(next(my_generator))
print(next(my_generator))
# we can also loop over this
for i in range(0, 5):
print(next(my_generator))
```
%% Output
0
1
2
3
4
5
6
7
%% Cell type:markdown id: tags:
We can also introduce a stop criterion that prevents us from reaching the ```yield``` statement. Then, as with the iterators above, a ```StopIteration``` exception to signal the end.
%% Cell type:code id: tags:
``` python
def my_infinite_sum(stop):
sum = 0
while sum < stop:
yield sum
sum = sum + 1
my_generator = my_infinite_sum(5)
for i in my_generator:
print(i)
```
%% Output
0
1
2
3
4
%% Cell type:markdown id: tags:
***Exercise***
Rewrite the function for the Fibonacci series as a generator.
%% Cell type:code id: tags:
``` python
def fibonacci():
# ... your code here ...
my_generator = fibonacci()
i = 0
while i < 10:
print('Next Fibonacci number {}'.format(next(my_generator)))
i = i+1
```
%% Cell type:markdown id: tags:
You should obtain the output:
```
Next Fibonacci number 0
Next Fibonacci number 1
Next Fibonacci number 1
Next Fibonacci number 2
Next Fibonacci number 3
Next Fibonacci number 5
Next Fibonacci number 8
Next Fibonacci number 13
Next Fibonacci number 21
Next Fibonacci number 34
```
While we have not avoided writing more code using the generator for the Fibonacci series, we have now separated the computation from the loop.
This allows us to further modularise the code and make it more flexible - and easier to debug. Also, conceptionally we have separted the actual computation from the way we use it.
This allows us to further modularise the code and make it more flexible - and easier to debug. Also, conceptionally we have separated the actual computation from the way we use it.
%% Cell type:markdown id: tags:
# Decorators
Decorators are "higher-level functions" - they are functions that operate on functions. \
This allows them to change the behaviour of the function without modifying the function itself.
There are many situations where this might be useful. For example, you might want to add additional print statements for debugging purpuses without clogging up your code, check the type of variables you pass to the function without changing the function itself, you might want to time how long the execution of a function takes, how much memory it consumes, etc.
Besides, there are software packages that, for example, speed up your code without you having to modify it.
The general syntax is:
```
def my_decorator(func):
def wrapper(*args, **kwargs):
# do something before we call the function
result = func(*args, **kwargs)
# do something after the function returns
return result
return wrapper
```
We note:
* We define the decorator as an outer function that takes the function we want to manipulate as an argument.
* The constructs ```*args, **kwargs``` pass the arguments through the decorator to the function. Remeber that we do not know how many and which arguments we might have, hence we have to use the construct with the single and double asterix.
%% Cell type:code id: tags:
``` python
# define the decorator
def my_decorator(func):
def wrapper(*args, **kwargs):
print('Before the function starts')
result = func(*args, **kwargs)
print('After the function ends')
return result
return wrapper
```
%% Cell type:code id: tags:
``` python
# define the function
def sum(a,b):
return a + b
# execute the function normally:
c = sum(1,2)
print('The sum is {}'.format(c))
```
%% Output
The sum is 3
%% Cell type:markdown id: tags:
Now we use the decorator. There are two ways of calling the decorator:
* We can "wrap" the function by "replacing" it with the call to the decorator with the function as an argument
* We use the ```@``` symbol with the name of the decorator when we define the function.
The first approach is a little "clunky" as it adds a bit of code...
%% Cell type:code id: tags:
``` python
# Method 1
# define the function
def sum(a,b):
return a + b
sum = my_decorator(sum)
c = sum(1,2)
print('The sum is {}'.format(c))
```
%% Output
Before the function starts
After the function ends
The sum is 3
%% Cell type:code id: tags:
``` python
# Method 2
@my_decorator
def sum(a,b):
return a + b
c = sum(1,2)
print('The sum is {}'.format(c))
```
%% Output
Before the function starts
After the function ends
The sum is 3
%% Cell type:markdown id: tags:
Here you can see that we modify the behaviour of the function by "just" adding the decorator which has been defined elsewhere.
We do not modify the function itself, the only difference is that we use the ```@my_decorator```.
This can lead to interesting behaviour...
***Exercise***\
Write a decorator for the sum that intercepts the result and adds 10.
%% Cell type:code id: tags:
``` python
# ... your code here ...
```
Loading