|
@@ -1,11 +1,431 @@ |
|
|
% Programmation avec Python (chapitre 13) |
|
|
% Programmation avec Python (chapitre 13) |
|
|
% Dimitri Merejkowsky |
|
|
% Dimitri Merejkowsky |
|
|
|
|
|
|
|
|
|
|
|
# |
|
|
|
|
|
|
|
|
\center \huge Rappels |
|
|
\center \huge Rappels |
|
|
|
|
|
|
|
|
* venv |
|
|
|
|
|
* pytest, assert, TDD |
|
|
|
|
|
|
|
|
# Virtualenvs |
|
|
|
|
|
|
|
|
|
|
|
* Création: |
|
|
|
|
|
|
|
|
|
|
|
``` |
|
|
|
|
|
$ python3 -m venv /path/to/foo |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
* Utilisation: |
|
|
|
|
|
|
|
|
|
|
|
``` |
|
|
|
|
|
$ /path/to/foo/bin/pip install requests |
|
|
|
|
|
# ou |
|
|
|
|
|
$ source /path/to/foo/bin/activate |
|
|
|
|
|
(foo) $ pip install requests |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Tests |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
# foo.py |
|
|
|
|
|
def add_3(x): |
|
|
|
|
|
return x + 3 |
|
|
|
|
|
|
|
|
|
|
|
# test_foo.py |
|
|
|
|
|
import foo |
|
|
|
|
|
|
|
|
|
|
|
def test_add_3(): |
|
|
|
|
|
result = foo.add_3(2) |
|
|
|
|
|
assert result == 5 |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# pytest |
|
|
|
|
|
|
|
|
|
|
|
``` |
|
|
|
|
|
$ pytest test_foo.py |
|
|
|
|
|
============== test session starts ================= |
|
|
|
|
|
test_foo.py . [100%] |
|
|
|
|
|
=========== 1 passed in 0.01 seconds =============== |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# TDD |
|
|
|
|
|
|
|
|
|
|
|
* Une discipline |
|
|
|
|
|
* 4 règles |
|
|
|
|
|
* Un cycle |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Règles |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* Règle 1 : Il est interdit d'écrire du code de production, *sauf* si c'est pour faire passer un test qui |
|
|
|
|
|
a échoué. |
|
|
|
|
|
* Règle 2 : Il est interdit d'écrire plus de code que celui qui est nécessaire pour provoquer une erreur |
|
|
|
|
|
dans les tests (n'importe quelle erreur) |
|
|
|
|
|
* Règle 3 : Il est interdit d'écrire plus de code que celui qui est nécessaire pour faire passer |
|
|
|
|
|
un test qui a échoué |
|
|
|
|
|
* Règle 4 : Une fois que tous les tests passent, il est interdit de modifier le code sans s'arrêter |
|
|
|
|
|
pour considérer la possibilité d'un refactoring. |
|
|
|
|
|
|
|
|
|
|
|
# Cycle |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
* RED: on écrit un test qui échoue |
|
|
|
|
|
* GREEN: on fait passer le test |
|
|
|
|
|
* REFACTOR: on refactor le code de production et le code de test |
|
|
|
|
|
|
|
|
|
|
|
# |
|
|
|
|
|
|
|
|
|
|
|
\center \huge Générateurs |
|
|
|
|
|
|
|
|
|
|
|
\vfill |
|
|
|
|
|
|
|
|
|
|
|
\normalsize Ces objects qui sont "presque" des listes ... |
|
|
|
|
|
|
|
|
|
|
|
# Retour sur les boucles |
|
|
|
|
|
|
|
|
|
|
|
Itération sur une liste |
|
|
|
|
|
```python |
|
|
|
|
|
for i in [1, 2, 3]: |
|
|
|
|
|
print(i) |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Clés d'un dictionnaire |
|
|
|
|
|
```python |
|
|
|
|
|
for key in {"a": 1, "b": 2, "c": 3}.keys(): |
|
|
|
|
|
print(key) |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Valeurs d'un dictionnaire |
|
|
|
|
|
```python |
|
|
|
|
|
for value in {"a": 1, "b": 2, "c": 3}.values(): |
|
|
|
|
|
print(key) |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Pas des listes |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> my_dict = {"a": 1, "b": 2, "c": 3} |
|
|
|
|
|
>>> my_dict.keys() |
|
|
|
|
|
dict_keys(['a', 'b', 'c']) |
|
|
|
|
|
>>> my_dict.keys()[0] |
|
|
|
|
|
TypeError: 'dict_keys' object is not subscriptable |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Vues |
|
|
|
|
|
|
|
|
|
|
|
En fait, `.keys()`, `.values()`, `.items()` renvoient des *vues*. |
|
|
|
|
|
|
|
|
|
|
|
Elles changent quand le dictionnaire changent, mais on ne peut pas |
|
|
|
|
|
s'en servir pour changer la taille du dictionnaire. |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> my_dict = {"a": 1, "b": 2, "c": 3} |
|
|
|
|
|
>>> for k in my_dict.keys(): |
|
|
|
|
|
>>> if k == "a": |
|
|
|
|
|
>>> del my_dict[k] |
|
|
|
|
|
RuntimeError: dictionary changed size during iteration |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Vues (2) |
|
|
|
|
|
|
|
|
|
|
|
Si vraiment vous en avez besoin, vous pouver les convertir en liste |
|
|
|
|
|
avec `list()`. |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> my_dict = {"a": 1, "b": 2, "c": 3} |
|
|
|
|
|
>>> for k in list(my_dict.keys()): |
|
|
|
|
|
>>> if k == "a": |
|
|
|
|
|
>>> del my_dict[k] |
|
|
|
|
|
>>> my_dict |
|
|
|
|
|
{"b": 2, "c": 3} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Listes par compréhension |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
# 1 |
|
|
|
|
|
sequence = ["a", "b", "c"] |
|
|
|
|
|
new_sequence = [] |
|
|
|
|
|
for element in sequence: |
|
|
|
|
|
new_sequence.append(element.upper()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 2 |
|
|
|
|
|
sequence = ["a", "b", "c"] |
|
|
|
|
|
new_sequence = [element.upper() for element in sequence] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Listes par compréhension (2) |
|
|
|
|
|
|
|
|
|
|
|
On peut transformer la séquence originale: |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> sequence = [element.upper() for element in sequence] |
|
|
|
|
|
>>> sequence |
|
|
|
|
|
["A", "B", "C"] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
\vfill |
|
|
|
|
|
|
|
|
|
|
|
On peut changer le type: |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> as_strings = ["1", "2", "3"] |
|
|
|
|
|
>>> numbers = [int(x) for x in as_strings] |
|
|
|
|
|
>>> numbers |
|
|
|
|
|
[1, 2, 3] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Filtrer une liste |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> numbers = [1, 2, 3, 4] |
|
|
|
|
|
>>> odds = [x for x in numbers if x % 2 == 1] |
|
|
|
|
|
>>> odds |
|
|
|
|
|
[1, 3] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
\vfill |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> numbers = [1, 2, 3, 4] |
|
|
|
|
|
>>> odds_squared = [x*x for x in numbers if x % 2 == 1] |
|
|
|
|
|
>>> odds_squared |
|
|
|
|
|
[1, 9] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Dictionnaires et ensembles par compréhension |
|
|
|
|
|
|
|
|
|
|
|
Même principe: |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> scores = [("bob", 2), ("alice", 3)] |
|
|
|
|
|
>>> my_dict = {t[0]:t[1] for t in scores} |
|
|
|
|
|
>>> my_dict |
|
|
|
|
|
{"bob": 2, "alice": 3} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
\vfill |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> numbers = [-1, 2, -3, 3] |
|
|
|
|
|
>>> numbers = {abs(x) for x in numbers} |
|
|
|
|
|
>>> numbers |
|
|
|
|
|
{1, 2, 3} |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Avantage des compréhensions |
|
|
|
|
|
|
|
|
|
|
|
* Code plus succint |
|
|
|
|
|
* Code plus rapide :) |
|
|
|
|
|
* Code "idiomatique" |
|
|
|
|
|
|
|
|
|
|
|
# Apparté: Python est gourmand |
|
|
|
|
|
|
|
|
|
|
|
Python évalue d'habitude de manière "gourmande": |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
def foo(): |
|
|
|
|
|
.. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bar(): |
|
|
|
|
|
... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
x = foo(bar(y)) |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
On calcule d'abord `y`, puis `bar(y)` puis `foo(bar(y))` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Générateurs |
|
|
|
|
|
|
|
|
|
|
|
Concept: fournir les valeurs une par une, à la demande. |
|
|
|
|
|
|
|
|
|
|
|
Le code *à l'intérieur* des compréhensions est un générateur. |
|
|
|
|
|
|
|
|
|
|
|
On peut le voir si on utilise des parenthèses: |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> generator = (x for x in range(0, 3)) |
|
|
|
|
|
>>> generator |
|
|
|
|
|
<generator object <genexpr> at 0x7f654c272138> |
|
|
|
|
|
>>> list(generator) |
|
|
|
|
|
[1, 2, 3] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Les générateurs sont "feignants": ils ne calculent leur valeurs que quand |
|
|
|
|
|
c'est demandé |
|
|
|
|
|
|
|
|
|
|
|
# Les générateurs s'épuisent |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> generator = (x for x in range(0, 3)) |
|
|
|
|
|
>>> list(generator) |
|
|
|
|
|
[1, 2, 3] |
|
|
|
|
|
>>> list(generator) |
|
|
|
|
|
[] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# next() |
|
|
|
|
|
|
|
|
|
|
|
On peut aussi appeler `next()` sur un générateur, au lieu de `for ... in` |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> generator = (x for x in range(0, 2)) |
|
|
|
|
|
>>> next(generator) |
|
|
|
|
|
0 |
|
|
|
|
|
>>> next(generator) |
|
|
|
|
|
1 |
|
|
|
|
|
>>> next(generator) |
|
|
|
|
|
StopIteration |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Fabriquer un générateur à l'aide d'une classe |
|
|
|
|
|
|
|
|
|
|
|
Avec une méthode `__next__()` |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
class Squares: |
|
|
|
|
|
def __init__(self): |
|
|
|
|
|
self.value = 0 |
|
|
|
|
|
|
|
|
|
|
|
def __next__(self): |
|
|
|
|
|
self.value += 1 |
|
|
|
|
|
return self.value ** 2 |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> squares = Squares() |
|
|
|
|
|
>>> next(squares) |
|
|
|
|
|
1 |
|
|
|
|
|
>>> next(squares) |
|
|
|
|
|
4 |
|
|
|
|
|
>>> next(squares) |
|
|
|
|
|
9 |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Les générateurs ne sont pas des itérateurs |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> [x for x in s if x < 10] |
|
|
|
|
|
TypeError: 'Squares' object is not iterable |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
Pour faire `for in`, il faut un itérateur, en plus d'un générateur |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Fabriquer un itérateur à l'aide d'une classe |
|
|
|
|
|
|
|
|
|
|
|
Avec `__iter__` *et* `__next__`: |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
class SquaresLessThan: |
|
|
|
|
|
def __init__(self, stop_value): |
|
|
|
|
|
self.value = 0 |
|
|
|
|
|
self.stop_value = stop_value |
|
|
|
|
|
|
|
|
|
|
|
def __iter__(self): |
|
|
|
|
|
return self |
|
|
|
|
|
|
|
|
|
|
|
def __next__(self): |
|
|
|
|
|
self.value += 1 |
|
|
|
|
|
res = self.value ** 2 |
|
|
|
|
|
if res > self.stop_value: |
|
|
|
|
|
raise StopIteration() |
|
|
|
|
|
return res |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Fabriquer un itérateur à l'aide d'une classe |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
>>> squares = SquaresLessThan(100) |
|
|
|
|
|
>>> [x for x in squares] |
|
|
|
|
|
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
On retrouve les propriétés des expressions génératrices: |
|
|
|
|
|
```python |
|
|
|
|
|
>>> squares = SquaresLessThan(100) |
|
|
|
|
|
>>> squares[2] |
|
|
|
|
|
TypeError: 'SquaresLessThan' object is not subscriptable |
|
|
|
|
|
>>> squares = SquaresLessThan(100) |
|
|
|
|
|
>>> list(squares) |
|
|
|
|
|
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100] |
|
|
|
|
|
>>> list(squares) |
|
|
|
|
|
[] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# yield |
|
|
|
|
|
|
|
|
|
|
|
On peut aussi faire des générateurs avec `yield` |
|
|
|
|
|
pour mettre le générateur "en pause", et `return` pour |
|
|
|
|
|
terminer: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
def squares(max_value): |
|
|
|
|
|
x = 1 |
|
|
|
|
|
res = 1 |
|
|
|
|
|
while res <= max_value: |
|
|
|
|
|
yield res |
|
|
|
|
|
x += 1 |
|
|
|
|
|
res = x * x |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
>>> s = squares(100) |
|
|
|
|
|
>>> [x for x in s] |
|
|
|
|
|
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# Combiner avec une classe génératrices |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
class Squares: |
|
|
|
|
|
def __init__(self): |
|
|
|
|
|
self.value = 0 |
|
|
|
|
|
|
|
|
|
|
|
def __next__(self): |
|
|
|
|
|
self.value += 1 |
|
|
|
|
|
return self.value ** 2 |
|
|
|
|
|
|
|
|
|
|
|
def squares_less_than(max_value): |
|
|
|
|
|
squares = Squares() |
|
|
|
|
|
while True: |
|
|
|
|
|
x = next(squares) |
|
|
|
|
|
if x > max_value: |
|
|
|
|
|
return |
|
|
|
|
|
yield x |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
|
|
|
# yield from |
|
|
|
|
|
|
|
|
|
|
|
On peut aussi chaîner les fonctions génératrices |
|
|
|
|
|
|
|
|
|
|
|
```python |
|
|
|
|
|
def integers(max_value): |
|
|
|
|
|
x = 0 |
|
|
|
|
|
while x < max_value: |
|
|
|
|
|
yield x |
|
|
|
|
|
x += 1 |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def zero_to_five(): |
|
|
|
|
|
yield from integers(5) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>>> s = zero_to_five() |
|
|
|
|
|
>>> [x for x in s] |
|
|
|
|
|
[0, 1, 2, 3, 4] |
|
|
|
|
|
``` |
|
|
|
|
|
|
|
|
# |
|
|
# |
|
|
|
|
|
|
|
|