| @@ -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] | |||||
| ``` | |||||
| # | # | ||||