├── .gitignore ├── LICENCE.md ├── Makefile ├── README.md ├── add_title_level.py ├── gen_archive.py ├── gen_titles.py ├── logo_cours.png ├── src ├── 0-presentation.md ├── 1-starters │ ├── 0-startings.md │ ├── 1-containers │ │ ├── 0-containers.md │ │ ├── 1-contains.md │ │ ├── 2-len.md │ │ ├── 3-indexables.md │ │ ├── 4-slices.md │ │ ├── 5-collections.md │ │ ├── 6-tp.md │ │ └── x-conclusion.md │ ├── 2-iterables │ │ ├── 0-iterables.md │ │ ├── 1-for.md │ │ ├── 2-iterables.md │ │ ├── 3-itertools.md │ │ ├── 4-unpacking.md │ │ ├── 5-tp.md │ │ └── x-conclusion.md │ └── 3-mutables-hashables │ │ ├── 0-mutables-hashables.md │ │ ├── 1-mutables.md │ │ ├── 2-egality-identity.md │ │ ├── 3-hashables.md │ │ ├── 4-tp.md │ │ └── x-conclusion.md ├── 2-functions │ ├── 0-functions.md │ ├── 1-callables │ │ ├── 0-callables.md │ │ ├── 1-functions-classes-lambdas.md │ │ ├── 2-parameters.md │ │ ├── 3-call.md │ │ ├── 4-callables.md │ │ ├── 5-operator-functools.md │ │ ├── 6-tp.md │ │ └── x-conclusion.md │ ├── 2-annotations-signatures │ │ ├── 0-annotations-signatures.md │ │ ├── 1-annotations.md │ │ ├── 2-inspect.md │ │ ├── 3-signatures.md │ │ ├── 4-tp.md │ │ └── x-conclusion.md │ └── 3-decorators │ │ ├── 0-decorators.md │ │ ├── 1-deco.md │ │ ├── 2-parameterize.md │ │ ├── 3-wraps.md │ │ ├── 4-tp.md │ │ └── x-conclusion.md ├── 3-further │ ├── 0-further.md │ ├── 1-generators │ │ ├── 0-generators.md │ │ ├── 1-generator.md │ │ ├── 2-send.md │ │ ├── 3-methods.md │ │ ├── 4-yield-from.md │ │ ├── 5-comprehension.md │ │ ├── 6-list-generators.md │ │ ├── 7-tp.md │ │ └── x-conclusion.md │ ├── 2-context-managers │ │ ├── 0-context-managers.md │ │ ├── 1-with.md │ │ ├── 2-open.md │ │ ├── 3-internal.md │ │ ├── 4-contextlib.md │ │ ├── 5-reusability.md │ │ ├── 6-tp.md │ │ └── x-conclusion.md │ └── 3-accessors-descriptors │ │ ├── 0-accessors-descriptors.md │ │ ├── 1-accessors.md │ │ ├── 2-descriptors.md │ │ ├── 3-properties.md │ │ ├── 4-methods.md │ │ ├── 5-tp.md │ │ └── x-conclusion.md ├── 4-classes │ ├── 0-classes.md │ ├── 1-types │ │ ├── 0-types.md │ │ ├── 1-instance-class-metaclass.md │ │ ├── 2-new.md │ │ ├── 3-inheritance-parameters.md │ │ ├── 4-tp.md │ │ └── x-conclusion.md │ ├── 2-metaclasses │ │ ├── 0-metaclasses.md │ │ ├── 1-type.md │ │ ├── 2-metaclass.md │ │ ├── 3-function-metaclass.md │ │ ├── 4-tp.md │ │ └── x-conclusion.md │ └── 3-abstract-classes │ │ ├── 0-abstract-classes.md │ │ ├── 1-abc.md │ │ ├── 2-isinstance.md │ │ ├── 3-issubclass.md │ │ ├── 4-collections.md │ │ ├── 5-tp.md │ │ └── x-conclusion.md ├── 5-exercises │ ├── 0-exercises.md │ ├── 2-3-decorators │ │ ├── 0-decorators.md │ │ ├── 1-check-type.md │ │ ├── 2-memoize.md │ │ ├── 3-singledispatch.md │ │ └── 4-tail-rec.md │ ├── 3-2-context-managers │ │ ├── 0-context-managers.md │ │ ├── 1-changedir.md │ │ ├── 2-contextmanager.md │ │ └── 3-suppress.md │ ├── 3-3-accessors-descriptors │ │ ├── 0-accesors-descriptors.md │ │ └── 1-property.md │ └── 4-2-metaclasses │ │ ├── 0-metaclasses.md │ │ ├── 1-lazy-evaluation.md │ │ └── 2-immutable.md └── x-conclusion.md └── title.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pdf 3 | *.zip 4 | manifest.json 5 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PDF = cours_python_avance.pdf 2 | ZIP = cours_python_avance.zip 3 | SRC = $(shell find src -name "*.md" | sort -n) 4 | 5 | FLAGS = --top-level-division=part --toc 6 | 7 | GEN = $(PDF) $(ZIP) 8 | 9 | $(PDF): title.md $(SRC) 10 | pandoc -V lang=fr -V geometry:margin=1in -V colorlinks=true $^ -o $@ $(FLAGS) 11 | 12 | $(ZIP): $(SRC) 13 | ./gen_archive.py $@ $^ 14 | 15 | clean: 16 | rm -f $(GEN) 17 | 18 | re: clean $(GEN) 19 | 20 | .PHONY: clean re 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notions de Python avancées 2 | 3 | Découvrez Python plus en profondeur. 4 | 5 | ## Lecture 6 | 7 | Le tutoriel est publié sur *Zeste de savoir* à l'adresse suivante : . 8 | 9 | L'ensemble du cours est rédigé en *Markdown* est peut donc être consulté directement depuis *Github* en parcourant le dossier [`src/`](src/). 10 | 11 | ## Compilation 12 | 13 | Le *Makefile* présente des règles de compilation vers deux formats : 14 | 15 | * *PDF* : `make cours_python_avance.pdf` (nécessite *pandoc* et *latex*) ; 16 | * Archive *ZIP* *Zeste de savoir* : `make cours_python_avance.zip` (nécessite *python*). 17 | 18 | ![Logo](logo_cours.png) 19 | 20 | ## Licence 21 | 22 | Cours sous licence [CC BY-SA](https://creativecommons.org/licenses/by-sa/4.0/deed.fr). 23 | -------------------------------------------------------------------------------- /add_title_level.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import os 5 | 6 | files = [] 7 | 8 | for directory, _, filenames in os.walk('src'): 9 | for filename in filenames: 10 | files.append(os.path.join(directory, filename)) 11 | 12 | code = False 13 | def handle_line(line): 14 | global code 15 | if re.match(r'```', line): 16 | code = not code 17 | return line 18 | if code: 19 | return line 20 | 21 | m = re.match(r'#+ .+$', line) 22 | if m: 23 | line = '#' + line 24 | return line 25 | 26 | for filename in files: 27 | with open(filename, 'r') as f: 28 | lines = f.readlines() 29 | lines = [handle_line(l) for l in lines] 30 | with open(filename, 'w') as f: 31 | f.write(''.join(lines)) 32 | -------------------------------------------------------------------------------- /gen_archive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import sys 5 | import os.path 6 | import re 7 | from collections import OrderedDict 8 | from zipfile import ZipFile 9 | 10 | archive_name = sys.argv[1] 11 | sections = sys.argv[2:] 12 | 13 | manifest = { 14 | 'slug': 'notions-de-python-avancees', 15 | 'title': 'Notions de Python avancées', 16 | 'version': 2, 17 | 'description': 'Découvrez Python plus en profondeur', 18 | 'type': 'TUTORIAL', 19 | 'licence': 'CC BY-SA' 20 | } 21 | 22 | from collections import OrderedDict 23 | container = OrderedDict() 24 | # Number of nested levels 25 | document_depth = 0 26 | trans = str.maketrans('','','#*_`\n') 27 | 28 | # Split filenames into a dict that represent file hierarchy 29 | for section in sections: 30 | *path, filename = section.split('/') 31 | document_depth = max(document_depth, len(path)) 32 | parent = container 33 | for p in path: 34 | parent = parent.setdefault(p, OrderedDict()) 35 | parent[filename] = section 36 | 37 | # Prefix for 1st-level titles 38 | title_prefix = '#' * (document_depth + 1) 39 | 40 | # Rewrite titles of files 41 | def rewrite_titles(f): 42 | code = False 43 | for i, line in enumerate(f): 44 | if line.startswith('```'): 45 | code = not code 46 | elif not code and line.startswith('#'): 47 | if line.startswith(title_prefix): 48 | line = line[document_depth:] 49 | else: 50 | print('Warning with title {!r} in file {}:{}'.format(line, f.name, i)) 51 | yield line 52 | 53 | # Write a file in the archive 54 | def write_file(archive, filename): 55 | with open(filename, 'r') as f: 56 | title = next(f).translate(trans).strip() 57 | content = ''.join(rewrite_titles(f)) 58 | archive.writestr(filename, content) 59 | return title 60 | 61 | # Recursively construct a document 62 | def make_document(archive, obj, name=None): 63 | if isinstance(obj, str): 64 | extract = {'object': 'extract', 'text': obj} 65 | extract['slug'], _ = os.path.splitext(name) 66 | extract['title'] = write_file(archive, obj) 67 | return extract 68 | container = {'object': 'container'} 69 | if name: 70 | container['slug'] = name 71 | keys = list(obj.keys()) 72 | if keys[0].startswith('0-'): 73 | container['introduction'] = obj.pop(keys[0]) 74 | container['title'] = write_file(archive, container['introduction']) 75 | if keys[-1].startswith('x-'): 76 | container['conclusion'] = obj.pop(keys[-1]) 77 | write_file(archive, container['conclusion']) 78 | if obj: 79 | container['children'] = [make_document(archive, child, name) for name, child in obj.items()] 80 | return container 81 | 82 | with ZipFile(archive_name, 'w') as archive: 83 | document = make_document(archive, container['src']) 84 | document.update(manifest) 85 | archive.writestr('manifest.json', json.dumps(document, indent=4)) 86 | -------------------------------------------------------------------------------- /gen_titles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import os 5 | 6 | files = [] 7 | 8 | for directory, _, filenames in os.walk('src'): 9 | for filename in filenames: 10 | files.append(os.path.join(directory, filename)) 11 | 12 | def sort_key(path): 13 | return tuple(int(re.sub(r'[^0-9]', '', x) or 0) for x in path.split('/')) 14 | 15 | files.sort(key=sort_key) 16 | 17 | 18 | print('% Notions de Python avancées') 19 | 20 | for filename in files: 21 | with open(filename, 'r') as f: 22 | code = False 23 | for line in f: 24 | if re.match(r'```', line): 25 | code = not code 26 | continue 27 | if code: 28 | continue 29 | 30 | m = re.match(r'(#+) (.+)$', line) 31 | if m: 32 | tabs = len(m.group(1)) - 1 33 | if not tabs: 34 | print() 35 | print('{}- {}'.format(' ' * tabs, m.group(2))) 36 | -------------------------------------------------------------------------------- /logo_cours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/entwanne/cours_python_avance/6f1771773032b696c9ed99e7508cb08a273c9c47/logo_cours.png -------------------------------------------------------------------------------- /src/0-presentation.md: -------------------------------------------------------------------------------- 1 | ## Présentation 2 | 3 | Python est simple. 4 | 5 | C'est probablement ce que l'on vous a dit de nombreuses fois, et ce que vous avez constaté en apprenant et pratiquant ce langage. 6 | Mais derrière cette simplicité apparente existent un certain nombre de concepts plus complexes qui forment la puissance de ce langage. 7 | 8 | En Python, on s'intéresse plus au comportement des objets qu'à leur nature. 9 | Ainsi, l'interface des objets (c'est-à-dire l'ensemble de leurs attributs et méthodes) est quelque chose de très important, 10 | c'est entre autres ce qui les définit. 11 | 12 | En effet, une grande partie des outils du langage sont génériques — tels les appels de fonctions ou les boucles `for` — c'est-à-dire qu'ils peuvent s'appliquer à des types différents. 13 | Python demande simplement à ces types de respecter une interface en implémentant un certain nombre de méthodes spéciales. 14 | Ces interfaces et méthodes seront décrites dans le cours. 15 | 16 | Le pré-requis pour suivre ce tutoriel est de connaître Python, même à un niveau intermédiaire. 17 | Il est simplement nécessaire de savoir manipuler les structures du langage (conditions, boucles, fonctions), 18 | les types de base (nombres, chaînes de caractères, listes, dictionnaires), 19 | et d'avoir des notions de [programmation objet en Python](https://zestedesavoir.com/tutoriels/1253/la-programmation-orientee-objet-en-python/). 20 | Connaître le mécanisme des exceptions est un plus. 21 | 22 | Ce cours se divise en chapitres consacrés chacun à une spécificité du langage. 23 | Ces dernières ne devraient plus avoir de secret pour vous une fois la lecture terminée. 24 | 25 | Je tiens aussi à préciser que ce cours s'adresse principalement aux utilisateurs de Python 3, 26 | et n'est pas valable pour les versions de Python inférieures. 27 | -------------------------------------------------------------------------------- /src/1-starters/0-startings.md: -------------------------------------------------------------------------------- 1 | # Mise en bouche 2 | 3 | ##### À table ! 4 | 5 | Quoi de mieux pour commencer qu'une petite mise en bouche ? 6 | 7 | Nous allons nous intéresser dans cette partie aux plus simples interfaces du langage. 8 | Celles qui nous permettent de confectionner des objets tels que les listes ou les dictionnaires. 9 | -------------------------------------------------------------------------------- /src/1-starters/1-containers/0-containers.md: -------------------------------------------------------------------------------- 1 | ## Conteneurs 2 | 3 | En Python, on appelle conteneur (*container*) un objet ayant vocation à en contenir d'autres, comme les chaînes de caractères, les listes, les ensembles ou les dictionnaires. 4 | 5 | Il existe plusieurs catégories de conteneurs, notamment celle des *subscriptables*. Ce nom barbare regroupe tous les objets sur lesquels l'opérateur `[]` peut être utilisé. 6 | L'ensemble des types cités dans le premier paragraphe sont *subscriptables*, à l'exception de l'ensemble (*set*), qui n'implémente pas l'opération `[]`. 7 | 8 | Les *subscriptables* se divisent en deux nouvelles catégories non exclusives : les *indexables* et les *sliceables*. Les premiers sont ceux pouvant être indexés avec des nombres entiers, les seconds pouvant l'être avec des `slice` (voir plus loin). 9 | 10 | On parle plus généralement de séquence quand un conteneur est *indexable* et *sliceable*. 11 | 12 | Une autre catégorie importante de conteneurs est formée par les *mappings* : il s'agit des objets qui associent des valeurs à des clefs, comme le font les dictionnaires. 13 | 14 | Une séquence et un *mapping* se caractérisent aussi par le fait qu'ils possèdent une taille, comme nous le verrons par la suite dans ce chapitre. 15 | -------------------------------------------------------------------------------- /src/1-starters/1-containers/1-contains.md: -------------------------------------------------------------------------------- 1 | ### Les conteneurs, c'est `in` 2 | 3 | Comme indiqué, les conteneurs sont donc des objets qui contiennent d'autres objets. 4 | Ils se caractérisent par l'opérateur `in` : `(0, 1, 2, 3)` étant un conteneur, il est possible de tester s'il contient telle ou telle valeur à l'aide de cet opérateur. 5 | 6 | ```python 7 | >>> 3 in (0, 1, 2, 3) 8 | True 9 | >>> 4 in (0, 1, 2, 3) 10 | False 11 | ``` 12 | 13 | Comment cela fonctionne ? C'est très simple. Comme pour de nombreux comportements, Python se base sur des méthodes spéciales des objets. 14 | Vous en connaissez déjà probablement, ce sont les méthodes dont les noms débutent et s'achèvent par `__`. 15 | 16 | Ici, l'opérateur `in` fait simplement appel à la méthode `__contains__` de l'objet, qui prend en paramètre l'opérande gauche, et retourne un booléen. 17 | 18 | ```python 19 | >>> 'o' in 'toto' 20 | True 21 | >>> 'toto'.__contains__('o') 22 | True 23 | ``` 24 | 25 | Il nous suffit ainsi d'implémenter cette méthode pour faire de notre objet un conteneur. 26 | 27 | ```python 28 | >>> class MyContainer: 29 | ... def __contains__(self, value): 30 | ... return value is not None # contient tout sauf None 31 | ... 32 | >>> 'salut' in MyContainer() 33 | True 34 | >>> 1.5 in MyContainer() 35 | True 36 | >>> None in MyContainer() 37 | False 38 | ``` 39 | -------------------------------------------------------------------------------- /src/1-starters/1-containers/2-len.md: -------------------------------------------------------------------------------- 1 | ### C'est pas la taille qui compte 2 | 3 | Un autre point commun partagé par de nombreux conteneurs est qu'ils possèdent une taille. 4 | C'est-à-dire qu'ils contiennent un nombre fini et connu d'éléments, et peuvent être passés en paramètre à la fonction `len` par exemple. 5 | 6 | ```python 7 | >>> len([1, 2, 3]) 8 | 3 9 | >>> len(MyContainer()) 10 | Traceback (most recent call last): 11 | File "", line 1, in 12 | TypeError: object of type 'MyContainer' has no len() 13 | ``` 14 | 15 | Comme pour l'opérateur `in`, la fonction `len` fait appel à une méthode spéciale de l'objet, `__len__`, qui ne prend ici aucun paramètre et doit retourner un nombre entier positif. 16 | 17 | ```python 18 | >>> len('toto') 19 | 4 20 | >>> 'toto'.__len__() 21 | 4 22 | ``` 23 | 24 | Nous pouvons donc aisément donner une taille à nos objets : 25 | 26 | ```python 27 | >>> class MySizeable: 28 | ... def __len__(self): 29 | ... return 18 30 | ... 31 | >>> len(MySizeable()) 32 | 18 33 | ``` 34 | 35 | Je vous invite à faire des essais en retournant d'autres valeurs (nombres négatifs, flottants, chaînes de caractères) pour observer le comportement. 36 | -------------------------------------------------------------------------------- /src/1-starters/1-containers/3-indexables.md: -------------------------------------------------------------------------------- 1 | ### Objets *indexables* 2 | 3 | Nous voilà bien avancés : nous savons mesurer la taille d'un objet, mais pas voir les éléments qu'il contient. 4 | 5 | L'accès aux éléments se fait via l'opérateur `[]`. De même que la modification et la suppression, quand celles-ci sont possibles (c'est-à-dire que l'objet est mutable). 6 | 7 | ```python 8 | >>> numbers = [4, 7, 6] 9 | >>> numbers[2] 10 | 6 11 | >>> numbers[1] = 5 12 | >>> numbers 13 | [4, 5, 6] 14 | >>> del numbers[0] 15 | >>> numbers 16 | [5, 6] 17 | ``` 18 | 19 | Le comportement interne est ici régi par 3 méthodes : `__getitem__`, `__setitem__`, et `__delitem__`. 20 | 21 | ```python 22 | >>> numbers = [4, 7, 6] 23 | >>> numbers.__getitem__(2) 24 | 6 25 | >>> numbers.__setitem__(1, 5) 26 | >>> numbers 27 | [4, 5, 6] 28 | >>> numbers.__delitem__(0) 29 | >>> numbers 30 | [5, 6] 31 | ``` 32 | 33 | Comme précédemment, nous pouvons donc implémenter ces méthodes dans un nouveau type. Nous allons ici nous contenter de faire un proxy autour d'une liste existante. 34 | 35 | Un proxy[^proxy] est un objet prévu pour se substituer à un autre, il doit donc répondre aux mêmes méthodes, de façon transparente. 36 | 37 | [^proxy]: 38 | 39 | ```python 40 | class MyList: 41 | def __init__(self, value=()): # Émulation du constructeur de list 42 | self.internal = list(value) 43 | 44 | def __len__(self): # Sera utile pour les tests 45 | return len(self.internal) 46 | 47 | def __getitem__(self, key): 48 | return self.internal[key] # Équivalent à return self.internal.__getitem__(key) 49 | 50 | def __setitem__(self, key, value): 51 | self.internal[key] = value 52 | 53 | def __delitem__(self, key): 54 | del self.internal[key] 55 | ``` 56 | 57 | Nous pouvons tester notre objet, celui-ci a bien le comportement voulu : 58 | 59 | ```python 60 | >>> numbers = MyList('123456') 61 | >>> len(numbers) 62 | 6 63 | >>> numbers[1] 64 | '2' 65 | >>> numbers[1] = '0' 66 | >>> numbers[1] 67 | '0' 68 | >>> del numbers[1] 69 | >>> len(numbers) 70 | 5 71 | >>> numbers[1] 72 | '3' 73 | ``` 74 | -------------------------------------------------------------------------------- /src/1-starters/1-containers/4-slices.md: -------------------------------------------------------------------------------- 1 | ### Les *slices* 2 | 3 | Les « slices » se traduisent en français par « tranches ». Cela signifie que l'on va prendre notre objet et le découper en morceaux. 4 | Par exemple, récupérer la première moitié d'une liste, ou cette même liste en ne conservant qu'un élément sur deux. 5 | 6 | Les *slices* sont une syntaxe particulière pour l'indexation, à l'aide du caractère `:` lors des appels à `[]`. 7 | 8 | ```python 9 | >>> letters = ('a', 'b', 'c', 'd', 'e', 'f') 10 | >>> letters[0:4] 11 | ('a', 'b', 'c', 'd') 12 | >>> letters[1:-2] 13 | ('b', 'c', 'd') 14 | >>> letters[::2] 15 | ('a', 'c', 'e') 16 | ``` 17 | 18 | Je pense que vous êtes déjà familier avec cette syntaxe. Le *slice* peut prendre jusqu'à 3 nombres : 19 | 20 | - Le premier est l'indice de départ (0 si omis) ; 21 | - Le second est l'indice de fin (fin de la liste si omis), l'élément correspondant à cet indice étant exclu ; 22 | - Le dernier est le pas, le nombre d'éléments passés à chaque itération (1 par défaut) ; 23 | 24 | Notons que les *slices* peuvent aussi servir pour la modification et la suppression : 25 | 26 | ```python 27 | >>> letters = ['a', 'b', 'c', 'd', 'e', 'f'] 28 | >>> letters[::2] = 'x', 'y', 'z' 29 | >>> letters 30 | ['x', 'b', 'y', 'd', 'z', 'f'] 31 | >>> del letters[0:3] 32 | >>> letters 33 | ['d', 'z', 'f'] 34 | ``` 35 | 36 | Une bonne chose pour nous est que, même avec les *slices*, ce sont les 3 mêmes méthodes `__getitem__`, `__setitem__` et `__delitem__` qui sont appelées lors des accès. 37 | Cela signifie que la classe `MyList` que nous venons d'implémenter est déjà compatible avec les *slices*. 38 | 39 | En fait, c'est simplement que le paramètre `key` passé ne représente pas un nombre, mais est un objet de type `slice` : 40 | 41 | ```python 42 | >>> s = slice(1, -1) 43 | >>> 'abcdef'[s] 44 | 'bcde' 45 | >>> 'abcdef'[slice(None, None, 2)] 46 | 'ace' 47 | ``` 48 | 49 | Comme vous le voyez, le slice se construit toujours de la même manière, avec 3 nombres pouvant être omis, ou précisés à `None` pour prendre leur valeur par défaut. 50 | 51 | L'objet ainsi construit contient 3 attributs : `start`, `stop`, et `step`. 52 | 53 | ```python 54 | >>> s = slice(1, 2, 3) 55 | >>> s.start 56 | 1 57 | >>> s.stop 58 | 2 59 | >>> s.step 60 | 3 61 | ``` 62 | 63 | Je vous conseille ce tutoriel de [**pascal.ortiz**](https://zestedesavoir.com/membres/voir/pascal.ortiz/) pour en savoir plus sur les slices : 64 | -------------------------------------------------------------------------------- /src/1-starters/1-containers/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Nous sommes maintenant à la fin du premier chapitre. En guise de conclusion, je vais vous fournir une liste de sources tirées de la documentation officielle permettant d'aller plus loin sur ces sujets. 4 | 5 | * La définition du terme séquence : 6 | * Celle du *mapping* : 7 | * Types de séquences : 8 | * Types standards : 9 | * Émuler les conteneurs : 10 | * Module `collections` : 11 | -------------------------------------------------------------------------------- /src/1-starters/2-iterables/0-iterables.md: -------------------------------------------------------------------------------- 1 | ## Itérables 2 | 3 | Un itérable est un objet dont on peut parcourir les valeurs, à l'aide d'un `for` par exemple. 4 | La liste que nous venons d'implémenter est un exemple d'itérable. 5 | 6 | Les types `str`, `tuple`, `list`, `dict` et `set` sont d'autres itérables bien connus. 7 | Un grand nombre d'outils Python que nous verrons par la suite travaillent avec des itérables, il est donc intéressant d'en tirer profit. 8 | 9 | L'objectif de ce chapitre va être de comprendre ce qu'est un itérable, et comment en implémenter un. 10 | -------------------------------------------------------------------------------- /src/1-starters/2-iterables/1-for.md: -------------------------------------------------------------------------------- 1 | ### `for` `for` lointain 2 | 3 | Les itérables et le mot-clef `for` sont intimement liés. C'est à partir de ce dernier que nous itérons sur les objets. 4 | 5 | Mais comment cela fonctionne en interne ? Je vous propose de regarder ça pas à pas, en nous aidant d'un objet de type `list`. 6 | 7 | ```python 8 | >>> numbers = [1, 2, 3, 4, 5] 9 | ``` 10 | 11 | La première opération réalisée par le `for` est d'appeler la fonction `iter` avec notre objet. 12 | `iter` retourne un itérateur. L'itérateur est l'objet qui va se déplacer le long de l'itérable. 13 | 14 | ```python 15 | >>> iter(numbers) 16 | 17 | ``` 18 | 19 | Puis, pas à pas, le `for` appelle `next` en lui précisant l'itérateur. 20 | `next` fait avancer l'itérateur et retourne la nouvelle valeur découverte à chaque pas. 21 | 22 | ```python 23 | >>> iterator = iter(numbers) 24 | >>> next(iterator) 25 | 1 26 | >>> next(iterator) 27 | 2 28 | >>> next(iterator) 29 | 3 30 | >>> next(iterator) 31 | 4 32 | >>> next(iterator) 33 | 5 34 | >>> next(iterator) 35 | Traceback (most recent call last): 36 | File "", line 1, in 37 | StopIteration 38 | ``` 39 | 40 | Qu'est-ce que ce `StopIteration` ? Il s'agit d'une exception, levée par l'itérateur quand il arrive à sa fin, qui signifie que nous en sommes arrivés au bout, et donc que la boucle doit cesser. `for` attrape cette exception pour nous, ce qui explique que nous ne la voyons pas survenir dans une boucle habituelle. 41 | 42 | Ainsi, le code suivant : 43 | 44 | ```python 45 | for number in numbers: 46 | print(number) 47 | ``` 48 | 49 | Peut se remplacer par celui-ci : 50 | 51 | ```python 52 | iterator = iter(numbers) 53 | while True: 54 | try: 55 | number = next(iterator) 56 | except StopIteration: 57 | break 58 | print(number) 59 | ``` 60 | 61 | En interne, `iter` fait habituellement appel à la méthode `__iter__` de l'itérable, et `next` à la méthode `__next__` de l'itérateur. 62 | Ces deux méthodes ne prennent aucun paramètre. Ainsi : 63 | 64 | - Un itérable est un objet possédant une méthode `__iter__`[^approximation_iter] retournant un itérateur ; 65 | - Un itérateur est un objet possédant une méthode `__next__` retournant la valeur suivante à chaque appel, et levant une exception de type `StopIteration` en fin de course. 66 | 67 | La [documentation Python](https://docs.python.org/3/glossary.html#term-iterator) indique aussi qu'un itérateur doit avoir une méthode `__iter__` où il se retourne lui-même, les itérateurs étant ainsi des itérables à part entièe. 68 | 69 | [^approximation_iter]: À une approximation près, comme détaillé dans « Le cas des indexables ». 70 | 71 | 72 | #### Le cas des indexables 73 | 74 | En début du chapitre, j'ai indiqué que notre liste `Deque` était aussi un itérable. Pourtant, nous ne lui avons pas implémenté de méthode `__iter__` permettant de la parcourir. 75 | 76 | Il s'agit en fait d'une particularité des indexables, et de la fonction `iter` qui est capable de créer un itérateur à partir de ces derniers. 77 | Cet itérateur se contentera d'appeler `__getitem__` sur notre objet avec des indices successifs, partant de 0 et continuant jusqu'à ce que la méthode lève une `IndexError`. 78 | 79 | Dans notre cas, ça nous évite donc d'implémenter nous-même `__iter__`, mais ça complexifie aussi les traitements. 80 | Souvenez-vous de notre méthode `__getitem__` : elle parcourt la liste jusqu'à l'élément voulu. 81 | 82 | Ainsi, pour accéder au premier maillon, on parcourt un élément, on en parcourt deux pour accéder au second, etc. 83 | Donc pour itérer sur une liste de 5 éléments, on va devoir parcourir `1 + 2 + 3 + 4 + 5` soit 15 maillons, là où 5 seraient suffisants. 84 | C'est pourquoi nous reviendrons sur `Deque` en fin de chapitre pour lui intégrer sa propre méthode `__iter__`. 85 | -------------------------------------------------------------------------------- /src/1-starters/2-iterables/2-iterables.md: -------------------------------------------------------------------------------- 1 | ### Utilisation des iterables 2 | 3 | #### Python et les itérables 4 | 5 | Ce concept d'itérateurs est utilisé par Python dans une grande partie des ses [builtins](https://docs.python.org/3/library/functions.html). Plutôt que de vous forcer à utiliser une liste, Python vous permet de fournir un objet itérable, pour `sum`, `max` ou `map` par exemple. 6 | 7 | Je vous propose de tester cela avec un itérable basique, qui nous permettra de réaliser un `range` simplifié. 8 | 9 | ```python 10 | class MyRange: 11 | def __init__(self, size): 12 | self.size = size 13 | 14 | def __iter__(self): 15 | return MyRangeIterator(self) 16 | 17 | class MyRangeIterator: 18 | def __init__(self, my_range): 19 | self.current = 0 20 | self.max = my_range.size 21 | 22 | def __iter__(self): 23 | return self 24 | 25 | def __next__(self): 26 | if self.current >= self.max: 27 | raise StopIteration 28 | ret = self.current 29 | self.current += 1 30 | return ret 31 | ``` 32 | 33 | Maintenant, testons notre objet, en essayant d'itérer dessus à l'aide d'un `for`. 34 | 35 | ```python 36 | >>> MyRange(5) 37 | <__main__.MyRange object at 0x7fcf3b0e8f28> 38 | >>> for i in MyRange(5): 39 | ... print(i) 40 | ... 41 | 0 42 | 1 43 | 2 44 | 3 45 | 4 46 | ``` 47 | 48 | Voilà pour l'itération, mais testons ensuite quelques autres *builtins* dont je parlais plus haut. 49 | 50 | ```python 51 | >>> sum(MyRange(5)) # sum réalise la somme de tous les éléments, soit 0 + 1 + 2 + 3 + 4 52 | 10 53 | >>> max(MyRange(5)) # max retourne la plus grande valeur 54 | 4 55 | >>> map(str, MyRange(5)) # Ici, map retournera chaque valeur convertie en str 56 | 57 | ``` 58 | 59 | Mmmh, que s'est-il passé ? En fait, `map` ne retourne pas une liste, mais un nouvel itérateur. Si nous voulons en voir le contenu, nous pouvons itérer dessus… ou plus simplement, convertir le résultat en liste : 60 | 61 | ```python 62 | >>> list(map(str, MyRange(5)) 63 | ['0', '1', '2', '3', '4'] 64 | ``` 65 | 66 | Vous l'aurez compris, `list` prend aussi n'importe quel itérable en argument, tout comme `zip` ou `str.join` par exemple. 67 | 68 | ```python 69 | >>> list(MyRange(5)) 70 | [0, 1, 2, 3, 4] 71 | >>> list(zip(MyRange(5), 'abcde')) # Les chaînes sont aussi des itérables 72 | [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e')] 73 | >>> ', '.join(map(str, MyRange(5))) 74 | '0, 1, 2, 3, 4' 75 | ``` 76 | 77 | J'en resterai là pour les exemples, sachez seulement que beaucoup de fonctions sont compatibles. Seules celles nécessitant des propriétés spécifiques de l'objet ne le seront pas par défaut, comme la fonction `reversed`. 78 | 79 | #### Retour sur `iter` 80 | 81 | Je voudrais ici revenir sur la fonction `iter`, qui crée un itérateur à partir d'un itérable. 82 | Sachez que ce n'est pas sa seule utilité. Elle peut aussi créer un itérateur à partir d'une fonction et d'une valeur de fin. 83 | C'est-à-dire que la fonction sera appelée tant que la valeur de fin n'a pas été retournée, par exemple : 84 | 85 | ```python 86 | >>> n = 0 87 | >>> def iter_func(): 88 | ... global n 89 | ... n += 1 90 | ... return n 91 | ... 92 | >>> for i in iter(iter_func, 10): 93 | ... print(i) 94 | ... 95 | 1 96 | 2 97 | 3 98 | 4 99 | 5 100 | 6 101 | 7 102 | 8 103 | 9 104 | ``` 105 | -------------------------------------------------------------------------------- /src/1-starters/2-iterables/3-itertools.md: -------------------------------------------------------------------------------- 1 | ### Utilisation avancée : le module `itertools` 2 | 3 | Nous l'avons vu, les itérables sont au cœur des fonctions élémentaires de Python. Je voudrais maintenant vous présenter un module qui vous sera propablement très utile : [`itertools`](https://docs.python.org/3/library/itertools.html). 4 | 5 | Ce module met à disposition de nombreux itérables plutôt variés, dont : 6 | 7 | - `chain(p, q, ...)` — Met bout à bout plusieurs itérables ; 8 | - `islice(p, start, stop, step)` — Fait un travail semblable aux slices, mais en travaillant avec des itérables (nul besoin de pouvoir indexer notre objet) ; 9 | - `combinations(p, r)` — Retourne toutes les combinaisons de `r` éléments possibles dans `p` ; 10 | - `zip_longest(p, q, ...)` — Similaire à `zip`, mais s'aligne sur l'itérable le plus grand plutôt que le plus petit (en permettant de spécifier une valeur de remplissage). 11 | 12 | ```python 13 | >>> import itertools 14 | >>> itertools.chain('abcd', [1, 2, 3]) 15 | 16 | >>> list(itertools.chain('abcd', [1, 2, 3])) 17 | ['a', 'b', 'c', 'd', 1, 2, 3] 18 | >>> list(itertools.islice(itertools.chain('abcd', [1, 2, 3]), 1, None, 2)) 19 | ['b', 'd', 2] 20 | >>> list(itertools.combinations('abc', 2)) 21 | [('a', 'b'), ('a', 'c'), ('b', 'c')] 22 | >>> list(itertools.zip_longest('abcd', [1, 2, 3])) 23 | [('a', 1), ('b', 2), ('c', 3), ('d', None)] 24 | ``` 25 | 26 | Je tiens enfin à attirer votre attention sur les [recettes (*recipes*)](https://docs.python.org/3/library/itertools.html#itertools-recipes), un ensemble d'exemples qui vous sont proposés mettant à profit les outils présents dans `itertools`. 27 | -------------------------------------------------------------------------------- /src/1-starters/2-iterables/4-unpacking.md: -------------------------------------------------------------------------------- 1 | ### L'*unpacking* 2 | 3 | Une fonctionnalité courante de Python, liée aux itérables, est celle de l'*unpacking*. 4 | Il s'agit de l'opération qui permet de décomposer un itérable en plusieurs variables. 5 | 6 | Prenons `values` une liste de 3 valeurs, il est possible en une ligne d'assigner chaque valeur à une variable différente. 7 | 8 | ```python 9 | >>> values = [1, 3, 5] 10 | >>> a, b, c = values 11 | >>> a 12 | 1 13 | >>> b 14 | 3 15 | >>> c 16 | 5 17 | ``` 18 | 19 | J'utilise ici une liste `values`, mais tout type d'itérable est accepté, on pourrait avoir un `range` ou un `set` par exemple. 20 | L'itérable n'a pas besoin d'être une séquence comme la liste. 21 | 22 | ```python 23 | >>> a, b, c = range(1, 6, 2) 24 | >>> a, b, c = {1, 3, 5} # l'ordre n'est pas assuré dans ce dernier cas 25 | ``` 26 | 27 | C'est aussi cette fonctionnalité qui est à l'origine de l'assignement multiple et de l'échange de variables. 28 | 29 | ```python 30 | >>> x, y = 10, 20 31 | >>> x 32 | 10 33 | >>> y 34 | 20 35 | >>> x, y = y, x 36 | >>> x 37 | 20 38 | >>> y 39 | 10 40 | ``` 41 | 42 | En effet, nous avons dans ces deux cas, à gauche comme à droite du signe `=`, des *tuples*. 43 | Et celui de droite est décomposé pour correspondre aux variables de gauche. 44 | 45 | Je parle de *tuples*, mais on retrouve la même chose avec des listes. 46 | Les assignations suivantes sont d'ailleurs équivalentes. 47 | 48 | ```python 49 | >>> x, y = 10, 20 50 | >>> (x, y) = (10, 20) 51 | >>> [x, y] = [10, 20] 52 | ``` 53 | 54 | #### Structures imbriquées 55 | 56 | Ces cas d'*unpacking* sont les plus simples : nous avons un itérable à droite et un ensemble « plat » de variables à gauche. 57 | Je dis « plat » parce qu'il n'y a qu'un niveau, aucune imbrication. 58 | 59 | Mais il est possible de faire bien plus que cela, en décomposant aussi des itérables imbriqués les uns dans les autres. 60 | 61 | ```python 62 | >>> a, ((b, c, d), e), (f, g) = [0, (range(1, 4), 5), '67'] 63 | >>> a 64 | 0 65 | >>> b 66 | 1 67 | >>> c 68 | 2 69 | >>> d 70 | 3 71 | >>> e 72 | 5 73 | >>> f 74 | '6' 75 | >>> g 76 | '7' 77 | ``` 78 | 79 | #### Opérateur *splat* 80 | 81 | Mais on peut aller encore plus loin avec l'opérateur *splat*. 82 | Cet opérateur est représenté par le caractère `*`. 83 | 84 | À ne pas confondre avec la multiplication, opérateur binaire entre deux objets, il s'agit ici d'un opérateur unaire : c'est-à-dire qu'il n'opère que sur un objet, en se plaçant devant. 85 | 86 | Utilisé à gauche lors d'une assignation, il permet de récupérer plusieurs éléments lors d'une décomposition. 87 | 88 | ```python 89 | >>> head, *tail = range(10) 90 | >>> head 91 | 0 92 | >>> tail 93 | [1, 2, 3, 4, 5, 6, 7, 8, 9] 94 | >>> head, *middle, last = range(10) 95 | >>> head 96 | 0 97 | >>> middle 98 | [1, 2, 3, 4, 5, 6, 7, 8] 99 | >>> last 100 | 9 101 | >>> head, second, *middle, last = range(10) 102 | >>> head 103 | 0 104 | >>> second 105 | 1 106 | >>> middle 107 | [2, 3, 4, 5, 6, 7, 8] 108 | >>> last 109 | 9 110 | ``` 111 | 112 | Vous l'avez compris, la variable précédée du *splat* devient une liste, dont la taille s'ajuste en fonction du nombre d'éléments. 113 | 114 | Il est donc impossible d'avoir deux variables précédées d'un *splat*, cela mènerait à une ambigüité. 115 | Ou plutôt, devrais-je préciser, une seul par niveau d'imbrication. 116 | 117 | ```python 118 | >>> *a, (b, *c) = (0, 1, 2, (3, 4, 5)) 119 | >>> a 120 | [0, 1, 2] 121 | >>> b 122 | 3 123 | >>> c 124 | [4, 5] 125 | ``` 126 | 127 | #### Encore du *splat* 128 | 129 | Nous avons vu l'opérateur *splat* utilisé à gauche de l'assignation, mais il est aussi possible depuis Python 3.5[^python_35] de l'utiliser à droite. 130 | Il aura simplement l'effet inverse, et décomposera un itérable comme si ses valeurs avaient été entrées une à une. 131 | 132 | ```python 133 | >>> values = *[0, 1, 2], 3, 4, *[5, 6], 7 134 | >>> values 135 | (0, 1, 2, 3, 4, 5, 6, 7) 136 | ``` 137 | 138 | Il est bien sûr possible de combiner les deux. 139 | 140 | ```python 141 | >>> first, *middle, last = *[0, 1, 2], 3, 4, *[5, 6], 7 142 | >>> first 143 | 0 144 | >>> middle 145 | [1, 2, 3, 4, 5, 6] 146 | >>> last 147 | 7 148 | ``` 149 | 150 | [^python_35]: Pour plus d'informations sur les possibilités étendues de l'opérateur *splat* offertes par Python 3.5 : 151 | -------------------------------------------------------------------------------- /src/1-starters/2-iterables/5-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : Itérateur sur listes chaînées 2 | 3 | Revenons sur nos listes chaînées afin d'y implémenter le protocole des itérables. 4 | Notre classe `Deque` a donc besoin d'une méthode `__iter__` retournant un itérateur, que nous appellerons simplement `DequeIterator`. 5 | 6 | ```python 7 | def __iter__(self): 8 | return DequeIterator(self) 9 | ``` 10 | 11 | Cet itérateur contiendra une référence vers un maillon, puis, à chaque appel à `__next__`, renverra la valeur du maillon courant, tout en prenant soin de passer au maillon suivant pour le prochain appel. `StopIteration` sera levée si le maillon courant vaut `None`. 12 | 13 | On ajoutera aussi `__iter__` dans l'itérateur, comme vu plus tôt, dans le cas où cet itérateur serait utilisé comme itérable. 14 | 15 | ```python 16 | class DequeIterator: 17 | def __init__(self, deque): 18 | self.current = deque.first 19 | 20 | def __next__(self): 21 | if self.current is None: 22 | raise StopIteration 23 | value = self.current.value 24 | self.current = self.current.next 25 | return value 26 | 27 | def __iter__(self): 28 | return self 29 | ``` 30 | 31 | Testons maintenant notre implémentation… 32 | 33 | ```python 34 | >>> for i in Deque([1, 2, 3, 4, 5]): 35 | ... print(i) 36 | ... 37 | 1 38 | 2 39 | 3 40 | 4 41 | 5 42 | ``` 43 | 44 | … Ça marche ! 45 | -------------------------------------------------------------------------------- /src/1-starters/2-iterables/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Passons enfin aux ressources de la documentation concernant les itérables et itérateurs. 4 | 5 | * Définition du terme itérable : 6 | * Du terme itérateur : 7 | * Type itérateur : 8 | * Module `itertools` : 9 | 10 | Les PEP, propositions et descriptions de nouvelles fonctionnalités, sont aussi des sources d'informations intéressantes. 11 | 12 | * *Unpacking* généralisé : 13 | 14 | Et pour finir, quelques ressources annexes sur ces sujets : 15 | 16 | * Nouvelles fonctionnalités de Python 3.5 : 17 | * Article Sam&Max sur l'opérateur *splat* : 18 | -------------------------------------------------------------------------------- /src/1-starters/3-mutables-hashables/0-mutables-hashables.md: -------------------------------------------------------------------------------- 1 | ## Objets mutables et hashables 2 | 3 | Nous allons ici étudier deux propriétés fondamentales des objets en Python : 4 | leur mutabilité et leur hashabilité. 5 | 6 | La première correspond à la capacité des objets à être altérés, modifiés. 7 | 8 | La seconde est la possibilité pour un objet d'être hashé, c'est-à-dire d'en calculer un condensat qui permet entre-autres à l'objet d'être utilisé comme clef dans un dictionnaire. 9 | -------------------------------------------------------------------------------- /src/1-starters/3-mutables-hashables/1-mutables.md: -------------------------------------------------------------------------------- 1 | ### Mutables 2 | 3 | Un objet mutable est ainsi un objet qui peut être modifié, dont on peut changer les propriétés une fois qu'il a été défini. 4 | Une erreur courante est de confondre modification et réassignation. 5 | 6 | La différence est facile à comprendre avec les listes. 7 | Les listes sont des objets mutables : une fois la liste instanciée, il est par exempe possible d'y insérer de nouveaux éléments. 8 | 9 | ```python 10 | >>> values = [0, 1, 2] 11 | >>> values.append(3) 12 | >>> values 13 | [0, 1, 2, 3] 14 | ``` 15 | 16 | Le fonctionnement des variables en Python fait qu'il est possible d'avoir plusieurs noms (étiquettes) sur une même valeur. 17 | Le principe de mutabilité s'observe alors très bien. 18 | 19 | ```python 20 | >>> values = othervalues = [0, 1, 2] 21 | >>> values.append(3) 22 | >>> values 23 | [0, 1, 2, 3] 24 | >>> othervalues 25 | [0, 1, 2, 3] 26 | ``` 27 | 28 | Sans que nous n'ayons explicitement touché à `othervalues`, sa valeur a changé. En effet, `values` et `othervalues` référencent un même objet. 29 | Un même objet mutable. 30 | 31 | En revanche, la réassignation fait correspondre le nom de la variable à un nouvel objet, il n s'agit pas d'une modification de la valeur initiale. 32 | 33 | ```python 34 | >>> values = othervalues = [0, 1, 2] 35 | >>> values = [0, 1, 2, 3] # réassignation de values 36 | >>> values 37 | [0, 1, 2, 3] 38 | >>> othervalues 39 | [0, 1, 2] 40 | ``` 41 | 42 | En Python, les objets de type `bool`, `int`, `str`, `bytes`, `tuple`, `range` et `frozenset` sont immutables. 43 | Tous les autres types, les listes, les dictionnaires, ou les instances de vos propres classes sont des objets mutables. 44 | 45 | Comme nous l'avons vu, les objets mutables sont à prendre avec des pincettes, car leur valeur peut changer sans que nous ne l'ayons explicitement demandé. 46 | Cela peut arriver lorsqu'une valeur mutable est passée en argument à une fonction. 47 | 48 | ```python 49 | >>> def append_42(values): 50 | ... values.append(42) 51 | ... return values 52 | ... 53 | >>> v = [1, 2, 3, 4] 54 | >>> append_42(v) 55 | [1, 2, 3, 4, 42] 56 | >>> v 57 | [1, 2, 3, 4, 42] 58 | ``` 59 | 60 | Cela ne pourra jamais arriver avec un *tuple* par exemple, qui est immutable et ne possède aucune méthode pour être altéré. 61 | 62 | ```python 63 | >>> def append_42(values): 64 | ... return values + (42,) 65 | ... 66 | >>> v = (1, 2, 3, 4) 67 | >>> append_42(v) 68 | (1, 2, 3, 4, 42) 69 | >>> v 70 | (1, 2, 3, 4) 71 | ``` 72 | 73 | Il n'est pas vraiment possible en Python de créer un nouveau type immutable. 74 | Cela peut être simulé en rendant les méthodes de modification/suppression inefficaces. 75 | Mais il est toujours possible de passer outre en appelant directement les méthodes d'une classe parente. 76 | 77 | ```python 78 | >>> class ImmutableDeque(Deque): 79 | ... def append(self, value): 80 | ... raise TypeError('Object is read-only') 81 | ... def insert(self, index, value): 82 | ... raise TypeError('Object is read-only') 83 | ... def __setitem__(self, key, value): 84 | ... raise TypeError('Object is read-only') 85 | ... 86 | >>> deque = ImmutableDeque() 87 | >>> deque.append('foo') 88 | Traceback (most recent call last): 89 | File "", line 1, in 90 | File "", line 3, in append 91 | TypeError: Object is read-only 92 | >>> Deque.append(deque, 'foo') 93 | >>> deque[0] 94 | 'foo' 95 | ``` 96 | 97 | La seule manière sûre est d'hériter d'un autre type immutable, comme les `namestuple` qui héritent de `tuple`. 98 | Nous verrons plus loin dans ce cours comment cela est réalisable. 99 | -------------------------------------------------------------------------------- /src/1-starters/3-mutables-hashables/2-egality-identity.md: -------------------------------------------------------------------------------- 1 | ### Égalité et identité 2 | 3 | L'égalité et l'identité sont deux concepts dont la distinction est parfois confuse. 4 | Deux valeurs sont égales lorsqu'elles partagent un même état : par exemple, deux chaînes qui contiennent les mêmes caractères sont égales. 5 | Deux valeurs sont identiques lorsqu'elles sont une même instance, c'est-à-dire un même objet en mémoire. 6 | 7 | En Python, on retrouve ces concepts sous les opérateurs `==` (égalité) et `is` (identité). 8 | 9 | ```python 10 | >>> [1, 2, 3] == [1, 2, 3] 11 | True 12 | >>> [1, 2, 3] is [1, 2, 3] 13 | False 14 | >>> values = [1, 2, 3] 15 | >>> values is values 16 | True 17 | ``` 18 | 19 | Leur différence est fondamentale pour les types mutables, puisque deux valeurs distinctes peuvent être égales à un moment et ne plus l'être par la suite (si l'une d'elles est modifiée). 20 | Deux valeurs identiques resteront à l'inverse égales, puisque les modifications seront perçues sur les deux variables. 21 | 22 | ```python 23 | >>> values1, values2 = [1, 2, 3], [1, 2, 3] 24 | >>> values1 == values2 25 | True 26 | >>> values1 is values2 27 | False 28 | >>> values1.append(4) 29 | >>> values1 == values2 30 | False 31 | ``` 32 | 33 | ```python 34 | >>> values1 = values2 = [1, 2, 3] 35 | >>> values1 == values2 36 | True 37 | >>> values1 is values2 38 | True 39 | >>> values1.append(4) 40 | >>> values1 == values2 41 | True 42 | ``` 43 | 44 | L'opérateur d'égalité est surchargeable en Python, *via* la méthode spéciale `__eq__` des objets. 45 | Il est en effet de la responsabilité du développeur de gérer la comparaison entre ses objets, et donc de déterminer quand ils sont égaux. 46 | Cette méthode reçoit en paramètre la valeur à laquelle l'objet est comparé, et retourne un booléen. 47 | 48 | On peut imaginer une valeur qui sera égale à toute les autres, grâce à une méthoe `__eq__` retournant toujours `True`. 49 | 50 | ```python 51 | >>> class AlwaysEqual: 52 | ... def __eq__(self, value): 53 | ... return True 54 | ... 55 | >>> val = AlwaysEqual() 56 | >>> val == 0 57 | True 58 | >>> 1 == val 59 | True 60 | ``` 61 | 62 | L'opérateur d'identité testant si deux objets sont une même instance, il n'est bien sûr pas possibe de le surcharger. 63 | En absence de surcharge, l'opérateur d'égalité donnera la même résultat que l'identité. 64 | 65 | Vous pouvez vous référer à [ce chapitre du cours sur la POO en Python](https://zestedesavoir.com/tutoriels/1253/la-programmation-orientee-objet-en-python/4-operators/) 66 | pour davantage d'informations sur la surcharge d'opérateurs. 67 | 68 | #### Quel opérateur utiliser ? 69 | 70 | Une question légitime à se poser suite à ces lignes est de savoir quel opérateur utiliser pour comparer nos valeurs. 71 | La réponse est que cela dépend des valeurs et des cas d'utilisation. 72 | 73 | En règle générale, c'est l'opérateur d'égalité (`==`) qui est à utiliser. 74 | Quand nous comparons un nombre entré par l'utilisateur avec un nombre à deviner, nous ne cherchons pas à savoir s'ils sont un même objet, mais s'ils représentent la même chose. 75 | 76 | L'opérateur `is` s'utilise principalement avec `None`. 77 | `None` est une valeur unique (*singleton*), il n'en existe qu'une instance. 78 | Quand on compare une valeur avec `None`, on vérifie qu'elle est `None` et non qu'elle vaut `None`. 79 | 80 | Globalement, `is` s'utilise pour la comparaison avec des *singletons*, et `==` s'utilise pour le reste. 81 | -------------------------------------------------------------------------------- /src/1-starters/3-mutables-hashables/3-hashables.md: -------------------------------------------------------------------------------- 1 | ### Hashables 2 | 3 | Comme je le disais plus tôt, les objets hashables vont notamment servir pour les clefs des dictionnaires. 4 | Mais voyons tout d'abord à quoi correspond cette capacité. 5 | 6 | #### Le condensat 7 | 8 | En informatique, et plus particulièrement en cryptographie, on appelle condensat (*hash*) un nombre calculé depuis une valeur quelconque, unique et invariable pour cette valeur. 9 | Deux valeurs égales partageront un même *hash*, deux valeurs différentes auront dans la mesure du possible des *hash* différents. 10 | 11 | En effet, le condensat est généralement un nombre de taille fixe (64 bits par exemple), il existe donc un nombre limité de *hashs* pour un nombre infini de valeurs. 12 | Deux valeurs différentes pourront alors avoir un même condensat, c'est ce que l'on appelle une collision. 13 | Les collisions sont plus ou moins fréquentes selon les algorithmes de *hashage*. 14 | En cela, l'égalité entre *hashs* ne doit jamais remplacer l'égalité entre les valeurs, elle n'est qu'une étape préliminaire qui peut servir à optimiser des calculs. 15 | 16 | #### La fonction `hash` 17 | 18 | En Python, on peut calculer le condensat d'un objet à l'aide de la fonction `hash`. 19 | 20 | ```python 21 | >>> hash(10) 22 | 10 23 | >>> hash(2**61 + 9) # collision 24 | 10 25 | >>> hash('toto') 26 | -7475273891964572862 27 | >>> hash((1, 2, 3)) 28 | 2528502973977326415 29 | >>> hash([1, 2, 3]) 30 | Traceback (most recent call last): 31 | File "", line 1, in 32 | TypeError: unhashable type: 'list' 33 | ``` 34 | 35 | Ce dernier exemple nous montre que les listes ne sont pas hashables. 36 | Pourquoi ? On a vu que le *hash* était invariable, mais il doit pourtant correspondre à la valeur. 37 | 38 | Or, en modifiant une liste, le condensat calculé auparavant deviendrait invalide. Il est donc impossible de hasher les listes. 39 | Il en est de même pour les dictionnaires et les ensembles (`set`). 40 | Tous les autres types d'objets sont par défaut hashables. 41 | 42 | On remarque une certaine corrélation entre types mutables et hashables. 43 | En effet, il est plus facile d'assurer l'invariabilité du condensat quand l'objet est lui-même immutable. 44 | Pour les objets mutables, le *hash* n'est possible que si la modification n'altère pas l'égalité entre deux objets, c'est-à-dire que deux objets égaux le resteront même si l'un est modifié. 45 | 46 | Il faut aussi garder à l'esprit que des types immutables peuvent contenir des mutables. Par exemple une liste dans un *tuple*. 47 | Dans ce genre de cas, la non-hashabilité des valeurs contenues rend non-hashable le conteneur. 48 | 49 | ```python 50 | >>> t = ((0, 1), (2, 3)) 51 | >>> hash(t) 52 | 8323144716662114087 53 | >>> t = ((0, 1), [2, 3]) 54 | >>> hash(t) 55 | Traceback (most recent call last): 56 | File "", line 1, in 57 | TypeError: unhashable type: 'list' 58 | ``` 59 | 60 | #### À quoi servent-ils ? 61 | 62 | Je parle depuis le début de clefs de dictionnaires, nous allons maintenant voir pourquoi les dictionnaires utilisent des condensats. 63 | 64 | Les dictionnaires, d'ailleurs appelés tables de hashage dans certains langages, sont des structures qui doivent permettre un accès rapide aux éléments. 65 | Ainsi, ils ne peuvent pas être une simple liste de couples clef/valeur, qui serait parcourue chaque fois que l'on demande un élément. 66 | 67 | À l'aide des *hash*, les dictionnaires disposent les éléments tels que dans un tableau et offrent un accès direct à la majorité d'entre eux. 68 | 69 | Outre les dictionnaires, ils sont aussi utilisés dans les `set`, ensembles non ordonnés de valeurs uniques. 70 | 71 | On remarque facilement que les objets non-hashables ne peuvent être utilisés en tant que clefs de dictionnaires ou dans un ensemble. 72 | 73 | ```python 74 | >>> {[0]: 'foo'} 75 | Traceback (most recent call last): 76 | File "", line 1, in 77 | TypeError: unhashable type: 'list' 78 | >>> {{'foo': 'bar'}} 79 | Traceback (most recent call last): 80 | File "", line 1, in 81 | TypeError: unhashable type: 'dict' 82 | ``` 83 | 84 | Plus généralement, les *hash* peuvent être utilisés pour optimiser le test d'égalité entre deux objets. 85 | Le *hash* étant invariable, il est possible de ne le calculer qu'une fois par objet (en stockant sa valeur). 86 | 87 | Ainsi, lors d'un test d'égalité, on peut facilement dire que les objets sont différents, si les *hash* le sont. 88 | L'inverse n'est pas vrai à cause des collisions : deux objets différents peuvent avoir un même *hash*. 89 | Le test d'égalité proprement dit (appel à la méthode `__eq__`) doit donc toujours être réalisé si les *hash* sont égaux. 90 | 91 | #### Implémentation 92 | 93 | Les types de votre création sont par défaut hashables, puisque l'égalité entre objets vaut l'idendité. 94 | La question de la *hashabilité* ne se pose donc que si vous surchargez l'opérateur `__eq__`. 95 | 96 | Dans ce cas, il convient normalement de vous occuper aussi de la méthode spéciale `__hash__`. 97 | C'est cette méthode qui est appelée par la fonction `hash` pour calculer le condensat d'un objet. 98 | 99 | Il est aussi possible d'assigner `None` à `__hash__` afin de rendre l'objet non-*hashable*. 100 | Python le fait par défaut lorsque nous surchargeons l'opérateur `__eq__`. 101 | 102 | Pour reprendre la classe `AlwaysEqual` définie précédemment : 103 | 104 | ```python 105 | >>> val = AlwaysEqual() 106 | >>> hash(val) 107 | Traceback (most recent call last): 108 | File "", line 1, in 109 | TypeError: unhashable type: 'AlwaysEqual' 110 | >>> print(val.__hash__) 111 | None 112 | ``` 113 | 114 | Si toutefois vous souhaitez redéfinir la méthode `__hash__`, il vous faut respecter les quelques règles énoncées plus haut. 115 | 116 | - L'invariabilité du *hash* ; 117 | - L'égalité entre deux *hashs* de valeurs égales. 118 | 119 | Ces conditions état plus faciles à respecter pour des valeurs immutables. 120 | 121 | Notons enfin que le résultat de la méthode `__hash__` est tronqué par la fonction `hash`, afin de tenir sur un nombre fixe de bits. 122 | 123 | Pour plus d'informations sur cette méthode `__hash__` : . 124 | -------------------------------------------------------------------------------- /src/1-starters/3-mutables-hashables/4-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : Égalité entre listes 2 | 3 | Nous allons maintenant nous intéresser à l'implémentation de l'opérateur d'égalité entre listes. 4 | Nos listes, mutables, deviendront par conséquent non-hashables. 5 | Nous reviendrons vers la fin de ce cours sur l'implémentation de listes immutables. 6 | 7 | L'opérateur d'égalité correspond donc à la méthode spéciale `__eq__`, recevant en paramètre l'objet auquel `self` est comparé. 8 | La méthode retourne ensuite `True` si les objets sont égaux, `False` s'ils sont différents, et `NotImplemented` si la comparaison ne peut être faite. 9 | 10 | `NotImplemented` est une facilité de Python pour gérer les opérateurs binaires. 11 | En effet, dans une égalité `a == b` par exemple, on ne peut pas savoir lequel de `a` ou `b` redéfinit la méthode `__eq__`. 12 | L'interpréteur va alors tester en premier d'appeler la méthode de `a` : 13 | 14 | * Si la méthode retourne `True`, les objets sont égaux ; 15 | * Si elle retourne `False`, ils sont différents ; 16 | * Si elle retourne `NotImplemented`, alors l'interpréteur appellera la méthode `__eq__` de `b` pour déterminer le résultat ; 17 | * Si les deux méthodes retournent `NotImplemented`, les objets sont différents. 18 | 19 | Dans notre méthode, nous allons donc premièrement vérifier le type du paramètre. 20 | S'il n'est pas du type attendu (`Deque`), nous retournerons `NotImplemented`. 21 | 22 | Nous comparerons ensuite la taille des listes, si la taille diffère, les listes sont nécessairement différentes. 23 | Dans l'idéal, nous devrions éviter cette comparaison car elle est coûteuse (elle nécessite de parcourir entièrement chacune des listes), mais nous pouvons la conserver dans le cadre de l'exercice. 24 | 25 | Enfin, nous itérerons simultanément sur nos deux listes pour vérifier que tous les éléments sont égaux. 26 | 27 | ```python 28 | def __eq__(self, other): 29 | if not isinstance(other, Deque): 30 | return NotImplemented 31 | if len(self) != len(other): 32 | return False 33 | for elem1, elem2 in zip(self, other): 34 | if elem1 != elem2: 35 | return False 36 | return True 37 | ``` 38 | 39 | C'est l'heure du test ! 40 | 41 | ```python 42 | >>> d = Deque([0, 1]) 43 | >>> d == Deque([0, 1]) 44 | True 45 | >>> d == Deque([0, 1, 2]) 46 | False 47 | >>> d == Deque([1, 2]) 48 | False 49 | >>> d == 0 50 | False 51 | >>> d.append(2) 52 | >>> d == Deque([0, 1]) 53 | False 54 | >>> d == Deque([0, 1, 2]) 55 | True 56 | ``` 57 | 58 | Et comme nous pouvons le constater, notre `Deque` a perdu son pouvoir d'hashabilité. 59 | 60 | ```python 61 | >>> hash(d) 62 | Traceback (most recent call last): 63 | File "", line 1, in 64 | TypeError: unhashable type: 'Deque' 65 | ``` 66 | -------------------------------------------------------------------------------- /src/1-starters/3-mutables-hashables/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Nous retrouvons ici les traditionnelles références vers la documentation officielle. 4 | 5 | * Définition du terme mutable : 6 | * Du terme hashable : 7 | * Fonction `hash` : 8 | * Méthode `__hash__` et protocole : 9 | -------------------------------------------------------------------------------- /src/2-functions/0-functions.md: -------------------------------------------------------------------------------- 1 | # Ainsi font fonctions 2 | 3 | ##### Trois petits tours et puis s'en vont 4 | 5 | Nous allons dans ces chapitres traiter le concept de fonction, 6 | dans sa définition large (c'est-à-dire englobant tous les objets se comportant comme des fonctions). 7 | -------------------------------------------------------------------------------- /src/2-functions/1-callables/0-callables.md: -------------------------------------------------------------------------------- 1 | ## Callables 2 | 3 | Nous allons maintenant nous intéresser à un « nouveau » type d'objets : les *callables*. Je place des guillemets autour de nouveau car vous les fréquentez en réalité depuis que vous faites du Python, les fonctions étant des *callables*. 4 | 5 | Qu'est-ce qu'un *callable* me demanderez-vous ? C'est un objet que l'on peut appeler. Appeler un objet consiste à utiliser l'opérateur `()`, en lui précisant un certain nombre d'arguments, de façon à recevoir une valeur de retour. 6 | 7 | ```python 8 | >>> print('Hello', 'world', end='!\n') # Appel d'une fonction avec différents arguments 9 | Hello world! 10 | >>> x = pow(2, 3) # Valeur de retour 11 | >>> x 12 | 8 13 | ``` 14 | -------------------------------------------------------------------------------- /src/2-functions/1-callables/1-functions-classes-lambdas.md: -------------------------------------------------------------------------------- 1 | ### Fonctions, classes et lambdas 2 | 3 | L'ensemble des *callables* contient donc les fonctions, mais pas seulement. Les classes en sont, les méthodes, les lambdas, etc. 4 | Sont callables tous les objets derrière lesquels on peut placer une paire de parenthèses, pour les appeler. 5 | 6 | En Python, on peut vérifier qu'un objet est appelable à l'aide de la fonction `callable`. 7 | 8 | ```python 9 | >>> callable(print) 10 | True 11 | >>> callable(lambda: None) 12 | True 13 | >>> callable(callable) 14 | True 15 | >>> callable('') 16 | False 17 | >>> callable(''.join) 18 | True 19 | >>> callable(str) 20 | True 21 | >>> class A: pass 22 | ... 23 | >>> callable(A) 24 | True 25 | >>> callable(A()) 26 | False 27 | ``` 28 | -------------------------------------------------------------------------------- /src/2-functions/1-callables/3-call.md: -------------------------------------------------------------------------------- 1 | ### Call-me maybe 2 | 3 | Je vous le disais, plusieurs types d'objets peuvent être appelés. Que cache donc un *callable* ? 4 | Encore une fois, il s'agit d'un objet qui possède une méthode spéciale. 5 | La méthode est ici `__call__`, dont les paramètres seront les arguments passés lors de l'appel. 6 | La valeur renvoyée par `__call__` sera le retour de l'appel. 7 | 8 | Ainsi, testons avec divers objets : 9 | 10 | ```python 11 | >>> def func(arg): return arg 12 | ... 13 | >>> func(1) 14 | 1 15 | >>> func.__call__(1) 16 | 1 17 | >>> (lambda x: x + 1)(1) 18 | 2 19 | >>> (lambda x: x + 1).__call__(1) 20 | 2 21 | ``` 22 | 23 | Mais, vous devez vous dire, si on peut appeler `func.__call__`, c'est que `func.__call__` est un *callable*, qui possède donc sa propre méthode `__call__` ? 24 | C'est le cas, et l'on peut continuer ainsi indéfiniment. 25 | 26 | ```python 27 | >>> func.__call__.__call__(1) 28 | 1 29 | >>> func.__call__.__call__.__call__.__call__.__call__.__call__(1) 30 | 1 31 | ``` 32 | 33 | Cela s'explique par le fait que `__call__` est une méthode, donc un *callable*. 34 | En interne, Python est capable d'identifier qu'il s'agit d'une fonction et d'en exécuter le code, pour ne pas avoir à appeler indéfiniment des `__call__`. 35 | 36 | Maintenant, implémentons `__call__` dans un objet de notre création : 37 | 38 | ```python 39 | class MyCallable: 40 | def __init__(self, a): 41 | self.a = a 42 | 43 | def __call__(self, b): 44 | return self.a + b 45 | ``` 46 | 47 | Nous avons là une classe `MyCallable`, dont les instances sont des *callables*, réalisant la somme du paramètre reçu à la construction avec celui reçu lors de l'appel. 48 | 49 | ```python 50 | >>> add_3 = MyCallable(3) 51 | >>> add_3(5) 52 | 8 53 | >>> add_3(10) 54 | 13 55 | >>> add_100 = MyCallable(100) 56 | >>> add_100(2) 57 | 102 58 | >>> MyCallable(6)(3) 59 | 9 60 | ``` 61 | 62 | Il y a différents intérêts à créer un type *callable*. Le premier serait simplement de rendre compatible notre objet à l'interface utilisée par de nombreuses fonctions Python que nous verrons dans la section suivante. 63 | Aussi, utiliser une classe pour cela est un moyen simple de sauvegarder un état, permettant d'avoir un comportement différent à chaque appel. 64 | 65 | ```python 66 | >>> class Increment: 67 | ... def __init__(self): 68 | ... self.n = 0 69 | ... def __call__(self): 70 | ... self.n += 1 71 | ... return self.n 72 | ... 73 | >>> incr = Increment() 74 | >>> incr() 75 | 1 76 | >>> incr() 77 | 2 78 | >>> Increment()() # Les deux objets sont bien indépendants 79 | 1 80 | >>> incr() 81 | 3 82 | ``` 83 | -------------------------------------------------------------------------------- /src/2-functions/1-callables/4-callables.md: -------------------------------------------------------------------------------- 1 | ### Utilisation des callables 2 | 3 | De même que pour les itérables, les *callables* sont au cœur de Python en pouvant être utilisés avec un grand nombre de *builtins*. 4 | 5 | Par exemple, la fonction `max` évoquée dans un précédent chapitre : en plus de prendre un itérable sur lequel trouver le maximum, elle peut aussi prendre un paramètre `key`. 6 | Ce paramètre est un *callable* expliquant comment extraire le maximum depuis les arguments passés à `max`. 7 | 8 | Si l'itérable ne contient que des entiers, il est plutôt simple de déterminer le maximum, mais si nous avons une liste de points 2D par exemple ? 9 | Le maximum pourrait être le point avec la plus grande abscisse, la plus grande ordonnée, le point le plus éloigné de l'origine du repère, ou encore bien d'autres choses. 10 | 11 | Le *callable* `key` est donc chargé de calculer une valeur numérique pour chacun des paramètres, et pouvoir ainsi les comparer entre-eux. Le paramètre ayant la plus grande valeur sera le maximum. 12 | 13 | Nous représenterons ici nos points par des tuples de deux valeurs. 14 | 15 | ```python 16 | >>> points = [(0, 0), (1, 4), (3, 3), (4, 0)] 17 | >>> max(points) # par défaut, Python sélectionne suivant le premier élément, soit l'abscisse 18 | (4, 0) 19 | >>> max(points, key=lambda p: p[0]) # Nous précisons ici explicitement la sélection par l'abscisse 20 | (4, 0) 21 | >>> max(points, key=lambda p: p[1]) # Par ordonnée 22 | (1, 4) 23 | >>> max(points, key=lambda p: p[0]**2 + p[1]**2) # Par distance de l'origine 24 | (3, 3) 25 | ``` 26 | 27 | En dehors de `max`, d'autres fonctions Python prennent un tel paramètre key, comme `min` ou encore `sorted` : 28 | 29 | ```python 30 | >>> sorted(points, key=lambda p: p[1]) 31 | [(0, 0), (4, 0), (3, 3), (1, 4)] 32 | >>> sorted(points, key=lambda p: p[0]**2 + p[1]**2) 33 | [(0, 0), (4, 0), (1, 4), (3, 3)] 34 | ``` 35 | 36 | `map`, que nous avons déjà vu, prend lui aussi un *callable* de n'importe quel type. 37 | 38 | ```python 39 | >>> list(map(lambda p: p[0], points)) 40 | [0, 1, 3, 4] 41 | >>> list(map(lambda p: p[1], points)) 42 | [0, 4, 3, 0] 43 | ``` 44 | 45 | Je vous invite une nouvelle fois à jeter un œil aux [*builtins* Python](https://docs.python.org/3/library/functions.html), 46 | ainsi qu'au [module `itertools`](https://docs.python.org/3/library/itertools.html), et de voir lesquels peuvent vous faire tirer profit des *callables*. 47 | -------------------------------------------------------------------------------- /src/2-functions/1-callables/5-operator-functools.md: -------------------------------------------------------------------------------- 1 | ### Modules `operator` et `functools` 2 | 3 | Passons maintenant à la présentation de deux modules, contenant deux collections de *callables*. 4 | 5 | #### `operator` 6 | 7 | Ce premier module, [`operator`](https://docs.python.org/3/library/operator.html) regroupe l'ensemble des opérateurs Python sous forme de fonctions. 8 | Ainsi, une addition pourrait se formuler par : 9 | 10 | ```python 11 | >>> import operator 12 | >>> operator.add(3, 4) 13 | 7 14 | ``` 15 | 16 | Outre les opérateurs habituels, nous en trouvons d'autres sur lesquels nous allons nous intéresser plus longuement, dont la particularité est de retourner des *callables*. 17 | 18 | ##### `itemgetter` 19 | 20 | `itemgetter` permet de récupérer un élément précis depuis un indexable, à la manière de l'opérateur `[]`. 21 | 22 | ```python 23 | >>> get_second = operator.itemgetter(1) # Récupère le 2ème élément de l'indexable donné en argument 24 | >>> get_second([5, 8, 0, 3, 1]) 25 | 8 26 | >>> get_second('abcdef') 27 | b 28 | >>> get_second(range(3,10)) 29 | 4 30 | >>> get_x = operator.itemgetter('x') 31 | >>> get_x({'x': 5, 'y': 1}) 32 | 5 33 | ``` 34 | 35 | ##### `methodcaller` 36 | 37 | `methodcaller` permet d'appeler une méthode prédéterminée d'un objet, avec ses arguments. 38 | 39 | ```python 40 | >>> dash_spliter = operator.methodcaller('split', '-') 41 | >>> dash_spliter('a-b-c-d') 42 | ['a', 'b', 'c', 'd'] 43 | >>> append_b = operator.methodcaller('append', 'b') 44 | >>> l = [0, 'a', 4] 45 | >>> append_b(l) 46 | >>> l 47 | [0, 'a', 4, 'b'] 48 | ``` 49 | 50 | #### `functools` 51 | 52 | Je tenais ensuite à évoquer le module [`functools`](https://docs.python.org/3/library/functools.html), et plus particulièrement la fonction `partial` : celle-ci permet de réaliser un appel partiel de fonction. 53 | 54 | Imaginons que nous ayons une fonction prenant divers paramètres, mais que nous voudrions fixer le premier : l'application partielle de la fonction nous créera un nouveau *callable* qui, quand il sera appelé avec de nouveaux arguments, nous renverra le résultat de la première fonction avec l'ensemble des arguments. 55 | 56 | Par exemple, prenons une fonction de journalisation `log` prenant quatre paramètres, un niveau de gravité, un type, un object, et un message descriptif : 57 | 58 | ```python 59 | def log(level, type, item, message): 60 | print('[{}]<{}>({}): {}'.format(level.upper(), type, item, message)) 61 | ``` 62 | 63 | Une application partielle reviendrait à avoir une fonction `warning` tel que chaque appel `warning('foo', 'bar', 'baz')` équivaudrait à `log('warning', 'foo', 'bar', 'baz')`. 64 | Ou encore une fonction `warning_foo` avec `warning_foo('bar', 'baz')` équivalent à l'appel précédent. 65 | 66 | Nous allons la tester avec une fonction du module `operator` : la fonction de multiplication. En appliquant partiellement `5` à la fonction `operator.mul`, `partial` nous retourne une fonction réalisant la multiplication par 5 de l'objet passé en paramètre. 67 | 68 | ```python 69 | >>> from functools import partial 70 | >>> mul_5 = partial(operator.mul, 5) 71 | >>> mul_5(3) 72 | 15 73 | >>> mul_5('z') 74 | 'zzzzz' 75 | >>> warning = partial(log, 'warning') 76 | >>> warning('overflow', 100, 'number is too large') 77 | [WARNING](100): number is too large 78 | >>> overflow = partial(log, 'warning', 'overflow') 79 | >>> overflow(-1, 'number is too low') 80 | [WARNING](-1): number is too low 81 | ``` 82 | 83 | Le module `functools` comprend aussi la fonction `reduce`, un outil tiré du fonctionnel permettant de transformer un itérable en une valeur unique. 84 | Pour cela, elle itère sur l'ensemble et applique une fonction à chaque valeur, en lui précisant la valeur courante et le résultat précédent. 85 | 86 | Imaginons par exemple que nous disposions d'une liste de nombres `[5, 8, 0, 3, 1]` et que nous voulions en calculer la somme. Nous savons faire la somme de deux nombres, il s'agit d'une addition, donc de la fonction `operator.add`. 87 | 88 | `reduce` va nous permettre d'appliquer `operator.add` sur les deux premiers éléments (`5 + 8 = 13`), réappliquer la fonction avec ce premier résultat et le prochain élément de la liste (`13 + 0 = 13`), puis avec ce résultat et l'élément suivant (`13 + 3 = 16`), et enfin, sur ce résultat et le dernier élément (`16 + 1 = 17`). 89 | 90 | Ce processus se résume en : 91 | 92 | ```python 93 | >>> from functools import reduce 94 | >>> reduce(operator.add, [5, 8, 0, 3, 1]) 95 | 17 96 | ``` 97 | 98 | qui revient donc à faire 99 | 100 | ```python 101 | >>> operator.add(operator.add(operator.add(operator.add(5, 8), 0), 3), 1) 102 | 17 103 | ``` 104 | 105 | Bien sûr, en pratique, `sum` est déjà là pour répondre à ce problème. 106 | -------------------------------------------------------------------------------- /src/2-functions/1-callables/6-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : `itemgetter` 2 | 3 | Dans ce nouveau TP, nous allons réaliser `itemgetter` à l'aide d'une classe formant des objets *callables*. 4 | 5 | La clef à récupérer est passée à l'instanciation de l'objet `itemgetter`, et donc à son constructeur. 6 | L'objet depuis lequel nous voulons récupérer la clef sera passé lors de l'appel (dans la méthode `__call__`). 7 | Il nous suffit alors, dans cette méthode, d'appeler l'opérateur `[]` sur l'objet avec la clef enregistrée au moment de la construction. 8 | 9 | ```python 10 | class itemgetter: 11 | def __init__(self, key): 12 | self.key = key 13 | 14 | def __call__(self, obj): 15 | return obj[self.key] 16 | ``` 17 | 18 | C'est aussi simple que cela, et nous pouvons le tester : 19 | 20 | ```python 21 | >>> points = [(0, 0), (1, 4), (3, 3), (4, 0)] 22 | >>> sorted(points, key=itemgetter(0)) 23 | [(0, 0), (1, 4), (3, 3), (4, 0)] 24 | >>> sorted(points, key=itemgetter(1)) 25 | [(0, 0), (4, 0), (3, 3), (1, 4)] 26 | ``` 27 | 28 | Nous aurions aussi pu profiter des fermetures (*closures*) de Python pour réaliser `itemgetter` sous la forme d'une fonction retournant une fonction. 29 | 30 | ```python 31 | def itemgetter(key): 32 | def function(obj): 33 | return obj[key] 34 | return function 35 | ``` 36 | 37 | Si vous vous êtes intéressés de plus près à `operator.itemgetter`, vous avez aussi pu remarquer que celle-ci pouvait prendre plus d'un paramètre : 38 | 39 | ```python 40 | >>> from operator import itemgetter 41 | >>> get_x = itemgetter('x') 42 | >>> get_x_y = itemgetter('x', 'y') 43 | >>> get_x({'x': 9, 'y': 6}) 44 | 9 45 | >>> get_x_y({'x': 9, 'y': 6}) 46 | (9, 6) 47 | ``` 48 | 49 | Je vous propose, pour aller un peu plus loin, d'ajouter cette fonctionnalité à notre classe, et donc d'utiliser les listes d'arguments positionnels. 50 | Vous trouverez la solution dans la [documentation du module `operator`](https://docs.python.org/3/library/operator.html). 51 | -------------------------------------------------------------------------------- /src/2-functions/1-callables/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Voici donc quelques liens relatifs aux *callables*. 4 | 5 | * Définition du terme paramètre : 6 | * Et du terme argument : 7 | * Types callables : 8 | * Émuler les callables : 9 | * Syntaxe des appels : 10 | * Module `operator` : 11 | * Module `functools` : 12 | * Description des *trucmuchables* : 13 | -------------------------------------------------------------------------------- /src/2-functions/2-annotations-signatures/0-annotations-signatures.md: -------------------------------------------------------------------------------- 1 | ## Annotations et signatures 2 | 3 | Intéressons-nous maintenant à ce qui entoure les fonctions. 4 | Une fonction n'est pas seulement un nom ou un bloc de code à exécuter. 5 | Elle possède aussi des informations complémentaires comme des noms de paramètres, des annotations, ou une *docstring*. 6 | 7 | Ce chapitre s'intéresse justement à ces informations : 8 | comment les définir, mais surtout comment y accéder et ce que l'on peut en faire. 9 | -------------------------------------------------------------------------------- /src/2-functions/2-annotations-signatures/1-annotations.md: -------------------------------------------------------------------------------- 1 | ### Annotations 2 | 3 | Commençons par les annotations. 4 | Il se peut que vous ne les ayez jamais rencontrées, il s'agit d'une fonctionnalité relativement nouvelle du langage. 5 | 6 | Les annotations sont des informations de types que l'on peut ajouter sur les paramètres et le retour d'une fonction. 7 | 8 | Prenons une fonction d'addition entre deux nombres. 9 | 10 | ```python 11 | def addition(a, b): 12 | return a + b 13 | ``` 14 | 15 | Nous avons destiné cette fonction à des calculs numériques, mais nous pourrions aussi l'appeler avec des chaînes de caractères. 16 | Les annotations vont nous permettre de préciser le type des paramètres attendus, et le type de la valeur de retour. 17 | 18 | ```python 19 | def addition(a: int, b: int) -> int: 20 | return a + b 21 | ``` 22 | 23 | Leur définition est assez simple : pour annoter un paramètre, on le fait suivre d'un `:` et on ajoute le type attendu. 24 | Pour annoter le retour de la fonction, on ajoute `->` puis le type derrière la liste des paramètres. 25 | 26 | Attention cependant, les annotations ne sont là qu'à titre indicatif. 27 | Rien n'empêche de continuer à appeler notre fonction avec des chaînes de caractères. 28 | 29 | À ce titre, on notera aussi que donner des types comme annotations n'est qu'une convention. 30 | Annoter des paramètres avec des chaînes de caractères ne provoquera pas d'erreur par exemple. 31 | 32 | #### Utilité des annotations 33 | 34 | Si elles ne sont là qu'à titre indicatif, les annotations sont surtout utiles dans un but de documentation. 35 | Elles sont le meilleur moyen en Python de documenter les types des paramètres et de retour d'une fonction. 36 | 37 | Elles apparaissent d'ailleurs dans l'aide de la fonction fournie par `help`. 38 | 39 | ```python 40 | >>> help(addition) 41 | Help on function addition in module __main__: 42 | 43 | addition(a:int, b:int) -> int 44 | ``` 45 | 46 | Les annotations ne sont pas censées avoir d'autre utilité lors de l'exécution d'un programme Python. 47 | Elles ne sont pas destinées à vérifier au *runtime* le type des paramètres par exemple. 48 | La définitition d'annotations ne doit normalement rien changer sur le déroulement du programme. 49 | 50 | Toutefois, les annotations ont l'utilité que l'on veut bien leur donner. 51 | Il existe des outils d'analyse statique tels que `mypy` qui peuvent en tirer partie. 52 | Ces outils n'exécutent pas le code, mais se contentent de vérifier que les types utilisés n'entrent pas en conflit avec les annotations. 53 | 54 | #### Des types plus complexes (module `typing`) 55 | 56 | Nous avons défini une fonction addition opérant sur deux nombres, mais l'avons annotée comme ne pouvant recevoir que des nombres entiers (`int`). 57 | 58 | En effet, les annotations utilisées jusqu'ici étaient plutôt simples. 59 | Mais elles peuvent accueillir des expressions plus complexes. 60 | 61 | Le [module `typing`](https://docs.python.org/3/library/typing.html) nous présente une collection de classes pour composer des types. 62 | Ce module a été introduit dans Python 3.5, et n'est donc pas disponible dans les versions précédentes du langage. 63 | 64 | Dans notre fonction `addition`, nous voudrions en fait que les `int`, `float` et `complex` soient admis. 65 | Nous pouvons pour cela utiliser le type `Union` du module `typing`. 66 | Il nous permet de définir un ensemble de types valides pour nos paramètres, et s'utilise comme suit. 67 | 68 | ```python 69 | Number = Union[int, float, complex] 70 | 71 | def addition(a: Number, b: Number) -> Number: 72 | return a + b 73 | ``` 74 | 75 | Nous définissons premièrement un type `Number` comme l'ensemble des types `int`, `float` et `complex` *via* la syntaxe `Union[...]`. 76 | Puis nous utilisons notre nouveau type `Number` au sein de nos annotations. 77 | 78 | Outre `Union`, le module `typing` présente d'autres types génériques pour avoir des annotations plus précises. 79 | Nous pourrions avoir, par exemple : 80 | 81 | * `List[str]` -- Une liste de chaînes de caractères ; 82 | * `Sequence[str]` -- Une séquence (liste/*tuple*/etc.) de chaînes de caractères ; 83 | * `Callable[[str, int], str]` -- Un *callable* prenant deux paramètres de types `str` et `int` respectivement, et retournant un objet `str` ; 84 | * Et bien d'autres à découvrir dans la [documentation du module](https://docs.python.org/3/library/typing.html). 85 | 86 | Attention encore, le module `typing` ne doit servir que dans le cadre des annotations. 87 | Les types fournis par ce module ne doivent pas être utilisées au sein d'expressions avec `isinstance` ou `issubclass`. 88 | 89 | Dans le cas précis de notre fonction `addition`, nous aurions aussi pu utiliser le type `Number` du [module `numbers`](https://docs.python.org/3/library/numbers.html). 90 | Nous y reviendrons plus tard dans ce cours, mais il s'agit d'un type qui regroupe et hiérarchise tous les types numériques. 91 | 92 | #### Annotations de variables 93 | 94 | Les annotations sont ici abodées sous l'angle des fonctions et de leurs paramètres. 95 | Mais il est à noter que depuis Python 3.6, il est aussi possible d'annoter les variables et attributs. 96 | 97 | La syntaxe est la même que pour les paramètres de fonction. 98 | Encore une fois, les annotations sont là à titre indicatif, et pour les analyseurs statiques. 99 | Elles sont toutefois stockées dans le dictionnaire `__annotations__` du module ou de la classe qui les contient. 100 | 101 | ```python 102 | max_value : int = 10 # Définition d'une variable max_value annotée comme int 103 | min_value : int # Annotation seule, la variable n'est pas définie dans ce cas 104 | ``` 105 | 106 | Un petit coup d'œil à la variable `__annotations__` et aux variables annotées. 107 | 108 | ```python 109 | >>> __annotations__ 110 | {'max_value': , 'min_value': } 111 | >>> max_value 112 | 10 113 | >>> min_value 114 | Traceback (most recent call last): 115 | File "", line 1, in 116 | NameError: name 'min_value' is not defined 117 | ``` 118 | -------------------------------------------------------------------------------- /src/2-functions/2-annotations-signatures/2-inspect.md: -------------------------------------------------------------------------------- 1 | ### Inspecteur Gadget 2 | 3 | Nous savons maintenant renseigner des informations complémentaires sur nos fonctions. 4 | En plus des annotations vues précédemment, vous avez probablement déjà rencontré les *docstrings*. 5 | 6 | Les *docstrings* sont des chaînes de caractères, à placer en tête d'une fonction, d'une classe ou d'un module. 7 | Elles servent à décrire l'usage de ces objets, et sont accessibles dans l'aide fournie par `help`, au même titre que les annotations. 8 | 9 | ```python 10 | def addition(a:int, b:int) -> int: 11 | "Return the sum of the two numbers `a` and `b`" 12 | return a + b 13 | ``` 14 | 15 | ```python 16 | >>> help(addition) 17 | Help on function addition in module __main__: 18 | 19 | addition(a:int, b:int) -> int 20 | Return the sum of the two numbers `a` and `b` 21 | ``` 22 | 23 | Elles deviennent aussi accessibles par l'attribut spécial `__doc__` de l'objet. 24 | 25 | ```python 26 | >>> addition.__doc__ 27 | 'Return the sum of the two numbers `a` and `b`' 28 | ``` 29 | 30 | #### Module `inspect` 31 | 32 | [`inspect`](https://docs.python.org/3/library/inspect.html) est un module de la bibliothèque standard qui permet d'extraire des informations complémentaires sur les objets Python. 33 | Il est notamment dédié aux modules, classes et fonctions. 34 | 35 | Il comporte en effet des fonctions pour vérifier le type d'un objet : `ismodule`, `isclass`, `isfunction`, etc. 36 | 37 | ```python 38 | >>> import inspect 39 | >>> inspect.ismodule(inspect) 40 | True 41 | >>> inspect.isclass(int) 42 | True 43 | >>> inspect.isfunction(addition) 44 | True 45 | >>> inspect.isbuiltin(len) 46 | True 47 | ``` 48 | 49 | D'autres fonctions vont s'intéresser plus particulièrement aux documentations (`getdoc`) et à la gestion du code source (`getsource`, `getsourcefile`, etc.). 50 | 51 | Imaginons un fichier `operations.py` contenant le code suivant : 52 | 53 | ```python 54 | "Mathematical operations" 55 | 56 | def addition(a:int, b:int) -> int: 57 | """ 58 | Return the sum of the two numbers `a` and `b` 59 | 60 | ex: addition(3, 5) -> 8 61 | """ 62 | return a + b 63 | ``` 64 | 65 | Depuis la fonction `addition` importée du module, nous pourrons grâce à `inspect` récupérer toutes les informations nécessaires au débogage. 66 | 67 | ```python 68 | >>> import inspect 69 | >>> from operations import addition 70 | >>> inspect.getdoc(addition) 71 | 'Return the sum of the two numbers `a` and `b`\n\nex: addition(3, 5) -> 8' 72 | >>> inspect.getsource(addition) 73 | 'def addition(a:int, b:int) -> int:\n [...]\n return a + b\n' 74 | >>> inspect.getsourcefile(addition) 75 | '/home/entwanne/operations.py' 76 | >>> inspect.getmodule(addition) 77 | 78 | >>> inspect.getsourcelines(addition) 79 | (['def addition(a:int, b:int) -> int:\n', ..., ' return a + b\n'], 3) 80 | ``` 81 | 82 | L'intérêt de `getdoc` par rapport à l'attribut `__doc__` étant que la documentation est nettoyée (suppression des espaces en début de ligne) par la fonction `cleandoc`. 83 | 84 | ```python 85 | >>> addition.__doc__ 86 | '\n Return the sum of the two numbers `a` and `b`\n\n ex: addition(3, 5) -> 8\n ' 87 | >>> inspect.cleandoc(addition.__doc__) 88 | 'Return the sum of the two numbers `a` and `b`\n\nex: addition(3, 5) -> 8' 89 | ``` 90 | -------------------------------------------------------------------------------- /src/2-functions/2-annotations-signatures/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Retrouvons les quelques ressources complémentaires sur ces sujets. 4 | 5 | * Module `inspect` : 6 | * Annotations de fonctions : 7 | * `mypy`, analyseur statique de code à partir des annotations : 8 | -------------------------------------------------------------------------------- /src/2-functions/3-decorators/0-decorators.md: -------------------------------------------------------------------------------- 1 | ## Décorateurs 2 | 3 | Dans ce chapitre nous allons découvrir les décorateurs et leur utilisation. 4 | 5 | Le nom de décorateur provient du patron de conception [décorateur](http://fr.wikipedia.org/wiki/D%C3%A9corateur_%28patron_de_conception%29) (*pattern decorator*). 6 | Un décorateur permet de se soustraire à un objet pour en modifier le comportement. 7 | -------------------------------------------------------------------------------- /src/2-functions/3-decorators/1-deco.md: -------------------------------------------------------------------------------- 1 | ### D&CO, une semaine pour tout changer 2 | 3 | En Python, vous en avez peut-être déjà croisé, les décorateurs se repèrent au caractère `@`. 4 | 5 | Le principe de la décoration en Python est d'appliquer un décorateur à une fonction, afin de retourner un nouvel objet (généralement une fonction). 6 | On peut donc voir le décorateur comme une fonction prenant une fonction en paramètre, et retournant une nouvelle fonction[^approximation_decorateur] 7 | 8 | [^approximation_decorateur]: Bien que la définition soit plus large que cela. 9 | Le décorateur est un *callable* prenant un *callable* en paramètre, et pouvant retourner tout type d'objet. 10 | 11 | ```python 12 | def decorator(f): # decorator est un décorateur 13 | print(f.__name__) 14 | return f 15 | ``` 16 | 17 | Pour appliquer un décorateur, on précède la ligne de définition de la fonction à décorer par une ligne comportant un `@` puis le nom du décorateur à appliquer, par exemple : 18 | 19 | ```python 20 | >>> @decorator 21 | ... def addition(a, b): 22 | ... return a + b 23 | ... 24 | addition 25 | ``` 26 | 27 | Cela a pour effet de remplacer `addition` par le retour de la fonction `decorator` appelée avec `addition` en paramètre, ce qui est strictement équivalent à : 28 | 29 | ```python 30 | def addition(a, b): 31 | return a + b 32 | 33 | addition = decorator(addition) 34 | ``` 35 | 36 | On voit donc bien que le décorateur est appliqué au moment de la définition de la fonction, et non lors de ses appels. 37 | Nous utilisons ici un décorateur très simple qui retourne la même fonction, mais il se pourrait très bien qu'il en retourne une autre, qui serait par exemple créée à la volée. 38 | 39 | Disons que nous aimerions modifier notre fonction `addition` pour afficher les opérandes puis le résultat, sans toucher au corps de notre fonction. Nous pouvons réaliser un décorateur qui retournera une nouvelle fonction se chargeant d'afficher les paramètres, d'appeler notre fonction originale, puis d'afficher le retour et de le retourner (afin de conserver le comportement original). 40 | 41 | Ainsi, notre décorateur devient : 42 | 43 | ```python 44 | def print_decorator(function): 45 | def new_function(a, b): # Nouvelle fonction se comportant comme la fonction à décorer 46 | print('Addition des nombres {} et {}'.format(a, b)) 47 | ret = function(a, b) # Appel de la fonction originale 48 | print('Retour: {}'.format(ret)) 49 | return ret 50 | return new_function # Ne pas oublier de retourner notre nouvelle fonction 51 | ``` 52 | 53 | Si on applique maintenant ce décorateur à notre fonction d'addition : 54 | 55 | ```python 56 | >>> @print_decorator 57 | ... def addition(a, b): 58 | ... return a + b 59 | ... 60 | >>> addition(1, 2) 61 | Addition des nombres 1 et 2 62 | Retour: 3 63 | 3 64 | ``` 65 | 66 | Mais notre décorateur est ici très spécialisé, il ne fonctionne qu'avec les fonctions prenant deux paramètres, et affichera « Addition » dans tous les cas. Nous pouvons le modifier pour le rendre plus générique (souvenez vous d'`*args` et `**kwargs`). 67 | 68 | ```python 69 | def print_decorator(function): 70 | def new_function(*args, **kwargs): 71 | print('Appel de la fonction {} avec args={} et kwargs={}'.format( 72 | function.__name__, args, kwargs)) 73 | ret = function(*args, **kwargs) 74 | print('Retour: {}'.format(ret)) 75 | return ret 76 | return new_function 77 | ``` 78 | 79 | Je vous laisse l'essayer sur des fonctions différentes pour vous rendre compte de sa généricité. 80 | 81 | Les définitions de fonctions ne sont pas limitées à un seul décorateur : il est possible d'en spécifier autant que vous le souhaitez, en les plaçant les uns à la suite des autres. 82 | 83 | ```python 84 | @decorator 85 | @print_decoration 86 | def useless(): 87 | pass 88 | ``` 89 | 90 | L'ordre dans lequel ils sont spécifiés est important, le code précédent équivaut à : 91 | 92 | ```python 93 | def useless(): 94 | pass 95 | useless = decorator(print_decorator(useless)) 96 | ``` 97 | 98 | On voit donc que les décorateurs spécifiés en premiers sont ceux qui seront appliqués en derniers. 99 | 100 | J'ai dit plus haut que les décorateurs s'appliquaient aux fonctions. C'est aussi valable pour les fonctions définies à l'intérieur de classes (les méthodes, donc). 101 | Mais sachez enfin que les décorateurs s'étendent aux déclarations de classes. 102 | 103 | ```python 104 | @print_decorator 105 | class MyClass: 106 | @decorator 107 | def method(self): 108 | pass 109 | ``` 110 | -------------------------------------------------------------------------------- /src/2-functions/3-decorators/2-parameterize.md: -------------------------------------------------------------------------------- 1 | ### Décorateurs paramétrés 2 | 3 | Nous avons vu comment appliquer un décorateur à une fonction, nous pourrions cependant vouloir paramétrer le comportement de ce décorateur. 4 | Dans notre exemple précédent (`print_decorator`), nous affichons du texte avant et après l'appel de fonction. Mais que faire si nous souhaitons modifier ce texte (pour en changer la langue, utiliser un autre terme que « fonction ») ? 5 | 6 | Nous ne voulons pas avoir à créer un décorateur différent pour chaque phrase possible et imaginable. Nous aimerions pouvoir passer nos chaînes de caractères à notre décorateur pour qu'il s'occupe de les afficher au moment opportun. 7 | 8 | En fait, `@` ne doit pas nécessairement être suivi d'un nom d'objet, des arguments peuvent aussi s'y ajouter à l'aide de parenthèses (comme on le ferait pour un appel). 9 | Mais le comportement peut vous sembler étrange au premier abord. 10 | 11 | Par exemple, pour utiliser un tel décorateur paramétré : 12 | 13 | ```python 14 | @param_print_decorator('call {} with args({}) and kwargs({})', 'ret={}') 15 | def test_func(x): 16 | return x 17 | ``` 18 | 19 | Il nous faudra posséder un *callable* `param_print_decorator` qui, quand il sera appelé, retournera un décorateur qui pourra ensuite être appliqué à notre fonction. 20 | Un décorateur paramétré n'est ainsi qu'un *callable* retournant un décorateur simple. 21 | 22 | Le code de `param_print_decorator` ressemblerait ainsi à : 23 | 24 | ```python 25 | def param_print_decorator(before, after): # Décorateur paramétré 26 | def decorator(function): # Décorateur 27 | def new_function(*args, **kwargs): # Fonction qui remplacera notre fonction décorée 28 | print(before.format(function.__name__, args, kwargs)) 29 | ret = function(*args, **kwargs) 30 | print(after.format(ret)) 31 | return ret 32 | return new_function 33 | return decorator 34 | ``` 35 | -------------------------------------------------------------------------------- /src/2-functions/3-decorators/3-wraps.md: -------------------------------------------------------------------------------- 1 | ### Envelopper une fonction 2 | 3 | Une fonction n'est pas seulement un bout de code avec des paramètres. C'est aussi un nom (des noms, avec ceux des paramètres), une documentation (*docstring*), des annotations, etc. 4 | Quand nous décorons une fonction à l'heure actuelle (dans les cas où nous en retournons une nouvelle), nous perdons toutes ces informations annexes. 5 | 6 | Un exemple pour nous en rendre compte : 7 | 8 | ```python 9 | >>> def decorator(f): 10 | ... def decorated(*args, **kwargs): 11 | ... return f(*args, **kwargs) 12 | ... return decorated 13 | ... 14 | >>> @decorator 15 | ... def addition(a: int, b: int) -> int: 16 | ... "Cette fonction réalise l'addition des paramètres `a` et `b`" 17 | ... return a + b 18 | ... 19 | >>> help(addition) 20 | Help on function decorated in module __main__: 21 | 22 | decorated(*args, **kwargs) 23 | ``` 24 | 25 | Alors, que voit-on ? Pas grand chose. 26 | Le nom qui apparaît est celui de `decorated`, les paramètres sont `*args` et `**kwargs` (sans annotations), et nous avons aussi perdu notre *docstring*. 27 | Autant dire qu'il ne reste rien pour comprendre ce que fait la fonction. 28 | 29 | #### Envelopper des fonctions 30 | 31 | Plus tôt dans ce cours, je vous parlais du module [`functools`](https://docs.python.org/3/library/functools.html). 32 | Il ne nous a pas encore révélé tous ses mystères. 33 | 34 | Nous allons ici nous intéresser aux fonctions `update_wrapper` et `wraps`. 35 | Ces fonctions vont nous permettre de copier les informations d'une fonction vers une nouvelle. 36 | 37 | `update_wrapper` prend en premier paramètre la fonction à laquelle ajouter les informations et celle dans laquelle les puiser en second. Pour reprendre notre exemple précédent, il nous faudrait faire : 38 | 39 | ```python 40 | import functools 41 | 42 | def decorator(f): 43 | def decorated(*args, **kwargs): 44 | return f(*args, **kwargs) 45 | functools.update_wrappers(decorated, f) # Nous copions les informations de `f` dans `decorated` 46 | return decorated 47 | ``` 48 | 49 | Mais une autre fonction nous sera bien plus utile car plus concise, et recommandée par la documentation Python pour ce cas. 50 | Il s'agit de `wraps`, qui retourne un décorateur lorsqu'appelé avec une fonction. 51 | 52 | La fonction décorée par `wraps` prendra les informations de la fonction passée à l'appel de `wraps`. 53 | Ainsi, nous n'aurons qu'à précéder toutes nos fonctions decorées par `@functools.wraps(fonction_a_decorer)`. Dans notre exemple : 54 | 55 | ```python 56 | import functools 57 | 58 | def decorator(f): 59 | @functools.wraps(f) 60 | def decorated(*args, **kwargs): 61 | return f(*args, **kwargs) 62 | return decorated 63 | ``` 64 | 65 | Vous pouvez maintenant redéfinir la fonction `addition`, et tester à nouveau l'appel à `help` pour constater les différences. 66 | -------------------------------------------------------------------------------- /src/2-functions/3-decorators/4-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : Arguments positionnels 2 | 3 | Nous avons vu avec les signatures qu'il existait en Python des paramètres *positional-only*, 4 | c'est-à-dire qui ne peuvent recevoir que des arguments positionnels. 5 | 6 | Mais il n'existe à ce jour aucune syntaxe pour écrire en Python une fonction avec des paramètres *positional-only*. 7 | Il est seulement possible, comme nous l'avons fait au TP précédent, de récupérer les arguments positionnels avec `*args` et d'en extraire les valeurs qui nous intéressent. 8 | 9 | Nous allons alors développer un décorateur pour pallier à ce manque. 10 | Ce décorateur modifiera la signature de la fonction reçue pour transformer en *positional-only* ses `n` premiers paramètres. 11 | `n` sera un paramètre du décorateur. 12 | 13 | Python nous permet de redéfinir la signature d'une fonction, en assignant la nouvelle signature à son attribut `__signature__`. 14 | Mais cette redéfinition n'est que cosmétique (elle apparaît par exemple dans l'aide de la fonction). 15 | Ici, nous voulons que la modification ait un effet. 16 | 17 | Nous créerons donc une fonction *wrapper*, qui se chargera de vérifier la conformité des arguments avec la nouvelle signature. 18 | 19 | Nous allons diviser le travail en deux parties : 20 | 21 | * Dans un premier temps, nous réaliserons une fonction pour réécrire une signature ; 22 | * Puis dans un second temps, nous écrirons le code du décorateur. 23 | 24 | #### Réécriture de la signature 25 | 26 | La première fonction, que nous appellerons `signature_set_positional`, recevra en paramètres une signature et un nombre `n` de paramètres à passer en *positional-only*. 27 | La fonction retournera une signature réécrite. 28 | 29 | Nous utiliserons donc les méthodes `replace` de la signature et des paramètres, pour changer le positionnement des paramètres ciblés, et mettre à jour la liste des paramètres de la signature. 30 | 31 | La fonction itérera sur les `n` premiers paramètres, pour les convertir en *positional-only*. 32 | 33 | On distinguera trois cas : 34 | 35 | * Le paramètre est déjà *positional-only*, il n'y a alors rien à faire ; 36 | * Le paramètre est *positional-or-keyword*, il peut être transformé ; 37 | * Le paramètre est d'un autre type, il ne peut pas être transformé, on lèvera alors une erreur. 38 | 39 | Puis une nouvelle signature sera créée et retournée avec cette nouvelle liste de paramètres. 40 | 41 | ```python 42 | def signature_set_positional(sig, n): 43 | params = list(sig.parameters.values()) # Liste des paramètres 44 | if len(params) < n: 45 | raise TypeError('Signature does not have enough parameters') 46 | for i, param in zip(range(n), params): # Itère sur les n premiers paramètres 47 | if param.kind == param.POSITIONAL_ONLY: 48 | continue 49 | elif param.kind == param.POSITIONAL_OR_KEYWORD: 50 | params[i] = param.replace(kind=param.POSITIONAL_ONLY) 51 | else: 52 | raise TypeError('{} parameter cannot be converted to POSITIONAL_ONLY'.format(param.kind)) 53 | return sig.replace(parameters=params) 54 | ``` 55 | 56 | #### Décorateur paramétré 57 | 58 | Passons maintenant à `positional_only`, notre décorateur paramétré. 59 | Pour rappel, un décorateur paramétré est une fonction qui retourne un décorateur. 60 | Et un décorateur est une fonction qui reçoit une fonction et retourne une fonction. 61 | 62 | Le décorateur proprement dit se chargera de calculer la nouvelle signature et de l'appliquer à la fonction décorée. 63 | Il créera aussi un *wrapper* à la fonction, lequel se chargera de vérifier la correspondance des arguments avec la signature. 64 | 65 | Nous n'oublierons pas d'appliquer `functools.wraps` à notre *wrapper* pour récupérer les informations de la fonction initiale. 66 | 67 | ```python 68 | import functools 69 | import inspect 70 | 71 | def positional_only(n): 72 | def decorator(f): 73 | sig = signature_set_positional(inspect.signature(f), n) 74 | @functools.wraps(f) 75 | def decorated(*args, **kwargs): 76 | bound = sig.bind(*args, **kwargs) 77 | return f(*bound.args, **bound.kwargs) 78 | decorated.__signature__ = sig 79 | return decorated 80 | return decorator 81 | ``` 82 | 83 | Voyons maintenant l'utilisation. 84 | 85 | ```python 86 | >>> @positional_only(2) 87 | ... def addition(a, b): 88 | ... return a + b 89 | ... 90 | >>> print(inspect.signature(addition)) 91 | (a, b, /) 92 | >>> addition(3, 5) 93 | 8 94 | >>> addition(3, b=5) 95 | Traceback (most recent call last): 96 | File "", line 1, in 97 | TypeError: 'b' parameter is positional only, but was passed as a keyword 98 | ``` 99 | 100 | ```python 101 | >>> @positional_only(1) 102 | ... def addition(a, b): 103 | ... return a + b 104 | ... 105 | >>> addition(3, 5) 106 | 8 107 | >>> addition(3, b=5) 108 | 8 109 | >>> addition(a=3, b=5) 110 | Traceback (most recent call last): 111 | File "", line 1, in 112 | TypeError: 'a' parameter is positional only, but was passed as a keyword 113 | ``` 114 | -------------------------------------------------------------------------------- /src/2-functions/3-decorators/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Les ressources sur les décorateurs sont peu nombreuses dans la documentation, car il ne s'agit au final que d'un sucre syntaxique. 4 | Je vous indique tout de même les deux principales pages qui les évoquent. 5 | 6 | * Définition du terme décorateur : 7 | * Syntaxe de déclaration d'une fonction : 8 | -------------------------------------------------------------------------------- /src/3-further/0-further.md: -------------------------------------------------------------------------------- 1 | # Plus loin, un peu plus loin 2 | 3 | ##### Au-delà des mers 4 | 5 | Continuons notre voyage, et voguons vers de nouveaux horizons. 6 | Aventurons-nous maintenant au sein d'objets plus complexes du langage. 7 | -------------------------------------------------------------------------------- /src/3-further/1-generators/0-generators.md: -------------------------------------------------------------------------------- 1 | ## Générateurs 2 | 3 | Nous étudierons dans ce chapitre les générateurs, un nouveau type d'itérables. 4 | -------------------------------------------------------------------------------- /src/3-further/1-generators/1-generator.md: -------------------------------------------------------------------------------- 1 | ### Dessine-moi un générateur 2 | 3 | Les générateurs sont donc des itérables, mais aussi des itérateurs, ce qui implique qu'ils se consomment quand on les parcourt (et que nous ne pouvons donc les parcourir qu'une seule fois). 4 | 5 | Ils sont généralement créés par des fonctions construites à l'aide du mot clef `yield`. 6 | Par abus de langage ces fonctions génératrices sont parfois elles-mêmes appelées générateurs. 7 | 8 | #### Le mot-clef `yield` 9 | 10 | Un générateur est donc construit à partir d'une fonction. Pour être génératrice, une fonction doit contenir un ou plusieurs `yield`. 11 | 12 | Lors de l'appel, la fonction retournera un générateur, et à chaque appel à la fonction *builtin* `next` sur ce générateur, le code jusqu'au prochain `yield` sera exécuté. 13 | Comme pour tout itérateur, la fonction `next` appelle la méthode spéciale `__next__` du générateur. 14 | 15 | `yield` peut-être ou non suivi d'une expression. La valeur retournée par `next` sera celle apposée au `yield`, ou `None` dans le cas où aucune valeur n'est spécifiée. 16 | 17 | Un exemple pour mieux comprendre cela : 18 | 19 | ```python 20 | >>> def function(): 21 | ... yield 4 22 | ... yield 23 | ... yield 'hej' 24 | ... 25 | >>> gen = function() 26 | >>> next(gen) 27 | 4 28 | >>> next(gen) 29 | None 30 | >>> next(gen) 31 | 'hej' 32 | >>> next(gen) 33 | Traceback (most recent call last): 34 | File "", line 1, in 35 | StopIteration 36 | >>> list(gen) # Tout le générateur a été parcouru 37 | [] 38 | ``` 39 | 40 | Ou avec un `for` : 41 | 42 | ```python 43 | >>> for i in function(): 44 | ... print(i) 45 | ... 46 | 4 47 | None 48 | hej 49 | ``` 50 | 51 | Il est aussi possible pour une fonction génératrice d'utiliser `return`, qui aura pour effet de le stopper (`StopIteration`). 52 | La valeur passée au `return` sera contenue dans l'exception levée. 53 | 54 | ```python 55 | >>> def function(): 56 | ... yield 4 57 | ... yield 58 | ... return 2 59 | ... yield 'hej' 60 | ... 61 | >>> gen = function() 62 | >>> next(gen) 63 | 4 64 | >>> next(gen) 65 | None 66 | >>> next(gen) 67 | Traceback (most recent call last): 68 | File "", line 1, in 69 | StopIteration: 2 70 | ``` 71 | 72 | Le générateur en lui-même ne retourne rien (il n'est pas *callable*), il produit des valeurs à l'aide de `yield`. 73 | Pour bien faire la différence entre notre générateur et sa fonction génératrice, on peut regarder ce qu'en dit Python. 74 | 75 | ```python 76 | >>> gen 77 | 78 | >>> function 79 | 80 | ``` 81 | 82 | Bien sûr, notre générateur est très simpliste dans l'exemple, mais toutes les structures de contrôle du Python peuvent y être utilisées. De plus, le générateur peut aussi être paramétré *via* les arguments passés à la fonction. 83 | Voici un exemple un peu plus poussé avec un générateur produisant les `n` premiers termes d'une [suite de Fibonacci](https://fr.wikipedia.org/wiki/Suite_de_Fibonacci) débutant par `a` et `b`. 84 | 85 | ```python 86 | >>> def fibonacci(n, a=0, b=1): 87 | ... for _ in range(n): 88 | ... yield a 89 | ... a, b = b, a + b 90 | ... 91 | >>> list(fibonacci(10)) 92 | [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 93 | >>> list(fibonacci(5, 6, 7)) 94 | [6, 7, 13, 20, 33] 95 | ``` 96 | -------------------------------------------------------------------------------- /src/3-further/1-generators/2-send.md: -------------------------------------------------------------------------------- 1 | ### Altérer un générateur avec `send` 2 | 3 | Maintenant que nous savons créer des générateurs, nous allons voir qu'il est possible de communiquer avec eux après leur création. 4 | 5 | Pour cela, les générateurs sont dotés d'une méthode `send`. Le paramètre reçu par cette méthode sera transmis au générateur. 6 | Mais comment le reçoit-il ? 7 | 8 | Au moment où il arrive sur une instruction `yield`, le générateur se met en pause. Lors de l'itération suivante, l'exécution reprend au niveau de ce même `yield`. 9 | Quand ensuite vous appelez la méthode `send` du générateur, en lui précisant un argument, l'exécution reprend ; et `yield` retourne la valeur passée à `send`. 10 | 11 | Attention donc, un appel à `send` produit une itération supplémentaire dans le générateur. `send` retourne alors la valeur de la prochaine itération comme le ferait `next`. 12 | 13 | ```python 14 | >>> gen = fibonacci(10) 15 | >>> next(gen) 16 | 0 17 | >>> next(gen) 18 | 1 19 | >>> gen.send('test') # Le send consomme une itération 20 | 1 21 | >>> next(gen) 22 | 2 23 | ``` 24 | 25 | Comme je le disais, une valeur peut-être retournée par l'instruction `yield`, c'est-à-dire dans le corps même de la fonction génératrice. 26 | Modifions quelque peu notre générateur `fibonacci` pour nous en apercevoir. 27 | 28 | ```python 29 | >>> def fibonacci(n, a=0, b=1): 30 | ... for _ in range(n): 31 | ... ret = yield a 32 | ... print('retour:', ret) 33 | ... a, b = b, a + b 34 | ... 35 | >>> gen = fibonacci(10) 36 | >>> next(gen) 37 | 0 38 | >>> next(gen) 39 | retour: None 40 | 1 41 | >>> gen.send('test') 42 | retour: test 43 | 1 44 | >>> next(gen) 45 | retour: None 46 | 2 47 | ``` 48 | 49 | Nous pouvons voir que lors de notre premier `next`, aucun retour n'est imprimé : c'est normal, le générateur n'étant encore jamais passé dans un `yield`, nous ne sommes pas encore arrivés jusqu'au premier appel à `print` (qui se trouve après le premier `yield`). 50 | 51 | Cela signifie aussi qu'il est impossible d'utiliser `send` avant le premier `yield` (puisqu'il n'existe aucun précédent `yield` qui retournerait la valeur). 52 | 53 | ```python 54 | >>> gen = fibonacci(10) 55 | >>> gen.send('test') 56 | Traceback (most recent call last): 57 | File "", line 1, in 58 | TypeError: can´t send non-None value to a just-started generator 59 | ``` 60 | 61 | Pour comprendre un peu mieux l'intérêt de `send`, nous allons implémenter une file (*queue*) par l'intermédiaire d'un générateur. Celle-ci sera construite à l'aide des arguments donnés à l'appel, retournera le premier élément à chaque itération (en le retirant de la file). 62 | On pourra aussi ajouter de nouveaux éléments à cette *queue* *via* `send`. 63 | 64 | ```python 65 | def queue(*args): 66 | elems = list(args) 67 | while elems: 68 | new = yield elems.pop(0) 69 | if new is not None: 70 | elems.append(new) 71 | ``` 72 | 73 | Testons un peu pour voir. 74 | 75 | ```python 76 | >>> q = queue('a', 'b', 'c') 77 | >>> next(q) 78 | 'a' 79 | >>> q.send('d') 80 | 'b' 81 | >>> next(q) 82 | 'c' 83 | >>> next(q) 84 | 'd' 85 | ``` 86 | 87 | Et si nous souhaitons itérer sur notre file : 88 | 89 | ```python 90 | >>> q = queue('a', 'b', 'c') 91 | >>> for letter in q: 92 | ... if letter == 'a': 93 | ... q.send('d') 94 | ... print(letter) 95 | ... 96 | 'b' 97 | a 98 | c 99 | d 100 | ``` 101 | 102 | Que se passe-t-il ? En fait, `send` consommant une itération, le `b` n'est pas obtenu via le `for`, mais en retour de `send`, et directement imprimé sur la sortie de l'interpréteur (avant même le `print` de `a` puisque le `send` est fait avant). 103 | 104 | Nous pouvons assigner le retour de `q.send` afin d'éviter que l'interpréteur n'en imprime le résultat, mais cela ne changerait pas tellement le problème : nous ne tomberons jamais sur `'b'` dans nos itérations du `for`. 105 | 106 | Pour obtenir le comportement attendu, nous pourrions avancer dans les itérations uniquement si le dernier `yield` a renvoyé `None` (un `yield` renvoyant `None` étant considéré comme un `next`). 107 | Comment faire cela ? Par une boucle qui exécute `yield` jusqu'à ce qu'il retourne `None`. Ce `yield` n'aura pas de paramètre spécifique, cette valeur étant celle retournée ensuite par `send`, elle ne nous intéresse pas ici. 108 | 109 | Ainsi, les `send` ne consommeront qu'une itération de la sous-boucle, tandis que les « vraies » itérations seront réservées aux `next`. 110 | 111 | ```python 112 | def queue(*args): 113 | elems = list(args) 114 | while elems: 115 | new = yield elems.pop(0) 116 | while new is not None: 117 | elems.append(new) 118 | new = yield 119 | ``` 120 | 121 | ```python 122 | >>> q = queue('a', 'b', 'c') 123 | >>> for letter in q: 124 | ... if letter == 'a': 125 | ... q.send('d') 126 | ... print(letter) 127 | ... 128 | a 129 | b 130 | c 131 | d 132 | ``` 133 | -------------------------------------------------------------------------------- /src/3-further/1-generators/3-methods.md: -------------------------------------------------------------------------------- 1 | ### Méthodes des générateurs 2 | 3 | Nous avons vu que les générateurs possédaient des méthodes `__next__` et `send`. 4 | Nous allons maintenant nous intéresser aux deux autres méthodes de ces objets : `throw` et `close`. 5 | 6 | #### `throw` 7 | 8 | La méthode `throw` permet de lever une exception depuis le générateur, à l'endroit où ce dernier s'est arrêté. 9 | Elle a pour effet de réveiller le générateur pour lui faire lever une exception du type indiqué. 10 | 11 | Il s'agit alors d'une autre manière d'influer sur l'exécution d'un générateur, par des événements qui lui sont extérieurs. 12 | 13 | `throw` possède 3 paramètres dont deux facultatifs : 14 | 15 | * Le premier, `type`, est le type d'exception à lever ; 16 | * Le second, `value`, est la valeur à passer en instanciant cette exception ; 17 | * Et le troisième, `traceback`, permet de passer un pile d'appel (*traceback object*) particulière à l'exception. 18 | 19 | L'exception survient donc au niveau du `yield`, et peut tout à fait être attrapée par le générateur. 20 | Si c'est le cas, `throw` retournera la prochaine valeur produite par le générateur, ou lèvera une exception `StopIteration` indiquant que le générateur a été entièrement parcouru, à la manière de `next`. 21 | 22 | ```python 23 | >>> def generator_function(): 24 | ... for i in range(5): 25 | ... try: 26 | ... yield i 27 | ... except ValueError: 28 | ... print('Something goes wrong') 29 | ... 30 | >>> gen = generator_function() 31 | >>> next(gen) 32 | 0 33 | >>> next(gen) 34 | 1 35 | >>> gen.throw(ValueError) 36 | Something goes wrong 37 | 2 38 | >>> next(gen) 39 | 3 40 | >>> next(gen) 41 | 4 42 | >>> gen.throw(ValueError) 43 | Something goes wrong 44 | Traceback (most recent call last): 45 | File "", line 1, in 46 | StopIteration 47 | ``` 48 | 49 | Si l'exception n'est pas attrapée par le générateur, elle provoque alors sa fermeture, et remonte logiquement jusqu'à l'objet ayant fait appel à lui. 50 | 51 | ```python 52 | >>> def generator_function(): 53 | ... for i in range(5): 54 | ... yield i 55 | ... 56 | >>> gen = generator_function() 57 | >>> next(gen) 58 | 0 59 | >>> next(gen) 60 | 1 61 | >>> gen.throw(ValueError) 62 | Traceback (most recent call last): 63 | File "", line 1, in 64 | File "", line 3, in generator_function 65 | ValueError 66 | >>> next(gen) 67 | Traceback (most recent call last): 68 | File "", line 1, in 69 | StopIteration 70 | ``` 71 | 72 | #### `close` 73 | 74 | Un cas particulier d'appel à la méthode `throw` est de demander au généraeur de s'arrêter en lui faisant lever une exception `GeneratorExit`. 75 | 76 | À la réception de cette dernière, il est attendu que le générateur se termine (`StopIteration`) ou lève à son tour une `GeneratorExit` (par exemple en n'attrapant pas l'exception survenue). 77 | 78 | La méthode `close` du générateur permet de réaliser cet appel et d'attraper les `StopIteration`/`GeneratorExit` en retour. 79 | 80 | ```python 81 | >>> def generator_function(): 82 | ... for i in range(5): 83 | ... yield i 84 | ... 85 | >>> gen = generator_function() 86 | >>> next(gen) 87 | 0 88 | >>> next(gen) 89 | 1 90 | >>> gen.close() 91 | >>> next(gen) 92 | Traceback (most recent call last): 93 | File "", line 1, in 94 | StopIteration 95 | ``` 96 | 97 | ```python 98 | >>> def generator_function(): 99 | ... for i in range(5): 100 | ... try: 101 | ... yield i 102 | ... except GeneratorExit: 103 | ... break 104 | ... 105 | >>> gen = generator_function() 106 | >>> next(gen) 107 | 0 108 | >>> next(gen) 109 | 1 110 | >>> gen.close() 111 | >>> next(gen) 112 | Traceback (most recent call last): 113 | File "", line 1, in 114 | StopIteration 115 | ``` 116 | 117 | Puisqu'elle attrape les `StopIteration`, il est possible d'appeler plusieurs fois la méthode `close` sur un même générateur. 118 | 119 | ```python 120 | >>> gen.close() 121 | >>> gen.close() 122 | ``` 123 | 124 | `close` s'occupe aussi de lever une `RuntimeError` dans le cas où le générateur ne s'arrêterait pas et continuerait à produire des valeurs. 125 | 126 | ```python 127 | >>> def generator_function(): 128 | ... for i in range(5): 129 | ... try: 130 | ... yield i 131 | ... except GeneratorExit: 132 | ... print('Ignoring') 133 | ... 134 | >>> gen = generator_function() 135 | >>> next(gen) 136 | 0 137 | >>> next(gen) 138 | 1 139 | >>> gen.close() 140 | Ignoring 141 | Traceback (most recent call last): 142 | File "", line 1, in 143 | RuntimeError: generator ignored GeneratorExit 144 | >>> next(gen) 145 | 3 146 | ``` 147 | -------------------------------------------------------------------------------- /src/3-further/1-generators/4-yield-from.md: -------------------------------------------------------------------------------- 1 | ### Déléguer à un autre générateur avec `yield from` 2 | 3 | Nous savons produire les valeurs d'un générateur à l'aide du mot clef `yield`. 4 | Voyons maintenant quelque chose d'un peu plus complexe avec `yield from`. 5 | Ce nouveau mot clef permet de déléguer l'itération à un sous-générateur pris en paramètre. 6 | La rencontre du `yield from` provoque une interruption du générateur courant, le temps d'itérer et produire les valeurs du générateur délégué. 7 | 8 | ```python 9 | def big_queue(): 10 | yield 0 11 | yield from queue(1, 2, 3) 12 | yield 4 13 | ``` 14 | 15 | Celui-ci agit comme si nous itérions sur `queue(1, 2, 3)` depuis `big_queue`, tout en *yieldant* toutes ses valeurs. 16 | 17 | ```python 18 | def big_queue(): 19 | yield 0 20 | for value in queue(1, 2, 3): 21 | yield value 22 | yield 4 23 | ``` 24 | 25 | À la différence près qu'avec `yield from`, les paramètres passés lors d'un `send` sont aussi relégués aux sous-générateurs. 26 | Tout comme sont relayés aux sous-générateurs les appels aux méthodes `throw` et `close`. 27 | 28 | Dans notre première version, nous pouvons nous permettre ceci : 29 | 30 | ```python 31 | >>> q = big_queue() 32 | >>> next(q) 33 | 0 34 | >>> next(q) 35 | 1 36 | >>> q.send('foo') 37 | >>> next(q) 38 | 2 39 | >>> next(q) 40 | 3 41 | >>> next(q) 42 | 'foo' 43 | >>> next(q) 44 | 4 45 | >>> next(q) 46 | Traceback (most recent call last): 47 | File "", line 1, in 48 | StopIteration 49 | ``` 50 | 51 | Où l'on voit bien que le `send` est pris en compte par la sous-*queue*, et la valeur ajoutée à la file. 52 | Je vous invite à essayer avec la seconde implémentation de `big_queue` (celle sans `yield from`), pour bien observer l'effet de la délégation du `send`. 53 | 54 | On peut aussi noter que `yield from` n'attend pas nécessairement un générateur en paramètre, mais n'importe quel type d'itérable. 55 | 56 | ```python 57 | def gen_from_iterables(): 58 | yield from [1, 2, 3] 59 | yield from 'abcdef' 60 | yield from {'x': 1, 'y': -1} 61 | ``` 62 | 63 | Et par exemple, si nous voulions réécrire la fonction `chain` du module `itertools`, nous pourrions procéder ainsi : 64 | 65 | ```python 66 | def chain(*iterables): 67 | for iterable in iterables: 68 | yield from iterable 69 | ``` 70 | -------------------------------------------------------------------------------- /src/3-further/1-generators/5-comprehension.md: -------------------------------------------------------------------------------- 1 | ### Listes et générateurs en intension 2 | 3 | Intéressons-nous maintenant aux sucres syntaxiques que sont les listes et générateurs en intension. 4 | 5 | #### Listes en intension 6 | 7 | Vous connaissez probablement déjà les listes en intension (*comprehension lists*), mais je vais me permettre un petit rappel. 8 | 9 | Les listes en intension sont une syntaxe courte pour définir des listes à partir d'un itérable et d'une expression à appliquer sur chaque élément (un peu à la manière de `map`, qui lui ne permet que d'appeler un *callable* pour chaque l'élément). 10 | 11 | Une expression étant en Python tout ce qui possède une valeur (même `None`), c'est-à-dire ce qui n'est pas une instruction (`if`, `while`, etc.). 12 | Il faut noter que les ternaires sont des expressions (`a if predicat() else b`), et peuvent donc y être utilisés. 13 | 14 | Ainsi, 15 | 16 | ```python 17 | >>> squares = [x**2 for x in range(10)] 18 | >>> squares 19 | [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 20 | ``` 21 | 22 | sera l'équivalent de 23 | 24 | ```python 25 | squares = [] 26 | for x in range(10): 27 | squres.append(x**2) 28 | ``` 29 | 30 | ou encore de 31 | 32 | ```python 33 | squares = list(map(lambda x: x**2, range(10))) 34 | ``` 35 | 36 | Le gain en lisibilité est net. 37 | 38 | Cette syntaxe permet aussi d'appliquer des filtres, à la manière de `filter`. Par exemple si nous ne voulions que les carrés supérieurs à 10 : 39 | 40 | ```python 41 | >>> [x**2 for x in range(10) if x**2 >= 10] 42 | [16, 25, 36, 49, 64, 81] 43 | ``` 44 | 45 | Une liste en intension peut directement être passée en paramètre à une fonction. 46 | 47 | ```python 48 | >>> sum([x**2 for x in range(10)]) 49 | 285 50 | ``` 51 | 52 | Étant des expressions, elles peuvent être imbriquées dans d'autres listes en intension : 53 | 54 | ```python 55 | >>> [[x + y for x in range(5)] for y in range(3)] 56 | [[0, 1, 2, 3, 4], [1, 2, 3, 4, 5], [2, 3, 4, 5, 6]] 57 | ``` 58 | 59 | Enfin, il est possible de parcourir plusieurs niveaux de listes dans une seule liste en intension : 60 | 61 | ```python 62 | >>> matrix = [[0, 1, 2], [3, 4, 5], [6, 7, 8]] 63 | >>> [elem + 1 for line in matrix for elem in line] 64 | [1, 2, 3, 4, 5, 6, 7, 8, 9] 65 | ``` 66 | 67 | Vous noterez que les `for` se placent dans l'ordre des dimensions que nous voulons explorer : d'abord les lignes, puis les éléments qu'elles contiennent. 68 | 69 | #### Générateurs en intension 70 | 71 | De la même manière que pour les listes, nous pouvons définir des générateurs en intension (*generator expressions*). 72 | La syntaxe est très similaire, il suffit de remplacer les crochets par des parenthèses pour passer d'une liste à un générateur. 73 | 74 | ```python 75 | >>> squares = (x**2 for x in range(10)) 76 | >>> squares 77 | at 0x7f8b8a9a7090> 78 | ``` 79 | 80 | Et nous pouvons les utiliser tels que les autres itérables. 81 | 82 | ```python 83 | >>> list(squares) 84 | [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 85 | >>> list(squares) # En gardant à l'esprit qu'ils se consumment 86 | [] 87 | ``` 88 | 89 | Et pour finir, il est possible de simplifier encore la syntaxe quand un générateur en intension est seul paramètre d'une fonction, en supprimant les parenthèses redondantes. 90 | 91 | ```python 92 | >>> sum((x**2 for x in range(10))) 93 | 285 94 | >>> sum(x**2 for x in range(10)) # Une paire de parenthèses est retirée 95 | 285 96 | ``` 97 | -------------------------------------------------------------------------------- /src/3-further/1-generators/6-list-generators.md: -------------------------------------------------------------------------------- 1 | ### Liste ou générateur ? 2 | 3 | Une question qui revient souvent est celle de savoir quand choisir une liste et quand choisir un générateur, voici donc un petit comparatif : 4 | 5 | * Les listes prennent généralement plus de place en mémoire : tous les éléments existent en même temps. Dans le cas d'un générateur, l'élément n'existe que quand l'itérateur l'atteint, et n'existe plus après (sauf s'il est référencé autre part) ; 6 | * Les générateurs peuvent être infinis (contrairement aux listes qui occupent un espace qui se doit d'être fini). 7 | 8 | ```python 9 | >>> def infinite(): 10 | ... n = 0 11 | ... while True: 12 | ... yield n 13 | ... n += 1 14 | ``` 15 | 16 | * Les générateurs ne sont pas indexables : on ne peut accéder à un élément particulier (il faut itérer jusque cet élément) ; 17 | * Les générateurs ont une durée de vie plus courte (ils ne contiennent plus rien une fois qu'ils ont été itérés en entier) ; 18 | * Du fait que les générateurs n'occupent que peu de place en mémoire, on peut les enchaîner sans crainte. 19 | 20 | ```python 21 | numbers = (x**2 for x in range(100)) 22 | numbers = zip(infinite(), numbers) 23 | numbers = (a + b for (a, b) in numbers) 24 | ``` 25 | 26 | Ainsi, les éléments du premier générateur ne seront calculés qu'au parcours de `numbers`. 27 | Il est aussi possible de profiter des avantages de l'un et de l'autre en récupérant une liste en fin de chaîne, par exemple en remplaçant la dernière ligne par : 28 | 29 | ```python 30 | numbers = [a + b for (a, b) in numbers] 31 | ``` 32 | 33 | Ou en ajoutant à la fin : 34 | 35 | ```python 36 | numbers = list(numbers) 37 | ``` 38 | -------------------------------------------------------------------------------- /src/3-further/1-generators/7-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : `map` 2 | 3 | Dans ce TP, nous allons réaliser un équivalent de la fonction `map`, que nous appellerons `my_map`. 4 | 5 | Pour rappel, `map` permet d'appliquer une fonction sur tous les éléments d'un itérable. 6 | Elle reçoit en paramètres la fonction à appliquer et l'itérable, 7 | et retourne un nouvel itérable correspondant à l'application de la fonction sur chacune des valeurs de l'itérable d'entrée. 8 | 9 | Un générateur se prête donc très bien à cet exercice : nous ferons de `my_map` une fonction génératrice. 10 | 11 | Elle se contentera dans un premier temps d'itérer sur l'entrée, et de *yielder* le résultat obtenu pour chaque élément. 12 | 13 | ```python 14 | def my_map(f, iterable): 15 | for item in iterable: 16 | yield f(item) 17 | ``` 18 | 19 | Cette implémentation répond très bien à la problématique initiale. 20 | 21 | ```python 22 | >>> for x in my_map(lambda x: x*2, range(10)): 23 | ... print(x) 24 | ... 25 | 0 26 | 2 27 | 4 28 | 6 29 | 8 30 | 10 31 | 12 32 | 14 33 | 16 34 | 18 35 | ``` 36 | 37 | Seulement, pour aller un peu plus loin et mettre en pratique ce que nous avons vu dans le chapitre, nous allons faire en sorte de pouvoir changer la fonction `f` en cours de route. 38 | 39 | Pour ce faire, nous communiquerons avec notre générateur à l'aide de sa méthode `send`. 40 | Nous voulons donc que chaque fois que `yield` retourne autre chose que `None`, cette valeur soit utilisée comme nouvelle fonction. 41 | 42 | Une approche simpliste serait la suivante : 43 | 44 | ```python 45 | def my_map(f, iterable): 46 | for item in iterable: 47 | ret = yield f(item) 48 | if ret is not None: 49 | f = ret 50 | ``` 51 | 52 | Mais comme `send` provoque une itération supplémentaire du générateur, elle a l'inconvénient de faire perdre des valeurs. 53 | (les valeurs ne sont pas vraiment perdues, mais sont les valeurs de retour de `send`) 54 | 55 | ```python 56 | >>> gen = my_map(lambda x: x+1, range(10)) 57 | >>> for x in gen: 58 | ... print('Got', x) 59 | ... if x == 5: 60 | ... gen.send(lambda x: x+2) 61 | ... 62 | Got 1 63 | Got 2 64 | Got 3 65 | Got 4 66 | Got 5 67 | 7 68 | Got 8 69 | Got 9 70 | Got 10 71 | Got 11 72 | ``` 73 | 74 | L'affichage du `7` étant dû à l'interpréteur interactif qui affiche la valeur de retour du `send`. 75 | 76 | Pour palier à ce petit problème, nous pouvons dans notre générateur *yielder* `None` quand une fonction a été reçue. 77 | Ainsi, le retour du `send` sera le `None` transmis par `yield`, et la valeur ne sera pas perdue. 78 | 79 | Nous réécrivons alors notre générateur en conséquence. 80 | 81 | ```python 82 | def my_map(f, iterable): 83 | for item in iterable: 84 | ret = yield f(item) 85 | if ret is not None: 86 | f = ret 87 | yield None 88 | ``` 89 | 90 | Et observons la correction. 91 | 92 | ```python 93 | >>> gen = my_map(lambda x: x+1, range(10)) 94 | >>> for x in gen: 95 | ... print('Got', x) 96 | ... if x == 5: 97 | ... gen.send(lambda x: x+2) 98 | ... 99 | Got 1 100 | Got 2 101 | Got 3 102 | Got 4 103 | Got 5 104 | Got 7 105 | Got 8 106 | Got 9 107 | Got 10 108 | Got 11 109 | ``` 110 | 111 | Mais notre générateur reste sujet à un problème plus subtil : des pertes se produisent si nous appelons `yield` plusieurs fois d'affilée. 112 | 113 | ```python 114 | >>> gen = my_map(lambda x: x+1, range(10)) 115 | >>> for x in gen: 116 | ... print('Got', x) 117 | ... if x == 5: 118 | ... gen.send(lambda x: x+2) 119 | ... gen.send(lambda x: x*2) 120 | ... 121 | Got 1 122 | Got 2 123 | Got 3 124 | Got 4 125 | Got 5 126 | 7 127 | Got 8 128 | Got 9 129 | Got 10 130 | Got 11 131 | ``` 132 | 133 | En effet, lors du premier `send`, nous retrons dans la condition du générateur pour arriver sur le `yield None`, valeur retournée par `send`. 134 | Mais nous ne nous inquiétons pas de savoir ce que retourne ce second `yield` (en l'occurrence, la fonction envoyée par le second `send`, qui n'est jamais prise en compte). 135 | 136 | Vous l'aurez compris, il nous suffira donc de mettre en place une boucle autour du `yield None` et n'en sortir qu'une fois qu'il aura retourné `None`. 137 | Chaque valeur différente sera enregistrée comme nouvelle fonction `f`. 138 | 139 | ```python 140 | def my_map(f, iterable): 141 | for value in iterable: 142 | newf = yield f(value) 143 | while newf is not None: 144 | f = newf 145 | newf = yield None 146 | ``` 147 | 148 | Cette fois-ci, plus de problèmes ! 149 | 150 | ```python 151 | >>> gen = my_map(lambda x: x+1, range(10)) 152 | >>> for x in gen: 153 | ... print('Got', x) 154 | ... if x == 5: 155 | ... gen.send(lambda x: x+2) 156 | ... gen.send(lambda x: x*2) 157 | ... 158 | Got 1 159 | Got 2 160 | Got 3 161 | Got 4 162 | Got 5 163 | Got 10 164 | Got 12 165 | Got 14 166 | Got 16 167 | Got 18 168 | ``` 169 | -------------------------------------------------------------------------------- /src/3-further/1-generators/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Quelques pages habituelles tirées de la documentation : 4 | 5 | * Définition du terme générateur : 6 | * Expressions `yield` : 7 | 8 | Je profite aussi de la fin de ce chapitre pour vous conseiller cet article de [**nohar**](https://zestedesavoir.com/membres/voir/nohar/) sur les coroutines, une utilisation possible et courante des générateurs : 9 | 10 | J'aurais aimé abordé le sujet des coroutines et de la programmation asynchrone en Python, mais un cours entier sur le sujet n'est pas de trop ! 11 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/0-context-managers.md: -------------------------------------------------------------------------------- 1 | ## Gestionnaires de contexte 2 | 3 | Avant de parler de cette spécificité du langage, je voudrais expliciter la notion de contexte. 4 | Un contexte est une portion de code cohérente, avec des garanties en entrée et en sortie. 5 | 6 | Par exemple, pour la lecture d'un fichier, on garantit que celui-ci soit ouvert et accessible en écriture en entrée (donc à l'intérieur du contexte), et l'on garantit sa fermeture en sortie (à l'extérieur). 7 | 8 | De multiples utilisations peuvent être faites des contextes, comme l'allocation et la libération de ressources (fichiers, verrous, etc.), ou encore des modifications temporaires sur l'environnement courant (répertoire de travail, redirection d'entrées/sorties). 9 | 10 | Python met à notre disposition des gestionnaires de contexte, c'est-à-dire une structure de contrôle pour les mettre en place, à l'aide du mot-clef `with`. 11 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/1-with.md: -------------------------------------------------------------------------------- 1 | ### `with` or without you 2 | 3 | Un contexte est ainsi un scope particulier, avec des opérations exécutées en entrée et en sortie. 4 | 5 | Un bloc d'instructions `with` se présente comme suit. 6 | 7 | ```python 8 | with expr as x: # avec expr étant un gestionnaire de contexte 9 | ... # operations sur x 10 | ``` 11 | 12 | La syntaxe est assez simple à appréhender, `x` permettra ici de contenir des données propres au contexte (`x` vaudra `expr` dans la plupart des cas). 13 | Si par exemple `expr` correspondait à une ressource, la libération de cette ressource (fermeture du fichier, déblocage du verrou, etc.) serait gérée pour nous en sortie du scope, dans tous les cas. 14 | 15 | Il est aussi possible de gérer plusieurs contextes dans un même bloc : 16 | 17 | ```python 18 | with expr1 as x, expr2 as y: 19 | ... # traitements sur x et y 20 | ``` 21 | 22 | équivalent à 23 | 24 | ```python 25 | with expr1 as x: 26 | with expr2 as y: 27 | ... # traitements sur x et y 28 | ``` 29 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/2-open.md: -------------------------------------------------------------------------------- 1 | ### La fonction `open` 2 | 3 | L'un des gestionnaires de contexte les plus connus est probablement le fichier, tel que retourné par la fonction `open`. 4 | Jusque là, vous avez pu l'utiliser de la manière suivante : 5 | 6 | ```python 7 | f = open('filename', 'w') 8 | # traitement sur le fichier 9 | ... 10 | f.close() 11 | ``` 12 | 13 | Mais sachez que ça n'est pas la meilleure façon de procéder. En effet, si une exception survient pendant le traitement, la méthode `close` ne sera par exemple jamais appelée, et les dernières données écrites pourraient être perdues. 14 | 15 | Il est donc conseillé de plutôt procéder de la sorte, avec `with` : 16 | 17 | ```python 18 | with open('filename', 'w') as f: 19 | # traitement sur le fichier 20 | ... 21 | ``` 22 | 23 | Ici, la fermeture du fichier est implicite, nous verrons plus loin comment cela fonctionne en interne. 24 | 25 | Nous pourrions reproduire un comportement similaire sans gestionnaire de contexte, mais le code serait un peu plus complexe. 26 | 27 | ```python 28 | try: 29 | f = open('filename', 'w') 30 | # traitement sur le fichier 31 | ... 32 | finally: 33 | f.close() 34 | ``` 35 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/3-internal.md: -------------------------------------------------------------------------------- 1 | ### Fonctionnement interne 2 | 3 | Ça, c'est pour le cas d'utilisation, nous étudierons ici le fonctionnement interne. 4 | 5 | Les gestionnaires de contexte sont en fait des objets disposant de deux méthodes spéciales : `__enter__` et `__exit__`, qui seront respectivement appelées à l'entrée et à la sortie du bloc `with`. 6 | 7 | Le retour de la méthode `__enter__` sera attribué à la variable spécifiée derrière le `as`. 8 | 9 | Le bloc `with` est donc un bloc d'instructions très simple, offrant juste un sucre syntaxique autour d'un `try`/`except`/`finally`. 10 | 11 | `__enter__` ne prend aucun paramètre, contrairement à `__exit__` qui en prend 3 : `exc_type`, `exc_value`, et `traceback`. 12 | Ces paramètres interviennent quand une exception survient dans le bloc `with`, et correspondent au type de l'exception levée, à sa valeur, et à son *traceback*. 13 | Dans le cas où aucune exception n'est survenue pendant le traitement de la ressource, ces 3 paramètres valent `None`. 14 | 15 | `__exit__` retourne un booléen, intervenant dans la propagation des exceptions. En effet, si `True` est retourné, l'exception survenue dans le contexte sera attrapée. 16 | 17 | Nous pouvons maintenant créer notre propre type de gestionnaire, contentons-nous pour le moment de quelque chose d'assez simple qui afficherait un message à l'entrée et à la sortie. 18 | 19 | ```python 20 | class MyContext: 21 | def __enter__(self): 22 | print('enter') 23 | return self 24 | def __exit__(self, exc_type, exc_value, traceback): 25 | print('exit') 26 | ``` 27 | 28 | Et à l'utilisation : 29 | 30 | ```python 31 | >>> with MyContext() as ctx: 32 | ... print(ctx) 33 | ... 34 | enter 35 | <__main__.MyContext object at 0x7f23cc446cf8> 36 | exit 37 | ``` 38 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/4-contextlib.md: -------------------------------------------------------------------------------- 1 | ### Simplifions-nous la vie avec la `contextlib` 2 | 3 | La [`contextlib`](https://docs.python.org/3/library/contextlib.html) est un module de la bibliothèque standard comportant divers outils ou gestionnaires de contexte bien utiles. 4 | 5 | Par exemple, une classe, `ContextDecorator`, permet de transformer un gestionnaire de contexte en décorateur, et donc de pouvoir l'utiliser comme l'un ou comme l'autre. 6 | Cela peut s'avérer utile pour créer un module qui mesurerait le temps d'exécution d'un ensemble d'instructions : on peut vouloir s'en servir via `with`, ou via un décorateur autour de notre fonction à mesurer. 7 | 8 | Cet outil s'utilise très facilement, il suffit que notre gestionnaire de contexte hérite de `ContextDecorator`. 9 | 10 | ```python 11 | from contextlib import ContextDecorator 12 | import time 13 | 14 | class spent_time(ContextDecorator): 15 | def __enter__(self): 16 | self.start = time.time() 17 | def __exit__(self, *_): 18 | print('Elapsed {:.3}s'.format(time.time() - self.start)) 19 | ``` 20 | 21 | Et à l'utilisation : 22 | 23 | ```python 24 | >>> with spent_time(): 25 | ... print('x') 26 | ... 27 | x 28 | Elapsed 0.000106s 29 | >>> @spent_time() 30 | ... def func(): 31 | ... print('x') 32 | ... 33 | >>> func() 34 | x 35 | Elapsed 0.000108s 36 | ``` 37 | 38 | Intéressons-nous maintenant à `contextmanager`. Il s'agit d'un décorateur capable de transformer une fonction génératrice en *context manager*. 39 | Cette fonction génératrice devra disposer d'un seul et unique `yield`. 40 | Tout ce qui est présent avant le `yield` sera exécuté en entrée, et ce qui se situe ensuite s'exécutera en sortie. 41 | 42 | ```python 43 | >>> from contextlib import contextmanager 44 | >>> @contextmanager 45 | ... def context(): 46 | ... print('enter') 47 | ... yield 48 | ... print('exit') 49 | ... 50 | >>> with context(): 51 | ... print('during') 52 | ... 53 | enter 54 | during 55 | exit 56 | ``` 57 | 58 | Attention tout de même, une exception levée dans le bloc d'instructions du `with` remonterait jusqu'au générateur, et empêcherait donc l'exécution du `__exit__`. 59 | 60 | ```python 61 | >>> with context(): 62 | ... raise Exception 63 | ... 64 | enter 65 | Traceback (most recent call last): 66 | File "", line 2, in 67 | Exception 68 | ``` 69 | 70 | Il convient donc d'utiliser un `try`/`finally` si vous souhaitez vous assurer que la fin du générateur sera toujours exécutée. 71 | 72 | ```python 73 | >>> @contextmanager 74 | ... def context(): 75 | ... try: 76 | ... print('enter') 77 | ... yield 78 | ... finally: 79 | ... print('exit') 80 | ... 81 | >>> with context(): 82 | ... raise Exception 83 | ... 84 | enter 85 | exit 86 | Traceback (most recent call last): 87 | File "", line 2, in 88 | Exception 89 | ``` 90 | 91 | Enfin, le module contient divers gestionnaires de contexte, qui sont : 92 | 93 | * [`closing`](https://docs.python.org/3/library/contextlib.html#contextlib.closing) qui permet de fermer automatiquement un objet (par sa méthode `close`) ; 94 | * [`suppress`](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) afin de supprimer certaines exceptions survenues dans un contexte ; 95 | * [`redirect_stdout`](https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout) pour rediriger temporairement la sortie standard du programme. 96 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/5-reusability.md: -------------------------------------------------------------------------------- 1 | ### Réutilisabilité et réentrance 2 | 3 | #### Réutilisabilité 4 | 5 | Nous avons vu que la syntaxe du bloc `with` était `with expr as var`. 6 | Dans les exemples précédents, nous avions toujours une expression `expr` à usage unique, qui était évaluée pour le `with`. 7 | 8 | Mais un même gestionnaire de contexte pourrait être utilisé à plusieurs reprises si l'expression est chaque fois une même variable. 9 | En reprenant la classe `MyContext` définie plus tôt : 10 | 11 | ```python 12 | >>> ctx = MyContext() 13 | >>> with ctx: 14 | ... pass 15 | ... 16 | enter 17 | exit 18 | >>> with ctx: 19 | ... pass 20 | ... 21 | enter 22 | exit 23 | ``` 24 | 25 | `MyContext` est un gestionnaire de contexte réutilisable : on peut utiliser ses instances à plusieurs reprises dans des blocs `with` successifs. 26 | 27 | Mais les fichiers tels que retournés par `open` ne sont par exemple pas réutilisables : une fois sortis du bloc `with`, le fichier est fermé, il est donc impossible d'ouvrir un nouveau contexte. 28 | 29 | ```python 30 | >>> f = open('filename', 'r') 31 | >>> with f: 32 | ... pass 33 | ... 34 | >>> with f: 35 | ... pass 36 | ... 37 | Traceback (most recent call last): 38 | File "", line 1, in 39 | ValueError: I/O operation on closed file. 40 | ``` 41 | 42 | Notre gestionnaire `context` créé grâce au décorateur `contextmanager` n'est pas non plus réutilisable : il dépend d'un générateur qui ne peut être itéré qu'une fois. 43 | 44 | #### Réentrance 45 | 46 | Un cas particulier de la réutilisabilité est celui de la réentrance. 47 | Un gestionnaire de contexte est réentrant quand il peut être utilisé dans des `with` imbriqués. 48 | 49 | ```python 50 | >>> ctx = MyContext() 51 | >>> with ctx: 52 | ... with ctx: 53 | ... pass 54 | ... 55 | enter 56 | enter 57 | exit 58 | exit 59 | ``` 60 | 61 | On peut alors prendre l'exemple des classes `Lock` et `RLock` du module `threading`, qui servent à poser des verrous sur des ressources. 62 | Le premier est un gestionnaire réutilisable (seulement) et le second est réentrant. 63 | 64 | Pour bien distinguer la différences entre les deux, je vous propose les codes suivant. 65 | 66 | ```python 67 | >>> from threading import Lock 68 | >>> lock = Lock() 69 | >>> with lock: 70 | ... with lock: 71 | ... pass 72 | ... 73 | 74 | ``` 75 | 76 | Python bloque à l'exécution de ces instructions. 77 | En effet, le bloc intérieur demande l'accès à une ressource (`lock`) déjà occupée par le bloc extérieur. 78 | Python met en pause l'exécution en attendant que la ressource se libère. 79 | Mais celle-ci ne se libérera qu'en sortie du bloc exétieur, qui attend la fin de l'exécution du bloc intérieur. 80 | 81 | Les deux blocs s'attendent mutuellement, l'exécution ne se terminera donc jamais. 82 | On est ici dans un cas de blocage, appelé *dead lock*. 83 | Dans notre cas, nous pouvons sortir à l'aide d'un ||Ctrl+C|| ou en fermant l'interpréteur. 84 | 85 | Passons à `RLock` maintenant. 86 | 87 | ```python 88 | >>> from threading import RLock 89 | >>> lock = RLock() 90 | >>> with lock: 91 | ... with lock: 92 | ... pass 93 | ... 94 | ``` 95 | 96 | Celui-ci supporte les `with` imbriqués, il est réentrant. 97 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/6-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : Redirection de sortie (`redirect_stdout`) 2 | 3 | Nous allons ici mettre en place un gestionnaire de contexte équivalent à `redirect_stdout` pour rediriger la sortie standard vers un autre fichier. 4 | Il sera aussi utilisable en tant que décorateur pour rediriger la sortie standard de fonctions. 5 | 6 | La redirection de sortie est une opération assez simple en Python. 7 | La sortie standard est identifiée par l'attribut/fichier `stdout` du module `sys`. 8 | Pour rediriger la sortie standard, il suffit alors de faire pointer `sys.stdout` vers un autre fichier. 9 | 10 | Notre gestionnaire de contexte sera construit avec un fichier dans lequel rediriger la sortie. 11 | Nous enregistrerons donc ce fichier dans un attribut de l'objet. 12 | 13 | À l'entrée du contexte, on gardera une trace de la sortie courante (`sys.stdout`) avant de la remplacer par notre cible. 14 | Et en sortie, il suffira de faire à nouveau pointer `sys.stdout` vers la précédente sortie standard, préalablement enregistrée. 15 | 16 | Nous pouvons faire hériter notre classe de `ContextDecorator` afin de pouvoir l'utiliser comme décorateur. 17 | 18 | ```python 19 | import sys 20 | from contextlib import ContextDecorator 21 | 22 | class redirect_stdout(ContextDecorator): 23 | def __init__(self, file): 24 | self.file = file 25 | 26 | def __enter__(self): 27 | self.old_output = sys.stdout 28 | sys.stdout = self.file 29 | 30 | def __exit__(self, exc_type, exc_value, traceback): 31 | sys.stdout = self.old_output 32 | ``` 33 | 34 | Pour tester notre gestionnaire de contexte, nous allons nous appuyer sur les `StringIO` du module `io`. 35 | Il s'agit d'objets se comportant comme des fichiers, mais dont tout le contenu est stocké en mémoire, et accessible à l'aide d'une méthode `getvalue`. 36 | 37 | ```python 38 | >>> from io import StringIO 39 | >>> output = StringIO() 40 | >>> with redirect_stdout(output): 41 | ... print('ceci est écrit dans output') 42 | ... 43 | >>> print('ceci est écrit sur la console') 44 | ceci est écrit sur la console 45 | >>> output.getvalue() 46 | 'ceci est écrit dans output\n' 47 | ``` 48 | 49 | ```python 50 | >>> output = StringIO() 51 | >>> @redirect_stdout(output) 52 | ... def addition(a, b): 53 | ... print('result =', a + b) 54 | ... 55 | >>> addition(3, 5) 56 | >>> output.getvalue() 57 | 'result = 8\n' 58 | ``` 59 | 60 | Notre gestionnaire de contexte se comporte comme nous le souhaitions, mais possède cependant une lacune : il n'est pas réentrant. 61 | 62 | ```python 63 | >>> output = StringIO() 64 | >>> redir = redirect_stdout(output) 65 | >>> with redir: 66 | ... with redir: 67 | ... print('ceci est écrit dans output') 68 | ... 69 | >>> print('ceci est écrit sur la console') 70 | ``` 71 | 72 | Comme on le voit, ou plutôt comme on ne le voit pas, le dernier affichage n'est pas imprimé sur la console, mais toujours dans `output`. 73 | En effet, lors de la deuxième entrée dans `redir`, `sys.stdout` ne pointait plus vers la console mais déjà vers notre `StringIO`, et la trace sauvegardée (`self.old_output`) est alors perdue puisqu'assignée à `sys.stdout`. 74 | 75 | Pour avoir un gestionnaire de contexte réentrant, il nous faudrait gérer une pile de fichiers de sortie. 76 | Ainsi, en entrée, la sortie actuelle serait ajoutée à la pile avant d'être remplacée par le fichier cible. 77 | Et en sortie, il suffirait de retirer le dernier élément de la pile et de l'assigner à `sys.stdout`. 78 | 79 | ```python 80 | import sys 81 | 82 | class redirect_stdout(ContextDecorator): 83 | def __init__(self, file): 84 | self.file = file 85 | self.stack = [] 86 | 87 | def __enter__(self): 88 | self.stack.append(sys.stdout) 89 | sys.stdout = self.file 90 | 91 | def __exit__(self, exc_type, exc_value, traceback): 92 | sys.stdout = self.stack.pop() 93 | ``` 94 | 95 | Vous pouvez constater en reprenant les tests précédent que cette version est parfaitement fonctionnelle (pensez juste à réinitialiser votre interpréteur suite aux tests qui ont définitivement redirigé `sys.stdout` vers une `StringIO`). 96 | -------------------------------------------------------------------------------- /src/3-further/2-context-managers/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Ne changeons pas les bonnes habitudes, ces quelques pages de documentation vous régaleront autant que les précédentes. 4 | 5 | * Définition du terme gestionnaire de contexte : 6 | * Gestionnaires de contexte : 7 | * Blocs `with` : 8 | * Module `contextlib` : 9 | * PEP liée au bloc `with` : 10 | -------------------------------------------------------------------------------- /src/3-further/3-accessors-descriptors/0-accessors-descriptors.md: -------------------------------------------------------------------------------- 1 | ## Accesseurs et descripteurs 2 | 3 | L'expression `foo.bar` est en apparence très simple : on accède à l'attribut `bar` d'un objet `foo`. 4 | Cependant, divers mécanismes entrent en jeu pour nous retourner cette valeur, nous permettant d'accéder à des attributs définis à la volée. 5 | 6 | Nous allons découvrir dans ce chapitre quels sont ces mécanismes, et comment les manipuler. 7 | 8 | Sachez premièrement que `foo.bar` revient à exécuter 9 | 10 | * `getattr(foo, 'bar')` 11 | 12 | Il s'agit là de la lecture, deux fonctions sont équivalentes pour la modification et la suppression : 13 | 14 | * `setattr(foo, 'bar', value)` pour `foo.bar = value` 15 | * `delattr(foo, 'bar')` pour `del foo.bar` 16 | -------------------------------------------------------------------------------- /src/3-further/3-accessors-descriptors/2-descriptors.md: -------------------------------------------------------------------------------- 1 | ### Les descripteurs 2 | 3 | Les descripteurs sont une manière de placer des comportements plus évolués derrière des attributs. 4 | En effet, plutôt que toujours recourir à `__getattr__` et consorts, ils sont un autre moyen d'avoir des attributs dynamiques. 5 | Les propriétés (*properties*) sont des exemples de descripteurs. 6 | 7 | Un descripteur se définit comme attribut d'une classe, et devient accessible en tant qu'attribut de ses instances. 8 | Pour cela, le descripteur peut implémenter des méthodes spéciales `__get__`/`__set__`/`__delete__` qui seront respectivement appelées lors de la lecture/écriture/suppression de l'attribut sur une instance de la classe. 9 | 10 | Par exemple, si une classe `Foo` définit un descripteur de type `Descriptor` sous son attribut `attr`, alors, avec `foo` instance de `Foo` : 11 | 12 | - `foo.attr` fera appel à `Descriptor.__get__` ; 13 | - `foo.attr = value` à `Descriptor.__set__` ; 14 | - et `del foo.attr` à `Descriptor.__delete__`. 15 | 16 | La méthode `__get__` du descripteur prend deux paramètres : `instance` et `owner`. 17 | `instance` correspond à l'objet depuis lequel on accède à l'attribut. 18 | Dans le cas où l'attribut est récupéré depuis depuis la classe (`Foo.attr` plutôt que `foo.attr`), `instance` vaudra `None`. 19 | C'est alors que `owner` intervient, ce paramètre contient toujours la classe définissant le descripteur (`Foo`). 20 | 21 | `__set__` prend simplement l'instance et la nouvelle valeur, et `__delete__` se contente de l'instance. 22 | Contrairement à `__get__`, ces deux dernières méthodes ne peuvent s'utiliser que sur les instances, et non sur la classe[^class_descriptor], d'où l'absence du paramètre `owner`. 23 | 24 | [^class_descriptor]: En effet, redéfinir `A.attr` ou le supprimer ne doit déclencher aucune méthode spéciale du descripteur, ça revient juste à redéfinir/supprimer l'attribut de classe. 25 | 26 | Pour reprendre notre exemple précédent sur les températures, nous pourrions avoir deux descripteurs `Celsius` et `Fahrenheit`, qui modifieraient à leur manière la valeur de notre objet `Temperature`. 27 | 28 | ```python 29 | class Celsius: 30 | def __get__(self, instance, owner): 31 | # Dans le cas où on appellerait `Temperature.celsius` 32 | # On préfère retourner le descripteur lui-même 33 | if instance is None: 34 | return self 35 | return instance.value 36 | def __set__(self, instance, value): 37 | instance.value = value 38 | 39 | class Fahrenheit: 40 | def __get__(self, instance, owner): 41 | if instance is None: 42 | return self 43 | return instance.value * 1.8 + 32 44 | def __set__(self, instance, value): 45 | instance.value = (value - 32) / 1.8 46 | 47 | class Temperature: 48 | # On instancie les deux attributs de la classe 49 | celsius = Celsius() 50 | fahrenheit = Fahrenheit() 51 | 52 | def __init__(self): 53 | self.value = 0 54 | ``` 55 | 56 | Je vous laisse exécuter à nouveau les exemples précédents pour constater que le comportement est le même. 57 | 58 | #### La méthode `__set_name__` 59 | 60 | Depuis Python 3.6[^python36], les descripteurs peuvent aussi être pourvus d'une méthode `__set_name__`. 61 | Cette méthode est appelée pour chaque assignation d'un descripteur à un attribut dans le corps de la classe. 62 | La méthode reçoit en paramètres la classe et le nom de l'attribut auquel le descripteur est assigné. 63 | 64 | [^python36]: 65 | 66 | ```python 67 | >>> class Descriptor: 68 | ... def __set_name__(self, owner, name): 69 | ... print(name) 70 | ... 71 | >>> class A: 72 | ... value = Descriptor() 73 | ... 74 | value 75 | ``` 76 | 77 | Le descripteur peut ainsi agir dynamiquement sur la classe en fonction du nom de son attribut. 78 | 79 | Nous pouvons imaginer un descripteur `PositiveValue`, qui assurera qu'un attribut sera toujours positif. 80 | Le descripteur stockera ici sa valeur dans un attribut de l'instance, en utilisant pour cela son nom préfixé d'un *underscore*. 81 | 82 | ```python 83 | class PositiveValue: 84 | def __get__(self, instance, owner): 85 | return getattr(instance, self.attr) 86 | 87 | def __set__(self, instance, value): 88 | setattr(instance, self.attr, max(0, value)) 89 | 90 | def __set_name__(self, owner, name): 91 | self.attr = '_' + name 92 | 93 | class A: 94 | x = PositiveValue() 95 | y = PositiveValue() 96 | ``` 97 | 98 | ```python 99 | >>> a = A() 100 | >>> a.x = 15 101 | >>> a.x 102 | 15 103 | >>> a._x 104 | 15 105 | >>> a.x -= 20 106 | >>> a.x 107 | 0 108 | >>> a.y = -1 109 | >>> a.y 110 | 0 111 | ``` 112 | -------------------------------------------------------------------------------- /src/3-further/3-accessors-descriptors/3-properties.md: -------------------------------------------------------------------------------- 1 | ### Les propriétés 2 | 3 | Les propriétés (ou *properties*) sont un moyen de simplifier l'écriture de descripteurs et de leurs 3 méthodes spéciales. 4 | 5 | En effet, `property` est une classe qui, à la création d'un objet, prend en paramètre les fonctions `fget`, `fset` et `fdel` qui seront respectivement appelées par `__get__`, `__set__` et `__delete__`. 6 | 7 | On pourrait ainsi définir une version simplifiée de `property` comme ceci : 8 | 9 | ```python 10 | class my_property: 11 | def __init__(self, fget, fset, fdel): 12 | self.fget = fget 13 | self.fset = fset 14 | self.fdel = fdel 15 | def __get__(self, instance, owner): 16 | return self.fget(instance) 17 | def __set__(self, instance, value): 18 | return self.fset(instance, value) 19 | def __delete__(self, instance): 20 | return self.fdel(instance) 21 | ``` 22 | 23 | Pour faire de `my_property` un clone parfait de `property`, il nous faudrait gérer le cas où `instance` vaut `None` dans la méthode `__get__` ; 24 | et permettre à `my_property` d'être utilisé en tant que décorateur autour du *getter*. 25 | Nous verrons dans la section exercices comment compléter notre classe à cet effet. 26 | 27 | Les propriétés disposent aussi de décorateurs `getter`, `setter` et `deleter` pour redéfinir les fonctions `fget`/`fset`/`fdel`. 28 | 29 | À l'utilisation, les propriétés nous offrent donc un moyen simple et élégant de réécrire notre classe `Temperature`. 30 | 31 | ```python 32 | class Temperature: 33 | def __init__(self): 34 | self.value = 0 35 | 36 | @property 37 | def celsius(self): # le nom de la méthode devient le nom de la propriété 38 | return self.value 39 | @celsius.setter 40 | def celsius(self, value): # le setter doit porter le même nom 41 | self.value = value 42 | 43 | @property 44 | def fahrenheit(self): 45 | return self.value * 1.8 + 32 46 | @fahrenheit.setter 47 | def fahrenheit(self, value): 48 | self.value = (value - 32) / 1.8 49 | ``` 50 | 51 | Pour plus d'informations sur l'utilisation des propriétés, je vous renvoie [ici](https://zestedesavoir.com/tutoriels/1253/la-programmation-orientee-objet-en-python/5-advanced-oo/#5-5-properties). 52 | -------------------------------------------------------------------------------- /src/3-further/3-accessors-descriptors/4-methods.md: -------------------------------------------------------------------------------- 1 | ### Les méthodes 2 | 3 | Les méthodes en Python vous réservent aussi bien des surprises. Si vous avez déjà rencontré les termes de méthodes de classe (*class methods*), méthodes statiques (*static methods*), ou méthodes préparées (*bound methods*), vous avez pu vous demander comment cela fonctionnait. 4 | 5 | En fait, les méthodes sont des descripteurs vers les fonctions que vous définissez à l'intérieur de votre classe. Elles sont même ce qu'on appelle des *non-data descriptors*, c'est-à-dire des descripteurs qui ne définissent ni *setter*, ni *deleter*. 6 | 7 | Définissons une simple classe `A` possédant différents types de méthodes. 8 | 9 | ```python 10 | class A: 11 | def method(self): 12 | return self 13 | @staticmethod 14 | def staticmeth(): 15 | pass 16 | @classmethod 17 | def clsmeth(cls): 18 | return cls 19 | ``` 20 | 21 | Puis observons à quoi correspondent les différents accès à ces méthodes. 22 | 23 | ```python 24 | >>> a = A() # on crée une instance `a` de `A` 25 | >>> A.method # méthode depuis la classe 26 | 27 | >>> a.method # méthode depuis l'instance 28 | > 29 | >>> A.staticmeth # méthode statique depuis la classe 30 | 31 | >>> a.staticmeth # depuis l'instance 32 | 33 | >>> A.clsmeth # méthode de classe depuis la classe 34 | > 35 | >>> a.clsmeth # depuis l'instance 36 | > 37 | ``` 38 | 39 | On remarque que certains accès retournent des fonctions, et d'autres des *bound methods*, mais quelle différence ? 40 | En fait, la différence survient lors de l'appel, pour le passage du premier paramètre. 41 | 42 | Ne vous êtes-vous jamais demandé comment l'objet courant arrivait dans `self` lors de l'appel d'une méthode ? C'est justement parce qu'il s'agit d'une *bound method*. 43 | C'est en fait une méthode dont le premier paramètre est déjà préparé, et qu'il n'y aura donc pas besoin de spécifier à l'appel. 44 | C'est le descripteur qui joue ce rôle, il est le seul à savoir si vous utilisez la méthode depuis une instance ou depuis la classe (`instance` valant `None` dans ce second cas), et connaît toujours le premier paramètre à passer (`instance`, `owner`, ou rien). 45 | Il peut ainsi construire un nouvel objet (*bound method*), qui lorsqu'il sera appelé se chargera de relayer l'appel à la vraie méthode en lui ajoutant ce paramètre. 46 | 47 | Le même comportement est utilisé pour les méthodes de classes, où la classe de l'objet doit être passée en premier paramètre (`cls`). 48 | Le cas des méthodes statiques est en fait le plus simple, il ne s'agit que de fonctions qui ne prennent pas de paramètres spéciaux, donc qui ne nécessitent pas d'être décorées par le descripteur. 49 | 50 | On remarque aussi que, `A.method` retournant une fonction et non une méthode préparée, il nous faudra indiquer une instance lors de l'appel. 51 | 52 | Pour rappel, voici comment s'utilisent ces différentes méthodes : 53 | 54 | ```python 55 | >>> A.method(a) 56 | <__main__.A object at 0x7fd412a3ad68> 57 | >>> a.method() 58 | <__main__.A object at 0x7fd412a3ad68> 59 | >>> A.staticmeth() 60 | >>> a.staticmeth() 61 | >>> A.clsmeth() 62 | 63 | >>> a.clsmeth() 64 | 65 | ``` 66 | -------------------------------------------------------------------------------- /src/3-further/3-accessors-descriptors/5-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : Méthodes 2 | 3 | Pour clore ce chapitre, je vous propose d'implémenter les descripteurs `staticmethod` et `classmethod`. J'ajouterai à cela un descripteur `method` qui reproduirait le comportement par défaut des méthodes en Python. 4 | 5 | Pour résumer : 6 | 7 | * Ces trois descripteurs sont de type *non-data* (n'implémentent que `__get__`) ; 8 | * `my_staticmethod` 9 | * Retourne la fonction cible, qu'elle soit utilisée depuis la classe ou depuis l'instance ; 10 | * `my_classmethod` 11 | * Retourne une méthode préparée avec la classe en premier paramètre ; 12 | * Même comportement que l'on utilise la méthode de classe depuis la classe ou l'instance ; 13 | * `my_method` 14 | * Si utilisée depuis la classe, retourne la fonction ; 15 | * Sinon, retourne une méthode préparée avec l'instance en premier paramètre. 16 | 17 | Notez que vous pouvez vous aider du type `MethodType` (`from types import MethodType`) pour créer vos *bound methods*. 18 | Il s'utilise très facilement, prenant en paramètres la fonction cible et le premier paramètre de cette fonction. 19 | 20 | ```python 21 | class my_staticmethod: 22 | def __init__(self, func): 23 | self.func = func 24 | def __get__(self, instance, owner): 25 | return self.func 26 | ``` 27 | 28 | ```python 29 | from types import MethodType 30 | 31 | class my_classmethod: 32 | def __init__(self, func): 33 | self.func = func 34 | def __get__(self, instance, owner): 35 | return MethodType(self.func, owner) 36 | ``` 37 | 38 | ```python 39 | from types import MethodType 40 | 41 | class my_method: 42 | def __init__(self, func): 43 | self.func = func 44 | def __get__(self, instance, owner): 45 | if instance is None: 46 | return self.func 47 | return MethodType(self.func, instance) 48 | ``` 49 | -------------------------------------------------------------------------------- /src/3-further/3-accessors-descriptors/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | La documentation est cette fois bien plus fournie, je vous souhaite donc une bonne lecture. 4 | Les liens de ce chapitre sont particulièrement intéressants, notamment conernant le protocole des descripteurs et le *MRO*. 5 | 6 | * Définition du terme descripteur : 7 | * Personnaliser l'accès aux attributs : 8 | * Mise en œuvre des descripteurs : 9 | * Protocole des descripteurs : 10 | * *Fonction* `property` : 11 | * Description du *MRO* : 12 | -------------------------------------------------------------------------------- /src/4-classes/0-classes.md: -------------------------------------------------------------------------------- 1 | # La rentrée des classes 2 | 3 | ##### Et de leurs parents 4 | 5 | Les classes renferment encore bien des secrets, que nous allons tenter de percer dans les présents chapitres. 6 | -------------------------------------------------------------------------------- /src/4-classes/1-types/0-types.md: -------------------------------------------------------------------------------- 1 | ## Types 2 | 3 | Il vous est peut-être arrivé de lire qu'en Python tout était objet. Il faut cependant nuancer quelque peu : tout ne l'est pas, une instruction n'est pas un objet par exemple. 4 | Mais toutes les valeurs que l'on peut manipuler sont des objets. 5 | 6 | À quoi peut-on alors reconnaître un objet ? Cela correspond à tout ce qui peut être assigné à une variable. 7 | Ainsi, les nombres, les chaînes de caractère, les fonctions ou même les classes sont des objets. 8 | Et ce sont ici ces dernières qui nous intéressent. 9 | -------------------------------------------------------------------------------- /src/4-classes/1-types/1-instance-class-metaclass.md: -------------------------------------------------------------------------------- 1 | ### Instance, classe et métaclasse 2 | 3 | On sait que tout objet est instance d'une classe. 4 | On dit aussi que la classe est le type de l'objet. 5 | Et donc, tout objet a un type. 6 | Le type d'un objet peut être récupéré grâce à la *fonction* `type`. 7 | 8 | ```python 9 | >>> type(5) 10 | 11 | >>> type('foo') 12 | 13 | >>> type(lambda: None) 14 | 15 | ``` 16 | 17 | Mais si les classes sont des objets, quel est alors leur type ? 18 | 19 | ```python 20 | >>> type(object) 21 | 22 | ``` 23 | 24 | Le type d'`object` est `type`. 25 | En effet, `type` est un peu plus complexe que ce que l'on pourrait penser, nous y reviendrons dans le prochain chapitre. 26 | 27 | On notera simplement qu'une classe est alors une instance de la classe `type`. 28 | Et qu'une classe telle que `type`, qui permet d'instancier d'autres classes, est appelée une métaclasse. 29 | 30 | Instancier une classe pour en créer une nouvelle n'est pas forcément évident. 31 | Nous avons plutôt l'habitude d'hériter d'une classe existante. 32 | Mais dans les cas où nous créons une classe par héritage, c'est aussi une instanciation de `type` qui est réalisée en interne. 33 | 34 | #### Caractéristiques des classes 35 | 36 | Les classes (ou *type objects*) sont un ensemble d'objets qui possèdent quelques caractéristiques communes : 37 | 38 | - Elles héritent d'`object` (mise à part `object` elle-même) ; 39 | - Elles sont des instances plus ou moins directes de `type` (de `type` ou de classes héritant de `type`) ; 40 | - On peut en hériter ; 41 | - Elles peuvent être instanciées (elles sont des *callables* qui retournent des objets de ce type). 42 | 43 | ```python 44 | >>> int.__bases__ # int hérite d'object 45 | (,) 46 | >>> type(int) # int est une instance de type 47 | 48 | >>> class A(int): pass # on peut hériter de la classe int 49 | >>> int() # on peut instancier int 50 | 0 51 | >>> type(int()) # ce qui retourne un objet du type int 52 | 53 | ``` 54 | 55 | Et on observe que notre classe `A` est elle aussi instance de `type`. 56 | 57 | ```python 58 | >>> type(A) 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- /src/4-classes/1-types/2-new.md: -------------------------------------------------------------------------------- 1 | ### Le vrai constructeur 2 | 3 | En Python, la méthode spéciale `__init__` est souvent appelée constructeur de l'objet. 4 | Il s'agit en fait d'un abus de langage : `__init__` ne construit pas l'objet, elle intervient après la création de ce dernier pour l'initialiser. 5 | 6 | Le vrai constructeur d'une classe est `__new__`. 7 | Cette méthode prend la classe en premier paramètre (le paramètre `self` n'existe pas encore puisque l'objet n'est pas créé), et doit retourner l'objet nouvellement créé (contrairement à `__init__`). 8 | Les autres paramètres sont identiques à ceux reçus par `__init__`. 9 | 10 | C'est aussi `__new__` qui est chargée d'appeler l'initialiseur `__init__` (ce que fait `object.__new__` par défaut, en lui passant aussi la liste d'arguments). 11 | 12 | ```python 13 | >>> class A: 14 | ... def __new__(cls): 15 | ... print('création') 16 | ... return super().__new__(cls) 17 | ... def __init__(self): 18 | ... print('initialisation') 19 | ... 20 | >>> A() 21 | création 22 | initialisation 23 | <__main__.A object at 0x7ffb15ef9048> 24 | ``` 25 | 26 | Nous choisissons ici de faire appel à `object.__new__` dans notre constructeur (via `super`), mais nous n'y sommes pas obligés. 27 | Rien ne nous oblige non plus — mise à part la logique — à retourner un objet du bon type. 28 | 29 | #### Le cas des immutables 30 | 31 | La méthode `__new__` est particulièrement utile pour les objets immutables. 32 | En effet, il est impossible d'agir sur les objets dans la méthode `__init__`, puisque celle-ci intervient après la création, et que l'objet n'est pas modifiable. 33 | 34 | Si l'on souhaite hériter d'un type immutable (`int`, `str`, `tuple`), et agir sur l'initialisation de l'objet, il est donc nécessaire de redéfinir `__new__`. 35 | Par exemple une classe `Point2D` immutable, qui hériterait de `tuple`. 36 | 37 | ```python 38 | class Point2D(tuple): 39 | def __new__(cls, x, y): 40 | return super().__new__(cls, (x, y)) 41 | 42 | @property 43 | def x(self): 44 | return self[0] 45 | 46 | @property 47 | def y(self): 48 | return self[1] 49 | ``` 50 | 51 | ```python 52 | >> p = Point2D(3, 5) 53 | >>> p.x 54 | 3 55 | >>> p.y 56 | 5 57 | >>> p.x = 10 58 | Traceback (most recent call last): 59 | File "", line 1, in 60 | AttributeError: can´t set attribute 61 | >>> p[0] = 10 62 | Traceback (most recent call last): 63 | File "", line 1, in 64 | TypeError: 'Point2D' object does not support item assignment 65 | ``` 66 | -------------------------------------------------------------------------------- /src/4-classes/1-types/3-inheritance-parameters.md: -------------------------------------------------------------------------------- 1 | ### Paramètres d'héritage 2 | 3 | Quand nous créons une classe, nous savons que nous pouvons spécifier entre parenthèses les classes à hériter. 4 | 5 | ```python 6 | class A(B, C): # A hérite de B et C 7 | pass 8 | ``` 9 | 10 | Les classes parentes sont ici comme les arguments positionnels d'un appel de fonction. 11 | Vous vous en doutez peut-être maintenant, mais il est aussi possible de préciser des arguments nommés. 12 | 13 | ```python 14 | class A(B, C, foo='bar', x=3): 15 | pass 16 | ``` 17 | 18 | Cette fonctionnalité existait déjà en Python 3.5, mais était assez étrange et se gérait au niveau de la métaclasse. 19 | La comportement est simplifié avec [Python 3.6 qui ajoute une méthode spéciale pour gérer ces arguments](https://zestedesavoir.com/articles/1540/sortie-de-python-3-6/#principales-nouveautes). 20 | 21 | Il est donc maintenant possible d'implémenter la méthode de classe `__init_subclass__`, qui recevra tous les arguments nommés. 22 | La méthode ne sera pas appelée pour la classe courante, mais le sera pour toutes ses classes filles. 23 | 24 | Pour reprendre notre class `Deque`, nous pourrions imaginer une classe `TypedDeque` qui gérerait des listes d'éléments d'un type prédéfini. 25 | Nous lèverions alors une exception pour toute insertion de valeur d'un type inadéquat. 26 | 27 | ```python 28 | class TypedDeque(Deque): 29 | elem_type = None 30 | 31 | def __init_subclass__(cls, type, **kwargs): 32 | super().__init_subclass__(**kwargs) 33 | cls.elem_type = type 34 | 35 | @classmethod 36 | def check_type(cls, value): 37 | if cls.elem_type is not None and not isinstance(value, cls.elem_type): 38 | raise TypeError('Cannot insert element of type ' 39 | f'{type(value).__name__} in {cls.__name__}') 40 | 41 | def append(self, value): 42 | self.check_type(value) 43 | super().append(value) 44 | 45 | def insert(self, i, value): 46 | self.check_type(value) 47 | super().insert(i, value) 48 | 49 | def __setitem__(self, key, value): 50 | self.check_type(value) 51 | super().__setitem__(key, value) 52 | 53 | class IntDeque(TypedDeque, type=int): 54 | pass 55 | 56 | class StrDeque(TypedDeque, type=str): 57 | pass 58 | ``` 59 | 60 | ```python 61 | >>> d = IntDeque([0, 1, 2]) 62 | >>> d.append(3) 63 | >>> list(d) 64 | [0, 1, 2, 3] 65 | >>> d.append('foo') 66 | Traceback (most recent call last): 67 | File "", line 1, in 68 | File "init_subclass.py", line 91, in append 69 | self.check_type(value) 70 | File "init_subclass.py", line 87, in check_type 71 | raise TypeError('Cannot insert element of type ' 72 | TypeError: Cannot insert element of type str in IntDeque 73 | >>> 74 | >>> d = StrDeque() 75 | >>> d.append('foo') 76 | >>> list(d) 77 | ['foo'] 78 | >>> d.insert(0, 5) 79 | Traceback (most recent call last): 80 | File "", line 1, in 81 | File "init_subclass.py", line 95, in insert 82 | self.check_type(value) 83 | File "init_subclass.py", line 87, in check_type 84 | raise TypeError('Cannot insert element of type ' 85 | TypeError: Cannot insert element of type int in StrDeque 86 | ``` 87 | 88 | Le paramètre `cls` de la méthode `__init_subclass__` correspond bien ici à la classe fille. 89 | Il convient d'utiliser `super` pour faire appel aux `__init_subclass__` des autres parents, en leur donnant le reste des arguments nommés. 90 | On note aussi qu'`__init_subclass__` étant oligatoirement une méthode de classe, l'utilisation du décorateur `@classmethod` est facultative. 91 | -------------------------------------------------------------------------------- /src/4-classes/1-types/4-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : Liste immutable 2 | 3 | Nous en étions resté sur notre liste chaînée à l'opérateur d'égalité, qui rendait la liste non-hashable. 4 | Je vous disais alors que l'on s'intéresserait à un type de liste immutable (et donc hashable) : chose promise, chose due. 5 | 6 | Nous savons que pour créer un type immutable en Python, il faut hériter d'un autre type immutable. 7 | Par commodité, nous choisissons `tuple` puisqu'il nous permet en tant que conteneur de stocker des données. 8 | 9 | Pour rappel, notre liste se compose de deux classes : `Node` et `Deque`. 10 | Nos nouvelles classes se nommeront `ImmutableNode` et `ImmutableDeque`. 11 | 12 | `ImmutableNode` est un ensemble de deux éléments : le contenu du maillon dans `value`, et le maillon suivant (ou `None`) dans `next`. 13 | On peut aisément représenter cette classe par un *tuple* de deux éléments. 14 | On ajoutera juste deux propriétés, `value` et `next`, pour faciliter l'accès à ces valeurs. 15 | 16 | ```python 17 | class ImmutableNode(tuple): 18 | def __new__(cls, value, next=None): 19 | return super().__new__(cls, (value, next)) 20 | 21 | @property 22 | def value(self): 23 | return self[0] 24 | 25 | @property 26 | def next(self): 27 | return self[1] 28 | ``` 29 | 30 | Passons maintenant à `ImmutableDeque`. 31 | Au final, il s'agit aussi d'un ensemble de deux éléments : `first` et `last`, les deux extrêmités de la liste. 32 | 33 | Mais `ImmutableDeque` présente un autre défi, c'est cette classe qui est chargée de créer les maillons, qui sont ici immutables. 34 | Cela signifie que le `next` de chaque maillon doit être connu lors de sa création. 35 | 36 | Pour rappel, la classe sera instanciée avec un itérable en paramètre, celui-ci servant à créer les maillons. 37 | Il nous faudra donc itérer sur cet ensemble en connaissant l'élément suivant. 38 | 39 | Je vous propose pour cela une méthode de classe récursive, `create_node`. 40 | Cette méthode recevra un itérateur en paramètre, récupérera la valeur actuelle avec la fonction `next`, puis appelera la méthode sur le reste de l'itérateur. 41 | `create_node` retournera un objet `ImmutableNode`, qui sera donc utilisé comme maillon `next` dans l'appel parent. 42 | En cas de `StopIteration` (fin de l'itérateur), `create_node` renverra simplement `None`. 43 | 44 | ```python 45 | class ImmutableDeque(tuple): 46 | def __new__(cls, iterable=()): 47 | first = cls.create_node(iter(iterable)) 48 | last = first 49 | while last and last.next: 50 | last = last.next 51 | return super().__new__(cls, (first, last)) 52 | 53 | @classmethod 54 | def create_node(cls, iterator): 55 | try: 56 | value = next(iterator) 57 | except StopIteration: 58 | return None 59 | next_node = cls.create_node(iterator) 60 | return ImmutableNode(value, next_node) 61 | ``` 62 | 63 | À la manière de notre classe `ImmutableNode`, nous ajoutons des propriétés `first` et `last`. 64 | Celles-ci diffèrent un peu tout de même : puisque `__getitem__` sera surchargé dans la classe, nous devons faire appel au `__getitem__` parent, *via* `super`. 65 | 66 | ```python 67 | @property 68 | def first(self): 69 | return super().__getitem__(0) 70 | 71 | @property 72 | def last(self): 73 | return super().__getitem__(1) 74 | ``` 75 | 76 | Les autres méthodes (`__contains__`, `__len__`, `__getitem__`, `__iter__` et `__eq__`) seront identiques à celles de la classe `Deque`. 77 | On prendra seulement soin, dans `__getitem__`, de remplacer les occurrence de `Deque` par `ImmutableDeque` en cas de *slicing*, ou de faire appel au type de `self` pour construire la nouvelle liste. 78 | 79 | Les méthodes de modification (`append`, `insert`, `__setitem__`) ne sont bien sûr pas à implémenter. 80 | On remarque d'ailleurs que l'attribut `last` de nos listes n'a pas vraiment d'intérêt ici, puisqu'il n'est pas utilisé pour faciliter l'ajout d'élements en fin de liste. 81 | 82 | Enfin, on peut maintenant ajouter une méthode `__hash__`, pour rendre nos objets *hashables*. 83 | Pour cela, nous ferons appel à la méthode `__hash__` du parent, qui retournera le condensat du *tuple*. 84 | 85 | ```python 86 | def __hash__(self): 87 | return super().__hash__() 88 | ``` 89 | 90 | Nous pouvons maintenant passer au test de notre nouvelle classe. 91 | 92 | ```python 93 | >>> d = ImmutableDeque(range(10)) 94 | >>> d 95 | ((0, (1, (2, (3, (4, (5, (6, (7, (8, (9, None)))))))))), (9, None)) 96 | >>> list(d) 97 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 98 | >>> d[1:-1:2] 99 | ((1, (3, (5, (7, None)))), (7, None)) 100 | >>> list(d[1:-1:2]) 101 | [1, 3, 5, 7] 102 | >>> 5 in d 103 | True 104 | >>> 11 in d 105 | False 106 | >>> len(d) 107 | 10 108 | >>> d[0], d[1], d[5], d[9] 109 | (0, 1, 5, 9) 110 | >>> d == ImmutableDeque(range(10)) 111 | True 112 | >>> d == ImmutableDeque(range(9)) 113 | False 114 | >>> hash(d) 115 | -9219024882206086640 116 | >>> {d: 0} 117 | {((0, (1, (2, (3, (4, (5, (6, (7, (8, (9, None)))))))))), (9, None)): 0} 118 | >>> {d: 0}[d] 119 | 0 120 | ``` 121 | -------------------------------------------------------------------------------- /src/4-classes/1-types/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Revenons maintenant avec la documentation sur les concepts étudiés dans ce chapitre. 4 | 5 | * Définition du terme type : 6 | * Méthode `__new__` : 7 | * Paramètres d'héritage : 8 | 9 | Un autre très bon article, qui revient sur les concepts de classe, d'instance, de métaclasse et d'héritage : 10 | -------------------------------------------------------------------------------- /src/4-classes/2-metaclasses/0-metaclasses.md: -------------------------------------------------------------------------------- 1 | ## Métaclasses 2 | 3 | Nous connaissons maintenant une première métaclasse, `type`. 4 | Une métaclasse est une classe dont les instances sont des classes. 5 | 6 | Ce chapitre a pour but de présenter comment créer nos propres métaclasses, et les mécanismes mis en œuvre par cela. 7 | -------------------------------------------------------------------------------- /src/4-classes/2-metaclasses/1-type.md: -------------------------------------------------------------------------------- 1 | ### Quel est donc ce `type` ? 2 | 3 | Ainsi, vous l'aurez compris, `type` n'est pas utile que pour connaître le type d'un objet. 4 | Dans l'utilisation que vous connaissiez, `type` prend un unique paramètre, et en retourne le type. 5 | 6 | Pour notre autre utilisation, ses paramètres sont au nombre de 3 : 7 | 8 | - `name` -- une chaîne de caractères représentant le nom de la classe à créer ; 9 | - `bases` -- un tuple contenant les classes dont nous héritons (`object` est implicite) ; 10 | - `dict` -- le dictionnaire des attributs et méthodes de la nouvelle classe. 11 | 12 | ```python 13 | >>> type('A', (), {}) 14 | 15 | >>> A = type('A', (), {'x': 4}) 16 | >>> A.x 17 | 4 18 | >>> A().x 19 | 4 20 | >>> type(A) 21 | 22 | >>> type(A()) 23 | 24 | ``` 25 | 26 | Nous avons ici une classe `A`, strictement équivalente à la suivante : 27 | 28 | ```python 29 | class A: 30 | x = 4 31 | ``` 32 | 33 | Voici maintenant un exemple plus complet, avec héritage et méthodes. 34 | 35 | ```python 36 | >>> B = type('B', (int,), {}) 37 | >>> B() 38 | 0 39 | >>> B = type('B', (int,), {'next': lambda self: self + 1}) 40 | >>> B(5).next() 41 | 6 42 | >>> def C_prev(self): 43 | ... return self - 1 44 | ... 45 | >>> C = type('C', (B,), {'prev': C_prev}) 46 | >>> C(5).prev() 47 | 4 48 | >>> C(5).next() 49 | 6 50 | ``` 51 | -------------------------------------------------------------------------------- /src/4-classes/2-metaclasses/3-function-metaclass.md: -------------------------------------------------------------------------------- 1 | ### Utiliser une fonction comme métaclasse 2 | 3 | Par extension, on appelle parfois métaclasse tout *callable* qui renverrait une classe lorsqu'il serait appelé. 4 | 5 | Ainsi, une fonction faisant appel à `type` serait considérée comme métaclasse. 6 | 7 | ```python 8 | >>> def meta(*args): 9 | ... print('enter metaclass') 10 | ... return type(*args) 11 | ... 12 | >>> class A(metaclass=meta): 13 | ... pass 14 | ... 15 | enter metaclass 16 | ``` 17 | 18 | Cependant, on ne peut pas à proprement parler de métaclasse, celle de notre classe `A` étant toujours `type`. 19 | 20 | ```python 21 | >>> type(A) 22 | 23 | ``` 24 | 25 | Ce qui fait qu'à l'héritage, l'appel à la métaclasse serait perdu (cet appel n'étant réalisé qu'une fois). 26 | 27 | ```python 28 | >>> class B(A): 29 | ... pass 30 | ... 31 | >>> type(B) 32 | 33 | ``` 34 | 35 | Pour rappel, le comportement avec une « vraie » métaclasse serait le suivant : 36 | 37 | ```python 38 | >>> class meta(type): 39 | ... def __new__(cls, *args): 40 | ... print('enter metaclass') 41 | ... return super().__new__(cls, *args) 42 | ... 43 | >>> class A(metaclass=meta): 44 | ... pass 45 | ... 46 | enter metaclass 47 | >>> type(A) 48 | 49 | >>> class B(A): 50 | ... pass 51 | ... 52 | enter metaclass 53 | >>> type(B) 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /src/4-classes/2-metaclasses/4-tp.md: -------------------------------------------------------------------------------- 1 | ### TP : Types immutables 2 | 3 | Nous avons vu dans le chapitre précédent comment réaliser un type immutable. 4 | Nous voulons maintenant aller plus loin, en mettant en place une métaclasse qui nous permettra facilement de créer de nouveaux types immutables. 5 | 6 | Déjà, à quoi ressemblerait une classe d'objets immutables ? 7 | Il s'agirait d'une classe dont les noms d'attributs seraient fixés à l'avance pour tous les objets. 8 | Et les attributs en question ne seraient bien sûr pas modifiables sur les objets. 9 | La classe pourrait bien sûr définir des méthodes, mais toutes ces méthodes auraient un accès en lecture seule sur les instances. 10 | 11 | On aurait par exemple quelque chose comme : 12 | 13 | ```python 14 | class Point(metaclass=ImmutableMeta): 15 | __fields__ = ('x', 'y') 16 | 17 | def distance(self): 18 | return (self.x**2 + self.y**2)**0.5 19 | ``` 20 | 21 | ```python 22 | >>> p = Point(x=3, y=4) 23 | >>> p.x 24 | 3 25 | >>> p.y 26 | 4 27 | >>> p.distance() 28 | 5.0 29 | >>> p.x = 0 30 | Traceback (most recent call last): 31 | File "", line 1, in 32 | AttributeError: can´t set attribute 33 | >>> p.z = 0 34 | Traceback (most recent call last): 35 | File "", line 1, in 36 | AttributeError: 'Point' object has no attribute 'z' 37 | ``` 38 | 39 | #### Hériter de `tuple` 40 | 41 | Plusieurs solutions s'offrent à nous pour mener ce travail. 42 | Nous pouvons, comme précédemment, faire hériter tous nos immutables de `tuple`. 43 | Il faudra alors faire pointer chacun des noms d'attributs sur les éléments du *tuple*, *via* des propriétés par exemple. 44 | On peut simplifier cela avec `namedtuple`, qui réalise cette partie du travail. 45 | 46 | Notre métaclasse se chargerait ainsi d'extraire les champs du type immutable, de créer un `namedtuple` correspondant, puis en faire hériter notre classe immutable. 47 | 48 | ```python 49 | class ImmutableMeta(type): 50 | def __new__(cls, name, bases, dict): 51 | fields = dict.pop('__fields__', ()) 52 | bases += (namedtuple(name, fields),) 53 | return super().__new__(cls, name, bases, dict) 54 | ``` 55 | 56 | Si l'on implémente une classe `Point` comme dans l'exemple plus haut, on remarque que la classe se comporte comme convenu jusqu'au `p.z = 0`. 57 | En effet, il nous est ici possible d'ajouter de nouveaux attributs à nos objets, pourtant voulus immutables. 58 | 59 | ```python 60 | >>> p = Point(x=3, y=4) 61 | >>> p.z = 5 62 | >>> p.z 63 | 5 64 | ``` 65 | 66 | #### Les slots à la rescousse 67 | 68 | Comme nous l'avons vu avec les accesseurs, il est possible de définir un ensemble `__slots__` des attributs possibles des instances de cette classe. 69 | Celui-ci a entre autres pour effet d'empêcher de définir d'autres attributs à nos objets. 70 | 71 | C'est donc dans ce sens que nous allons maintenant l'utiliser. 72 | Nos types immutables n'ont besoin d'aucun attribut : tout ce qu'ils stockent est contenu dans un *tuple*, et les accesseurs sont des propriétés. 73 | Ainsi, notre métaclasse `ImmutableMeta` peut simplement définir un attribut `__slots__ = ()` à nos classes. 74 | 75 | ```python 76 | class ImmutableMeta(type): 77 | def __new__(cls, name, bases, dict): 78 | fields = dict.pop('__fields__', ()) 79 | bases += (namedtuple(name, fields),) 80 | dict['__slots__'] = () 81 | return super().__new__(cls, name, bases, dict) 82 | ``` 83 | 84 | #### Le problème des méthodes de `tuple` 85 | 86 | Nous avons maintenant entre les mains une classe de types immutables répondant aux critères décrits plus haut. 87 | Mais si on y regarde de plus près, on remarque un léger problème : 88 | nos classes possèdent des méthodes incongrues héritées de `tuple` et `namedtuple`. 89 | On voit par exemple des méthodes `__getitem__`, `count` ou `index` qui ne nous sont d'aucune utilité et polluent les classes. 90 | `__getitem__` est d'autant plus problématique qu'il s'agit d'un opérateur du langage, qui se retrouve automatiquement surchargé. 91 | 92 | ```python 93 | >>> dir(Point) 94 | ['__add__', '__class__', '__contains__', '__delattr__', '__dict__', '__dir__', 95 | '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', 96 | '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__init__', '__iter__', 97 | '__le__', '__len__', '__lt__', '__module__', '__mul__', '__ne__', '__new__', 98 | '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', 99 | '__sizeof__', '__slots__', '__str__', '__subclasshook__', '_asdict', '_fields', 100 | '_make', '_replace', '_source', 'count', 'index', 'x', 'y'] 101 | ``` 102 | 103 | Alors on peut dans un premier temps choisir d'hériter de `tuple` plutôt que d'un `namedtuple` pour faire un premier tri, mais ça ne règle pas le soucis. 104 | Et il nous est impossible de supprimer ces méthodes, puisqu'elles ne sont pas définies dans nos classes mais dans une classe parente. 105 | 106 | Il faut alors bidouiller, en remplaçant les méthodes par des attributs levant des `AttributeError` pour faire croire à leur absence, en redéfinissant `__dir__` pour les en faire disparaître, etc. 107 | Mais nos objets continueront à être des *tuples* et ces méthodes resteront accessibles d'une manière ou d'une autre (en appelant directement `tuple.__getitem__`, par exemple). 108 | 109 | Nous verrons dans les exercices complémentaires une autre piste pour créer nos propres types immutables. 110 | -------------------------------------------------------------------------------- /src/4-classes/2-metaclasses/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Et pour terminer ce chapitre, un nouveau rappel vers la documenation Python. 4 | Je vous encourage vraiment à la lire le plus possible, elle est très complète et très instructive, bien que parfois un peu bordélique. 5 | 6 | * Définition du terme métaclasse : 7 | * Personnalisation de la création de classes : 8 | * Classe `type` : 9 | * PEP relative aux métaclasses : 10 | 11 | Je tenais aussi à présenter ici [un tutoriel/guide en 8 parties de Sam&Max dédié au modèle objet, à `type` et aux métaclasses](http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-1/). 12 | 13 | Enfin, je ne peux que vous conseiller de vous pencher sur [les sources de CPython](https://github.com/python/cpython) pour comprendre les mécanismes internes. 14 | Notamment [le fichier `Objects/typeobject.c`](https://github.com/python/cpython/blob/master/Objects/typeobject.c) qui définit les classes `type` et `object`. 15 | -------------------------------------------------------------------------------- /src/4-classes/3-abstract-classes/0-abstract-classes.md: -------------------------------------------------------------------------------- 1 | ## Classes abstraites 2 | 3 | Nous avons durant ce cours étudié diverses interfaces (conteneurs, itérables, hashables, etc.). 4 | Nous allons maintenant voir comment les classes abstraites peuvent nous permettre de reconnaître ces interfaces. 5 | 6 | Je ne reviendrai pas ici sur la notion même de classe abstraite, [présentée dans cet autre cours](https://zestedesavoir.com/tutoriels/1253/la-programmation-orientee-objet-en-python/5-advanced-oo/#5-6-abstract-classes), 7 | dont il est conseillé de prendre connaissance avant de passer à la suite. 8 | -------------------------------------------------------------------------------- /src/4-classes/3-abstract-classes/1-abc.md: -------------------------------------------------------------------------------- 1 | ### Module `abc` 2 | 3 | Pour rappel, le module [`abc`](https://docs.python.org/3/library/abc.html) donne accès à la classe `ABC` qui permet par héritage de construire une classe abstraite, et au décorateur `abstractmethod` pour définir des méthodes abstraites. 4 | 5 | Une autre classe importante réside dans ce module, `ABCMeta`. 6 | `ABCMeta` est la métaclasse de la classe `ABC`, et donc le type de toutes les classes abstraites. 7 | C'est `ABCMeta` qui s'occupe de référencer dans l'ensemble `__abstractmethods__`[^abstractmethods] 8 | les méthodes abstraites définies dans la classe. 9 | 10 | Mais outre le fait de pouvoir spécifier les méthodes à implémenter, les classes abstraites de Python ont un autre but : définir une interface. 11 | Vous connaissez probablement `isinstance`, qui permet de vérifier qu'un objet est du bon type ; 12 | peut-être moins `issubclass`, pour vérifier qu'une classe *hérite* d'une autre. 13 | 14 | ```python 15 | >>> isinstance(4, int) # 4 est un int 16 | True 17 | >>> isinstance(4, str) # 4 n'est pas une str 18 | False 19 | >>> issubclass(int, object) # int hérite d'object 20 | True 21 | >>> issubclass(int, str) # int n'hérite pas de str 22 | False 23 | ``` 24 | 25 | Ces deux fonctions sont en fait des opérateurs, qui font appel à des méthodes spéciales, et sont à ce titre surchargeables, comme nous le verrons par la suite. 26 | 27 | J'ai utilisé plus haut le terme « hérite » pour décrire l'opérateur `issubclass`. 28 | C'est en fait légèrement différent, `issubclass` permet de vérifier qu'une classe est une sous-classe (ou sous-type) d'une autre. 29 | 30 | Quand une classe hérite d'une autre, elle en devient un sous-type (sauf cas exceptionnels[^subclasscheck]). 31 | Mais elle peut aussi être sous-classe de classes dont elle n'hérite pas. 32 | 33 | C'est le but de la méthode `register` des classes `ABC`. 34 | Elle sert à enregistrer une classe comme sous-type de la classe abstraite. 35 | 36 | Imaginons une classe abstraite `Sequence` correspondant aux types de séquences connus (`str`, `list`, `tuple`)[^sequence]. 37 | Ces types sont des *builtins* du langage, il ne nous est pas pas possible de les redéfinir pour les faire hériter de `Sequence`. 38 | Mais la méthode `register` de notre classe abstraite `Sequence` va nous permettre de les enregistrer comme sous-classes. 39 | 40 | ```python 41 | >>> import abc 42 | >>> class Sequence(abc.ABC): 43 | ... pass 44 | ... 45 | >>> Sequence.register(str) 46 | 47 | >>> Sequence.register(list) 48 | 49 | >>> Sequence.register(tuple) 50 | 51 | >>> isinstance('foo', Sequence) 52 | True 53 | >>> isinstance(42, Sequence) 54 | False 55 | >>> issubclass(list, Sequence) 56 | True 57 | >>> issubclass(dict, Sequence) 58 | False 59 | ``` 60 | 61 | [^abstractmethods]: L'ensemble `__abstractmethods__` est ensuite analysé pour savoir si une classe peut être instanciée, le constructeur d'`object` levant une erreur dans le cas échéant. 62 | [^subclasscheck]: Voir à ce propos la section « `issubclass` ». 63 | [^sequence]: Pour rappel, une séquence est un objet *indexable* et *sliceable*. 64 | -------------------------------------------------------------------------------- /src/4-classes/3-abstract-classes/2-isinstance.md: -------------------------------------------------------------------------------- 1 | ### `isinstance` 2 | 3 | Nous venons de voir que `isinstance` était un opérateur, et qu'il était surcheargable. 4 | Nous allons ici nous intéresser à la mise en œuvre de cette surcharge. 5 | 6 | Pour rappel, la surchage d'opérateur se fait par la définition d'une méthode spéciale dans le type de l'objet. 7 | Par exemple, il est possible d'utiliser `+` sur le nombre `4` parce que `4` est de type `int`, et qu'`int` implémente la méthode `__add__`. 8 | 9 | `isinstance` est un opérateur qui s'applique à une classe (la classe dont on cherche à savoir si tel objet en est l'instance). 10 | La surcharge se fera donc dans le type de cette classe, c'est-à-dire dans la métaclasse. 11 | 12 | La méthode spéciale correspondant à l'opérateur est `__instancecheck__`, qui reçoit en paramètre l'objet à tester, et retourne un booléen (`True` si l'objet est du type en question, `False` sinon). 13 | 14 | On peut par exemple imaginer une classe `ABCIterable`, qui cherchera à savoir si un objet donné est itérable (possède une méthode `__iter__`). 15 | On teste pour cela si cet object a un attribut `__iter__`, et si cet attribut est *callable*. 16 | 17 | ```python 18 | class ABCIterableMeta(type): 19 | def __instancecheck__(self, obj): 20 | return hasattr(obj, '__iter__') and callable(obj.__iter__) 21 | 22 | class ABCIterable(metaclass=ABCIterableMeta): 23 | pass 24 | ``` 25 | 26 | ```python 27 | >>> isinstance([], ABCIterable) 28 | True 29 | >>> isinstance((0,), ABCIterable) 30 | True 31 | >>> isinstance('foo', ABCIterable) 32 | True 33 | >>> isinstance({'a': 'b'}, ABCIterable) 34 | True 35 | >>> isinstance(18, ABCIterable) 36 | False 37 | >>> isinstance(object(), ABCIterable) 38 | False 39 | ``` 40 | 41 | Quelques dernières précisions sur `isinstance` : l'opérateur est un peu plus complexe que ce qui a été montré. 42 | 43 | Premièrement, `isinstance` peut recevoir en deuxième paramètre un *tuple* de types plutôt qu'un type simple. 44 | Il regardera alors si l'object donné en premier paramètre est une instance de l'un de ces types. 45 | 46 | ```python 47 | >>> isinstance(4, (int, str)) 48 | True 49 | >>> isinstance('foo', (int, str)) 50 | True 51 | >>> isinstance(['bar'], (int, str)) 52 | False 53 | ``` 54 | 55 | Ensuite, la méthode `__instancecheck__` n'est pas toujours appelée. 56 | Lors d'un appel `isinstance(obj, cls)`, la méthode `__instancecheck__` est appelée que si `type(obj)` n'est pas `cls`. 57 | 58 | On peut s'en rendre compte avec une classe dont `__instancecheck__` renverrait `False` pour tout objet testé. 59 | 60 | ```python 61 | >>> class NoInstancesMeta(type): 62 | ... def __instancecheck__(self, obj): 63 | ... return False 64 | ... 65 | >>> class NoInstances(metaclass=NoInstancesMeta): 66 | ... pass 67 | ... 68 | >>> isinstance(NoInstances(), NoInstances) 69 | True 70 | ``` 71 | 72 | En revanche, si nous héritons de notre classe `NoInstances` : 73 | 74 | ```python 75 | >>> class A(NoInstances): 76 | ... pass 77 | ... 78 | >>> isinstance(A(), NoInstances) 79 | False 80 | ``` 81 | 82 | Pour comprendre le fonction d'`isinstance`, on pourrait grossièrement réécrire l'opérateur avec la fonction suivante.[^PyObject_IsInstance] 83 | 84 | ```python 85 | def isinstance(obj, cls): 86 | if type(obj) is cls: 87 | return True 88 | if issubclass(type(cls), tuple): 89 | return any(isinstance(obj, c) for c in cls) 90 | return type(cls).__instancecheck__(cls, obj) 91 | ``` 92 | 93 | [^PyObject_IsInstance]: Voir à ce propos la fonction `PyObject_IsInstance` du fichier [`Objects/abstract.c`](https://github.com/python/cpython/blob/master/Objects/abstract.c) des sources de CPython. 94 | -------------------------------------------------------------------------------- /src/4-classes/3-abstract-classes/3-issubclass.md: -------------------------------------------------------------------------------- 1 | ### `issubclass` 2 | 3 | Dans la même veine qu'`isinstance`, nous avons donc l'opérateur `issubclass`, qui vérifie qu'une classe est sous-classe d'une autre. 4 | 5 | La surcharge se fait là aussi sur la métaclasse, à l'aide de la méthode spéciale `__subclasscheck__`. 6 | Cette méthode est très semblable à `__instancecheck__` : en plus de `self` (la classe courante), elle reçoit en paramètre la classe à tester. 7 | Elle retourne elle aussi un booléen (`True` si la classe donnée est une sous-classe de l'actuelle, `False` sinon). 8 | 9 | Reprenons ici l'exemple précédent des itérables : notre classe `ABCIterable` permet de tester si une classe est un type d'objets itérables. 10 | 11 | ```python 12 | class ABCIterableMeta(type): 13 | def __subclasscheck__(self, cls): 14 | return hasattr(cls, '__iter__') and callable(cls.__iter__) 15 | 16 | class ABCIterable(metaclass=ABCIterableMeta): 17 | pass 18 | ``` 19 | 20 | ```python 21 | >>> issubclass(list, ABCIterable) 22 | True 23 | >>> issubclass(tuple, ABCIterable) 24 | True 25 | >>> issubclass(str, ABCIterable) 26 | True 27 | >>> issubclass(dict, ABCIterable) 28 | True 29 | >>> issubclass(int, ABCIterable) 30 | False 31 | >>> issubclass(object, ABCIterable) 32 | False 33 | ``` 34 | 35 | Cet exemple est d'ailleurs meilleur que le précédent, puisque comme Python il vérifie que la méthode `__iter__` est présente au niveau de la classe et pas au niveau de l'instance. 36 | 37 | Comme `isinstance`, `issubclass` peut recevoir en deuxième paramètre un *tuple* de différentes classes à tester. 38 | 39 | ```python 40 | >>> issubclass(int, (int, str)) 41 | True 42 | >>> issubclass(str, (int, str)) 43 | True 44 | >>> class Integer(int): 45 | ... pass 46 | ... 47 | >>> issubclass(Integer, (int, str)) 48 | True 49 | >>> issubclass(list, (int, str)) 50 | False 51 | ``` 52 | 53 | En revanche, pas de raccourci pour éviter l'appel à `__subclasscheck__`, même quand on cherche à vérifier qu'une classe est sa propre sous-classe. 54 | 55 | ```python 56 | >>> class NoSubclassesMeta(type): 57 | ... def __subclasscheck__(self, cls): 58 | ... return False 59 | ... 60 | >>> class NoSubclasses(metaclass=NoSubclassesMeta): 61 | ... pass 62 | ... 63 | >>> issubclass(NoSubclasses, NoSubclasses) 64 | False 65 | ``` 66 | 67 | #### Le cas des classes `ABC` 68 | 69 | Pour les classes abstraites `ABC`, c'est-à-dire qui ont `abc.ABCMeta` comme métaclasse, une facilité est mise en place. 70 | En effet, `ABCMeta` définit une méthode `__subclasscheck__` 71 | (qui s'occupe entre autres de gérer les classes enregistrées *via* `register`). 72 | 73 | Pour éviter de recourir à une nouvelle métaclasse et redéfinir `__subclasscheck__`, 74 | la méthode d'`ABCMeta` relaie l'appel à la méthode de classe `__subclasshook__`, si elle existe. 75 | Ainsi, une classe abstraite n'a qu'à définir `__subclasshook__` si elle veut étendre le comportement d'`issubclass`. 76 | 77 | ```python 78 | >>> import abc 79 | >>> class ABCIterable(abc.ABC): 80 | ... @classmethod 81 | ... def __subclasshook__(cls, subcls): 82 | ... return hasattr(subcls, '__iter__') and callable(subcls.__iter__) 83 | ... 84 | >>> issubclass(list, ABCIterable) 85 | True 86 | >>> issubclass(int, ABCIterable) 87 | False 88 | ``` 89 | 90 | On notera que la méthode `__subclasshook__` sert aussi à l'opérateur `isinstance`. 91 | 92 | ```python 93 | >>> isinstance([1, 2, 3], ABCIterable) 94 | True 95 | ``` 96 | 97 | Contrairement à `__subclasscheck__`, `__subclasshook__` ne retourne pas forcément un booléen. 98 | Elle peut en effet retourner `True`, `False`, ou `NotImplemented`. 99 | 100 | Dans le cas où elle retourne un booléen, il sera la valeur de retour de `isinstance`/`issubclass`. 101 | Mais dans le cas de `NotImplemented`, la main est rendue à la méthode `__subclasscheck__` d'`ABC`, qui s'occupe de vérifier si les classes sont parentes, ou si la classe est enregistrée (`register`). 102 | 103 | Nous allons donc réécrire notre classe `ABCIterable` de façon à retourner `True` si la classe implémente `__iter__`, et `NotImplemented` sinon. 104 | Ainsi, si la classe hérite d'`ABCIterable` mais n'implémente pas `__iter__`, elle sera tout de même considérée comme une sous-classe, ce qui n'est pas le cas actuellement. 105 | 106 | ```python 107 | >>> class ABCIterable(abc.ABC): 108 | ... @classmethod 109 | ... def __subclasshook__(cls, subcls): 110 | ... if hasattr(subcls, '__iter__') and callable(subcls.__iter__): 111 | ... return True 112 | ... return NotImplemented 113 | ... 114 | >>> issubclass(list, ABCIterable) 115 | True 116 | >>> issubclass(int, ABCIterable) 117 | False 118 | >>> class X(ABCIterable): pass 119 | ... 120 | >>> issubclass(X, ABCIterable) 121 | True 122 | ``` 123 | -------------------------------------------------------------------------------- /src/4-classes/3-abstract-classes/4-collections.md: -------------------------------------------------------------------------------- 1 | ### Collections abstraites 2 | 3 | Nous connaissons le module [`collections`](https://docs.python.org/3/library/collections.html), spécialisé dans les conteneurs ; 4 | et [`abc`](https://docs.python.org/3/library/abc.html), dédié aux classes abstraites. 5 | Que donnerait le mélange des deux ? [`collections.abc`](https://docs.python.org/3/library/collections.abc.html) ! 6 | 7 | Ce module fournit des classes abstraites toutes prêtes pour reconnaître les différentes interfaces du langage (`Container`, `Sequence`, `Mapping`, `Iterable`, `Iterator`, `Hashable`, `Callable`, etc.). 8 | 9 | Assez simples à appréhender, ces classes abstraites testent la présence de méthodes essentielles au respect de l'interface. 10 | 11 | ```python 12 | >>> import collections.abc 13 | >>> isinstance(10, collections.abc.Hashable) 14 | True 15 | >>> isinstance([10], collections.abc.Hashable) 16 | False 17 | >>> issubclass(list, collections.abc.Sequence) 18 | True 19 | >>> issubclass(dict, collections.abc.Sequence) 20 | False 21 | >>> issubclass(list, collections.abc.Mapping) 22 | False 23 | >>> issubclass(dict, collections.abc.Mapping) 24 | True 25 | ``` 26 | 27 | Outre la vérification d'interfaces, certaines de ces classes servent aussi de *mixins*, en apportant des méthodes abstraites et des méthodes prêtes à l'emploi. 28 | 29 | La classe `MutableMapping`, par exemple, a pour méthodes abstraites `__getitem__`, `__setitem__`, `__delitem__`, `__iter__` et `__len__`. 30 | Mais la classe fournit en plus l'implémentation d'autres méthodes utiles aux *mappings* : `__contains__`, `keys`, `items`, `values`, `get`, `__eq__`, `__ne__`, `pop`, `popitem`, `clear`, `update`, et `setdefault`. 31 | 32 | C'est-à-dire qu'il suffit de redéfinir les 5 méthodes abstraites pour avoir un type de dictionnaires parfaitement utilisable. 33 | 34 | ```python 35 | class MyMapping(collections.abc.MutableMapping): 36 | def __init__(self, *args, **kwargs): 37 | super().__init__() 38 | self._subdict = dict(*args, **kwargs) 39 | 40 | def __getitem__(self, key): 41 | return self._subdict[key] 42 | 43 | def __setitem__(self, key, value): 44 | self._subdict[key] = value 45 | 46 | def __delitem__(self, key): 47 | del self._subdict[key] 48 | 49 | def __iter__(self): 50 | return iter(self._subdict) 51 | 52 | def __len__(self): 53 | return len(self._subdict) 54 | ``` 55 | 56 | ```python 57 | >>> m = MyMapping() 58 | >>> m['a'] = 0 59 | >>> m['b'] = m['a'] + 1 60 | >>> len(m) 61 | 2 62 | >>> list(m.keys()) 63 | ['b', 'a'] 64 | >>> list(m.values()) 65 | [1, 0] 66 | >>> dict(m) 67 | {'b': 1, 'a': 0} 68 | >>> m.get('b') 69 | 1 70 | >>> 'a' in m 71 | True 72 | >>> m.pop('a') 73 | 0 74 | >>> 'a' in m 75 | False 76 | ``` 77 | 78 | Dans un genre similaire, on notera aussi les classes du module `numbers` : `Number`, `Complex`, `Real`, `Rational`, `Integral`. 79 | Ces classes abstraites, en plus de reconnaître l'ensemble des types numériques, permettent par héritage de créer nos propres types de nombres. 80 | -------------------------------------------------------------------------------- /src/4-classes/3-abstract-classes/5-tp.md: -------------------------------------------------------------------------------- 1 | ### TP: Reconnaissance d'interfaces 2 | 3 | Je vous propose dans ce TP de nous intéresser à la reconnaissance d'interfaces comme nous l'avons fait dans ce chapitre. 4 | Nous allons premièrement écrire une classe `Interface`, héritant de `abc.ABC`, qui permettra de vérifier qu'un type implémente un certain nombre de méthodes. 5 | Cette classe sera destinée à être héritée pour spécifier quelles méthodes doivent être implémentées par quels types. 6 | On trouvera par exemple une classe `Container` héritant d'`Interface` pour vérifier la présence d'une méthode `__contains__`. 7 | 8 | Les méthodes nécessaires pour se conformer au type seront inscrites dans un attribut de classe `__methods__`. 9 | Notre classe `Interface` définira la méthode `__subclasshook__` pour s'assurer que toutes les méthodes de la séquence `__methods__` sont présentes dans la classe. 10 | 11 | La méthode `__subclasshook__` se déroulera en 3 temps : 12 | 13 | * Premièrement, appeler l'implémentation parente *via* `super`, et retourner `False` si elle a retourné `False`. En effet, si la classe parente dit que le type n'est pas un sous-type, on est sûr qu'il n'en est pas un. Mais si la méthode parente retourne `True` ou `NotImplemented`, le doute peut persister ; 14 | * Dans un second temps, nous récupérerons la liste de toutes les méthodes à vérifier. Il ne s'agit pas seulement de l'attribut `__methods__`, mais de cet attribut ainsi que celui de toutes les classes parentes ; 15 | * Et finalement, nous testerons que chacune des méthodes est présente dans la classe, afin de retourner `True` si elle le sont toutes, et `NotImplemented` sinon. 16 | 17 | Le deuxième point va nous amener à explorer le *MRO*, à l'aide de la méthode de classe `mro`, et de concaténer les attributs `__methods__` de toutes les classes (*via* la fonction `sum`). 18 | Afin de toujours récupérer une séquence, nous utiliserons `getattr(cls, '__methods__', ())`, qui nous retournera un *tuple* vide si l'attribut `__methods__` n'est pas présent. 19 | 20 | Quant au 3ème point, la *builtin* `all` va nous permettre de vérifier que chaque nom de méthode est présent dans la classe, et qu'il s'agit d'un *callable* et donc d'une méthode. 21 | 22 | Notre classe `Interface` peut alors se présenter comme suit. 23 | 24 | ```python 25 | import abc 26 | 27 | class Interface(abc.ABC): 28 | # Attribut `__methods__` vide pour montrer sa structure 29 | __methods__ = () 30 | 31 | @classmethod 32 | def __subclasshook__(cls, subcls): 33 | # Appel au __subclasshook__ parent 34 | ret = super().__subclasshook__(cls, subcls) 35 | if not ret: 36 | return ret 37 | # Récupération de toutes les méthodes 38 | all_methods = sum((getattr(c, '__methods__', ()) for c in cls.mro()), ()) 39 | # Vérification de la présence des méthodes dans la classe 40 | if all(callable(getattr(subcls, meth, None)) for meth in all_methods): 41 | return True 42 | return NotImplemented 43 | ``` 44 | 45 | Nous pouvons dès lors créer nos nouvelles classes hérités d'`Interface` avec leurs propres attributs `__methods__`. 46 | 47 | ```python 48 | class Container(Interface): 49 | __methods__ = ('__contains__',) 50 | 51 | class Sized(Interface): 52 | __methods__ = ('__len__',) 53 | 54 | class SizedContainer(Sized, Container): 55 | pass 56 | 57 | class Subscriptable(Interface): 58 | __methods__ = ('__getitem__',) 59 | 60 | class Iterable(Interface): 61 | __methods__ = ('__iter__',) 62 | ``` 63 | 64 | Et qui fonctionnent comme prévu. 65 | 66 | ```python 67 | >>> isinstance([], Iterable) 68 | True 69 | >>> isinstance([], Subscriptable) 70 | True 71 | >>> isinstance([], SizedContainer) 72 | True 73 | >>> gen = (x for x in range(10)) 74 | >>> isinstance(gen, Iterable) 75 | True 76 | >>> isinstance(gen, Subscriptable) 77 | False 78 | >>> isinstance(gen, SizedContainer) 79 | False 80 | ``` 81 | -------------------------------------------------------------------------------- /src/4-classes/3-abstract-classes/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ### Liens utiles 2 | 3 | Pour terminer, un dernier tour par la documentation et ses pages intéressantes. 4 | 5 | * Définition du terme classe abstraite : 6 | * Personnaliser la vérification de types : 7 | * Module `abc` : 8 | * Module `collections.abc` : 9 | * Module `numbers` : 10 | -------------------------------------------------------------------------------- /src/5-exercises/0-exercises.md: -------------------------------------------------------------------------------- 1 | # Pour quelques exercices de plus 2 | 3 | ##### L'histoire sans fin 4 | 5 | Vous pensiez en avoir fini ? Malheureux ! 6 | 7 | Dans cette dernière partie, retrouvez des exercices plus complets pour mettre en œuvre les notions décrites dans ce cours. 8 | Car le Python vient en pythonant. 9 | 10 | Tous les exercices sont rangés selon le chapitre auquel ils se rapportent principalement. 11 | Mais ces exercices pouvant mêler plusieurs chapitres, les pré-requis à connaître sont chaque fois décrits en en-tête. 12 | -------------------------------------------------------------------------------- /src/5-exercises/2-3-decorators/0-decorators.md: -------------------------------------------------------------------------------- 1 | ## Décorateurs 2 | -------------------------------------------------------------------------------- /src/5-exercises/2-3-decorators/1-check-type.md: -------------------------------------------------------------------------------- 1 | ### Vérification de types 2 | 3 | **Pré-requis : Annotations, Signatures, Décorateurs** 4 | 5 | L'intérêt de ce nouvel exercice va être de vérifier dynamiquement que les types des arguments passés à notre fonction sont les bons, à l'aide d'annotations sur les paramètres de la fonction. 6 | La vérification ne pourra ainsi se faire que sur les paramètres possédant un nom. 7 | 8 | Notre décorateur va donc se charger d'analyser les paramètres lors de chaque appel à la fonction, et de les comparer un à un avec les annotations de notre fonction. 9 | Pour accéder aux annotations, nous allons à nouveau utiliser la fonction `signature`. La signature retournée comportant un attribut `parameters` contenant la liste des paramètres. 10 | 11 | Ces paramètres peuvent être de différentes natures, suivant leur emplacement dans la ligne de définition de la fonction. 12 | Par exemple, si notre fonction se définit par : 13 | 14 | ```python 15 | def f(a, b, c='', *args, d=(), e=[], **kwargs): pass 16 | ``` 17 | 18 | * `*args` est un `VAR_POSITIONNAL`, nous ne nous y intéresserons pas ici. 19 | * `**kwargs` est un `VAR_KEYWORD`, de même, il ne nous intéresse pas dans notre cas. 20 | * `a`, `b` et `c` sont des `POSITIONAL_OR_KEYWORD` (on peut les utiliser *via* des arguments positionnels ou *via* des arguments nommés). 21 | * Enfin, `d` et `e` sont des `KEYWORD_ONLY` (on ne peut les utiliser que *via* des arguments nommés). 22 | * Il existe aussi `POSITIONAL_ONLY`, mais celui-ci n'est pas représentable en Python ; il peut cependant exister dans des builtins ou des extensions. Ce type ne nous intéressera pas non plus ici. 23 | 24 | Ainsi, nous voudrons récupérer les annotations des paramètres `POSITIONAL_OR_KEYWORD` et `KEYWORD_ONLY`. 25 | Reprenons notre fonction donnée plus haut et ajoutons lui des annotations pour certains paramètres : 26 | 27 | ```python 28 | def f(a:int, b, c:str='', *args, d=(), e:list=[], **kwargs): pass 29 | ``` 30 | 31 | Analysons maintenant les paramètres tels que définis dans la signature de la fonction. 32 | 33 | ```python 34 | >>> for p in signature(f).parameters.values(): 35 | ... print(p.name, p.kind, p.annotation) 36 | a POSITIONAL_OR_KEYWORD 37 | b POSITIONAL_OR_KEYWORD 38 | c POSITIONAL_OR_KEYWORD 39 | args VAR_POSITIONAL 40 | d KEYWORD_ONLY 41 | e KEYWORD_ONLY 42 | kwargs VAR_KEYWORD 43 | ``` 44 | 45 | Nous retrouvons donc les types énoncés plus haut, ainsi que nos annotations (ou `empty` quand aucune annotation n'est donnée). 46 | Nous stockerons donc d'un côté les annotations des `POSITIONAL_OR_KEYWORD` dans une liste, et de l'autre celles des `KEYWORD_ONLY` dans un dictionnaire. 47 | Par la suite, `bind` rangera pour nous les paramètres de ce premier type dans `args`, et ceux du second dans `kwargs`. 48 | 49 | ```python 50 | from functools import wraps 51 | from inspect import signature 52 | 53 | def check_types(f): 54 | sig = signature(f) 55 | # Nous récupérons les types des paramètres positionnels 56 | # (en remplaçant les annotations vides par None pour conserver l'ordre) 57 | args_types = [(p.annotation if p.annotation != sig.empty else None) 58 | for p in sig.parameters.values() 59 | if p.kind == p.POSITIONAL_OR_KEYWORD] 60 | # Puis ceux des paramètres nommés 61 | # (il n'est pas nécessaire de conserver les empty ici) 62 | kwargs_types = {p.name: p.annotation for p in sig.parameters.values() 63 | if p.kind == p.KEYWORD_ONLY and p.annotation != p.empty} 64 | @wraps(f) 65 | def decorated(*args, **kwargs): 66 | # On range correctement les paramètres des deux types 67 | bind = sig.bind(*args, **kwargs) 68 | # Vérification des paramètres positionnels 69 | for value, typ in zip(bind.args, args_types): 70 | if typ and not isinstance(value, typ): 71 | raise TypeError('{} must be of type {}'.format(value, typ)) 72 | # Et des paramètres nommés 73 | for name, value in bind.kwargs.items(): 74 | # Si le type n'est pas précisé par l'annotation, on considère object 75 | # (toutes les valeurs sont de type object) 76 | typ = kwargs_types.get(name, object) 77 | if not isinstance(value, typ): 78 | raise TypeError('{} must be of type {}'.format(value, typ)) 79 | return f(*args, **kwargs) 80 | return decorated 81 | ``` 82 | 83 | Voyons maintenant ce que donne notre décorateur à l'utilisation. 84 | 85 | ```python 86 | >>> @check_types 87 | ... def addition(a:int, b:int): 88 | ... return a + b 89 | ... 90 | >>> @check_types 91 | ... def concat(a:str, b:str): 92 | ... return a + b 93 | ... 94 | >>> addition(1, 2) 95 | 3 96 | >>> concat('x', 'y') 97 | 'xy' 98 | >>> addition(1, 'y') 99 | Traceback (most recent call last): 100 | File "", line 1, in 101 | File "", line 19, in decorated 102 | TypeError: y must be of type 103 | >>> concat(1, 2) 104 | Traceback (most recent call last): 105 | File "", line 1, in 106 | File "", line 19, in decorated 107 | TypeError: 1 must be of type 108 | ``` 109 | -------------------------------------------------------------------------------- /src/5-exercises/2-3-decorators/2-memoize.md: -------------------------------------------------------------------------------- 1 | ### Mémoïsation 2 | 3 | **Pré-requis : Signatures, Décorateurs** 4 | 5 | Un des exemples les plus courants de mise en pratique des décorateurs est la réalisation d'un système de mise en cache (mémoïsation) : sauvegarder les résultats d'un calcul pour éviter de le refaire à chaque appel. 6 | 7 | Nous allons débuter par une version simple : pour chaque appel, nous enregistrerons la valeur de retour associée au couple `(args, kwargs)` si celle-ci n'existe pas déjà. Dans le cas contraire, il nous suffira de retourner la valeur existante. 8 | 9 | Seul bémol, nous ne pouvons pas stocker directement `(args, kwargs)` comme clef de notre dictionnaire, car certains objets n'y sont pas hashables (car modifiables, tel que le dictionnaire). 10 | Nous procéderons donc à l'aide d'une sérialisation via `repr` pour obtenir la représentation de nos paramètres sous forme d'une chaîne de caractères. 11 | 12 | ```python 13 | import functools 14 | 15 | def memoize(f): 16 | cache = {} 17 | @functools.wraps(f) 18 | def decorated(*args, **kwargs): 19 | key = repr((args, kwargs)) 20 | if key not in cache: 21 | cache[key] = f(*args, **kwargs) 22 | return cache[key] 23 | return decorated 24 | ``` 25 | 26 | Je vous conseille de le tester sur une fonction procédant à des affichages, pour bien constater la mise en cache. 27 | 28 | ```python 29 | >>> @memoize 30 | ... def addition(a, b): 31 | ... print('Computing addition of {} and {}'.format(a, b)) 32 | ... return a + b 33 | ... 34 | >>> addition(3, 5) 35 | Computing addition of 3 and 5 36 | 8 37 | >>> addition(3, 5) 38 | 8 39 | >>> addition(3, 6) 40 | Computing addition of 3 and 6 41 | 9 42 | ``` 43 | 44 | Comme je le disais, c'est une version simple, dans le sens où s'il nous venait à l'esprit d'utiliser une fonction tantôt avec des arguments positionnels, tantôt avec des arguments nommés, nous ne bénéficierions pas des capacités du cache. 45 | 46 | ```python 47 | >>> addition(3, b=5) 48 | Computing addition of 3 and 5 49 | 8 50 | >>> addition(a=3, b=5) 51 | Computing addition of 3 and 5 52 | 8 53 | ``` 54 | 55 | #### Signatures 56 | 57 | Afin d'avoir une représentation unique de nos arguments, nous allons alors utiliser les signatures de fonctions, et leur méthode `bind`. 58 | Nous obtiendrons ainsi un couple unique `(args, kwargs)`, où tous les arguments nommés qui peuvent l'être seront transfomés en positionnels. 59 | 60 | Il ne s'agit que de peu de modifications dans notre code. 61 | 62 | ```python 63 | import functools 64 | from inspect import signature 65 | 66 | def memoize(f): 67 | cache = {} 68 | sig = signature(f) 69 | @functools.wraps(f) 70 | def decorated(*args, **kwargs): 71 | bind = sig.bind(*args, **kwargs) 72 | key = repr((bind.args, bind.kwargs)) 73 | if not key in cache: 74 | cache[key] = f(*args, **kwargs) 75 | return cache[key] 76 | return decorated 77 | ``` 78 | 79 | À l'utilisation, nous obtenons donc : 80 | 81 | ```python 82 | >>> @memoize 83 | ... def addition(a, b): 84 | ... print('Computing addition of {} and {}'.format(a, b)) 85 | ... return a + b 86 | ... 87 | >>> addition(3, 5) 88 | Computing addition of 3 and 5 89 | 8 90 | >>> addition(3, 5) 91 | 8 92 | >>> addition(3, b=5) 93 | 8 94 | >>> addition(b=5, a=3) 95 | 8 96 | >>> addition(5, 3) 97 | Computing addition of 5 and 3 98 | 8 99 | ``` 100 | 101 | Nous avons déjà parlé de [`functools`](https://docs.python.org/3/library/functools.html) à plusieurs reprises dans ce cours. 102 | Si vous y avez prêté attention, vous avez remarqué que le décorateur que nous venons d'implémenter ressemble beaucoup à `lru_cache` (à l'exception près que notre version gère les types non-hashables, mais avec une perte de performances et une moins bonne fiabilité). 103 | -------------------------------------------------------------------------------- /src/5-exercises/2-3-decorators/3-singledispatch.md: -------------------------------------------------------------------------------- 1 | ### Fonctions génériques 2 | 3 | **Pré-requis : Callables, Décorateurs** 4 | 5 | Nous allons ici nous intéresser à `singledispatch`, une fonction du module `functools`. 6 | Il s'agit d'une implémentation de fonctions génériques en Python, permettant de *dispatcher* l'appel en fonction du type du premier paramètre. 7 | 8 | La généricité est un concept qui permet d'appeler une fonction avec des arguments de types variables. 9 | C'est le cas par défaut en Python : les variables étant typées dynamiquement, il est possible d'appeler les fonctions quels que soient les types des arguments envoyés. 10 | 11 | Mais, de pair avec la généricité vient le concept de spécialisation, qui est plus subtil en Python. 12 | Spécialiser une fonction générique, c'est fournir une implémentation différente de la fonction pour certains types de ses paramètres. 13 | 14 | En Python, `singledispatch` permet de spécialiser une fonction selon le type de son premier paramètre. 15 | Il est ainsi possible de définir plusieurs fois une même fonction, en spécifiant le type sur lequel on souhaite la spécialiser. 16 | 17 | `singledispatch` est un décorateur, prenant donc une fonction en paramètre (la fonction qui sera appelée si aucune spécialisation n'est trouvée), et retournant un nouveau *callable*. 18 | Ce *callable* possède une méthode `register`, qui s'utilisera comme un décorateur paramétré par le type pour lequel nous voulons spécialiser notre fonction. 19 | 20 | Lors de chaque appel au *callable* retourné par `singledispatch`, la fonction à appeler sera déterminée selon le type du premier paramètre. 21 | Nos appels devront donc posséder au minimum un argument positionnel. 22 | 23 | ```python 24 | >>> @singledispatch 25 | ... def print_type(arg): 26 | ... print('Je ne connais pas le type de ce paramètre') 27 | ... 28 | >>> @print_type.register(int) 29 | ... def _(arg): # Le nom doit être différent de print_type 30 | ... print(arg, 'est un entier') 31 | ... 32 | >>> @print_type.register(str) 33 | ... def _(arg): 34 | ... print(arg, 'est une chaîne') 35 | ... 36 | >>> print_type(15) 37 | 15 est un entier 38 | >>> print_type('foo') 39 | foo est une chaîne 40 | >>> print_type([]) 41 | Je ne connais pas le type de ce paramètre 42 | ``` 43 | 44 | Pour notre implémentation, je vous propose pour cette fois de réaliser le décorateur à l'aide d'une classe. Cela nous permettra d'avoir facilement un attribut `registry` à disposition. 45 | 46 | ```python 47 | import functools 48 | 49 | class singledispatch: 50 | def __init__(self, func): 51 | self.default = func 52 | self.registry = {} 53 | functools.update_wrapper(self, func) 54 | 55 | def __call__(self, *args, **kwargs): 56 | func = self.registry.get(type(args[0]), self.default) 57 | return func(*args, **kwargs) 58 | 59 | def register(self, type_): 60 | def decorator(func): 61 | self.registry[type_] = func 62 | return func 63 | return decorator 64 | ``` 65 | 66 | Il faut donc bien comprendre que c'est `__init__` qui sera appelée lors de la décoration de la première fonction, puisque cela revient à instancier un objet `singledispatch`. 67 | Cet objet contient alors une méthode `register` pour enregistrer des spécialisations de la fonction. 68 | Et enfin, il est appelable, *via* sa méthode `__call__`, qui déterminera laquelle des fonctions enregistrées appeler. 69 | 70 | Pour aller plus loin, nous pourrions aussi permettre de *dispatcher* en fonction du type de tous les paramètres, ou encore utiliser les annotations pour préciser les types. 71 | -------------------------------------------------------------------------------- /src/5-exercises/2-3-decorators/4-tail-rec.md: -------------------------------------------------------------------------------- 1 | ### Récursivité terminale 2 | 3 | **Pré-requis : Callables, Décorateurs** 4 | 5 | La récursivité terminale est un concept issu du [paradigme fonctionnel](https://fr.wikipedia.org/wiki/Programmation_fonctionnelle), permettant d'optimiser les appels récursifs. 6 | 7 | Chaque fois que vous réalisez un appel de fonction, un contexte doit se mettre en place afin de contenir les variables locales à la fonction (dont les paramètres). 8 | Il doit être conservé jusqu'à la fin de l'exécution de la fonction. 9 | 10 | Ces contextes sont stockés dans une zone mémoire appelée la pile, de taille limitée. Lors d'appels récursifs, les fonctions parentes restent présentes dans la pile, car n'ont pas terminé leur exécution. 11 | Donc plus on s'enfonce dans les niveaux de récursivité, plus la pile se remplit, jusqu'à parfois être pleine. 12 | Une fois pleine, il n'est alors plus possible d'appeler de nouvelles fonctions, cela est représenté par l'exception `RecursionError` en Python. 13 | 14 | Si vous avez déjà tenté d'écrire des fonctions récursives en Python, vous vous êtes rapidement confronté à l'impossibilité de descendre au-delà d'un certain niveau de récursion, à cause de la taille limitée de la pile d'appels. 15 | 16 | ```python 17 | >>> def factorial(n): 18 | ... if not n: 19 | ... return 1 20 | ... return n * factorial(n - 1) 21 | ... 22 | >>> factorial(5) 23 | 120 24 | >>> factorial(1000) 25 | Traceback (most recent call last): 26 | File "", line 1, in 27 | File "", line 4, in factorial 28 | File "", line 4, in factorial 29 | [...] 30 | File "", line 4, in factorial 31 | RecursionError: maximum recursion depth exceeded in comparison 32 | ``` 33 | 34 | Certains langages, notamment les langages fonctionnels, ont réussi à résoudre ce problème, à l'aide de la récursivité terminale. Il s'agit en fait d'identifier les appels terminaux dans la fonction (c'est-à-dire quand aucune autre opération n'est effectuée après l'appel récursif). 35 | Si l'appel est terminal, cela signifie que l'on ne fera plus rien d'autre dans la fonction, et il est alors possible de supprimer son contexte de la pile, et ainsi économiser de l'espace à chaque appel récursif. 36 | 37 | Prenons la fonction `factorial` codée plus haut : elle ne peut pas être optimisée par récursivité terminale. 38 | En effet, une multiplication est encore effectuée entre l'appel récursif et le `return`. 39 | 40 | Prenons maintenant cette seconde implémentation : 41 | 42 | ```python 43 | def factorial(n, acc=1): 44 | if not n: 45 | return acc 46 | return factorial(n - 1, acc * n) 47 | ``` 48 | 49 | Le problème est ici résolu : la multiplication est effectuée avant l'appel puisque dans les arguments. Cette deuxième fonction peut donc être optimisée. 50 | 51 | Cependant, [la récursivité terminale n'existe pas en Python](http://neopythonic.blogspot.com.au/2009/04/tail-recursion-elimination.html). Guido von Rossum le dit lui-même. 52 | Mais il nous est possible de la simuler, en reproduisant le comportement voulu, avec un décorateur dédié. 53 | 54 | En fait, nous allons nous contenter d'ajouter uné méthode `call` à nos fonctions. 55 | Lorsque nous ferons `function.call(...)`, nous n'appellerons pas réellement la fonction, mais enregistrerons l'appel. 56 | Le *wrapper* de notre fonction sera ensuite chargé d'exécuter en boucle tous ces appels enregistrés. 57 | 58 | Il faut bien noter que le retour de la méthode `call` ne sera pas le retour de notre fonction. Il s'agira d'un objet temporaire qui servira à réaliser plus tard le réel appel de fonction, dans le *wrapper*. 59 | 60 | Nous nous appuierons sur une classe `tail_rec_exec`, qui n'est autre qu'un *tuple* comportant la fonction à appeler et ses arguments (`args` et `kwargs`). 61 | 62 | ```python 63 | class tail_rec_exec(tuple): 64 | pass 65 | ``` 66 | 67 | Maintenant nous allons réaliser notre décorateur `tail_rec`, j'ai opté pour une classe : 68 | 69 | ```python 70 | import functools 71 | 72 | class tail_rec: 73 | def __init__(self, func): 74 | self.func = func 75 | functools.update_wrapper(self, func) 76 | 77 | def call(self, *args, **kwargs): 78 | return tail_rec_exec((self.func, args, kwargs)) 79 | 80 | def __call__(self, *args, **kwargs): 81 | r = self.func(*args, **kwargs) 82 | # Nous exécutons les appels "récursifs" tant que le retour est de type tail_rec_exec 83 | while isinstance(r, tail_rec_exec): 84 | func, args, kwargs = r 85 | r = func(*args, **kwargs) 86 | return r 87 | ``` 88 | 89 | La méthode `__call__` sera celle utilisée lorsque nous appellerons notre fonction, et la méthode `call` utilisée pour temporiser appel. 90 | 91 | À l'utilisation, cela donne : 92 | 93 | ```python 94 | @tail_rec 95 | def my_sum(values, acc=0): 96 | if not values: 97 | return acc 98 | return my_sum.call(values[1:], acc + values[0]) 99 | 100 | @tail_rec 101 | def factorial(n, acc=1): 102 | if not n: 103 | return acc 104 | return factorial.call(n - 1, acc * n) 105 | 106 | @tail_rec 107 | def even(n): 108 | if not n: 109 | return True 110 | return odd.call(n - 1) 111 | 112 | @tail_rec 113 | def odd(n): 114 | if not n: 115 | return False 116 | return even.call(n - 1) 117 | ``` 118 | 119 | ```python 120 | >>> my_sum(range(5000)) 121 | 12497500 122 | >>> factorial(5) 123 | 120 124 | >>> factorial(1000) 125 | 4023872600770937735437024... 126 | >>> even(5000) 127 | True 128 | >>> odd(5000) 129 | False 130 | >>> even(5001) 131 | False 132 | >>> odd(5001) 133 | True 134 | ``` 135 | -------------------------------------------------------------------------------- /src/5-exercises/3-2-context-managers/0-context-managers.md: -------------------------------------------------------------------------------- 1 | ## Gestionnaires de contexte 2 | -------------------------------------------------------------------------------- /src/5-exercises/3-2-context-managers/1-changedir.md: -------------------------------------------------------------------------------- 1 | ### Changement de répertoire 2 | 3 | **Pré-requis : Gestionnaires de contexte** 4 | 5 | Dans cet exercice, je vous propose d'implémenter un gestionnaire de contexte pour gérer le répertoire courant. 6 | En effet, on voudrait pouvoir changer temporairement de dossier courant, sans effet de bord sur la suite du programme. 7 | 8 | Nous voudrions aussi notre gestionnaire de contexte réutilisable et réentrant, à la manière du TP `redirect_stdout`, afin de ne pas perdre le répertoire de départ en cas de contextes imbriqués. 9 | 10 | Ainsi, à la construction de l'objet, on enregistrerait le dossier cible, qui serait passé en paramètre. 11 | Puis, à l'entrée du contexte, on garderait une trace du dossier courant (`os.getcwd()`) dans une pile de dossiers, avant de se déplacer vers la cible (`os.chdir`). 12 | En sortie, il nous suffirait de nous déplacer à nouveau vers le précédent dossier courant (le dernier élément de la pile). 13 | 14 | ```python 15 | import os 16 | 17 | class changedir: 18 | def __init__(self, target): 19 | self.target = target 20 | self.stack = [] 21 | 22 | def __enter__(self): 23 | current = os.getcwd() 24 | self.stack.append(current) 25 | os.chdir(self.target) 26 | 27 | def __exit__(self, exc_type, exc_value, traceback): 28 | old = self.stack.pop() 29 | os.chdir(old) 30 | ``` 31 | 32 | On constate que ce gestionnaire répond bien aux utilisations simples… 33 | 34 | ```python 35 | >>> os.getcwd() 36 | '/home/entwanne' 37 | >>> with changedir('/tmp'): 38 | ... os.getcwd() 39 | ... 40 | '/tmp' 41 | >>> os.getcwd() 42 | '/home/entwanne' 43 | ``` 44 | 45 | … comme aux complexes. 46 | 47 | ```python 48 | >>> cd = changedir('/tmp') 49 | >>> os.getcwd() 50 | '/home/entwanne' 51 | >>> with cd: 52 | ... with cd: 53 | ... os.getcwd() 54 | ... 55 | '/tmp' 56 | >>> os.getcwd() 57 | '/home/entwanne' 58 | ``` 59 | -------------------------------------------------------------------------------- /src/5-exercises/3-2-context-managers/3-suppress.md: -------------------------------------------------------------------------------- 1 | ### Suppression d'erreurs 2 | 3 | **Pré-requis : Gestionnaires de contexte** 4 | 5 | Vous pensiez en avoir terminé avec `contextlib` ? Que nenni ! 6 | Ce module présente beaucoup de gestionnaires de contexte assez simples à réimplémenter, il est donc idéal pour les exercices. 7 | 8 | Intéressons-nous maintenant à `suppress`, qui permet d'ignorer des exceptions, comme pourrait le faire un `try`/`except`. 9 | En effet, les deux exemples de codes qui suivent sont équivalents. 10 | 11 | ```python 12 | from contextlib import suppress 13 | 14 | with suppress(TypeError): 15 | print(1 + '2') 16 | ``` 17 | 18 | ```python 19 | try: 20 | print(1 + '2') 21 | except TypeError: 22 | pass 23 | ``` 24 | 25 | Nous l'avons vu, les erreurs qui surviennent dans un contexte sont transmises à la méthode `__exit__` du gestionnaire, qui peut choisir d'annuler l'exception en retournant `True`. 26 | Tout ce que nous avons à faire est donc de vérifier si une exception est survenue et si cette dernière est du bon type, puis retourner `True` si ces deux conditions sont respectées. 27 | 28 | Pour vérifier que l'exception est du bon type, il nous suffira de faire appel à `issubclass` et tester que le pramètre `exc_type` est un sous-type de celui passé à la construction du gestionnaire. 29 | 30 | Le code de notre gestionnaire de contexte se présente alors comme suit. 31 | 32 | ```python 33 | class suppress: 34 | def __init__(self, exc_type): 35 | self.exc_type = exc_type 36 | 37 | def __enter__(self): 38 | pass 39 | 40 | def __exit__(self, exc_type, exc_value, traceback): 41 | return exc_type is not None and issubclass(exc_type, self.exc_type) 42 | ``` 43 | 44 | Je vous laisse expérimenter et vérifier que notre classe répond bien aux attentes. 45 | Une petite subtilité tout de même : `suppress` peut normalement s'utiliser en spécifiant plusieurs types d'exception à annuler. 46 | 47 | ```python 48 | with suppress(TypeError, ValueError, IndexError): 49 | print(1 + '2') 50 | ``` 51 | 52 | Étant donné qu'`issubclass` peut prendre un *tuple* en second paramètre, la modification du code de notre gestionnaire de contexte sera très rapide. 53 | 54 | ```python 55 | class suppress: 56 | def __init__(self, *exc_types): 57 | self.exc_types = exc_types 58 | 59 | def __enter__(self): 60 | pass 61 | 62 | def __exit__(self, exc_type, exc_value, traceback): 63 | return exc_type is not None and issubclass(exc_type, self.exc_types) 64 | ``` 65 | -------------------------------------------------------------------------------- /src/5-exercises/3-3-accessors-descriptors/0-accesors-descriptors.md: -------------------------------------------------------------------------------- 1 | ## Accesseurs et descripteurs 2 | -------------------------------------------------------------------------------- /src/5-exercises/4-2-metaclasses/0-metaclasses.md: -------------------------------------------------------------------------------- 1 | ## Métaclasses 2 | -------------------------------------------------------------------------------- /src/x-conclusion.md: -------------------------------------------------------------------------------- 1 | ## Conclusion 2 | 3 | Ce cours touche maintenant à sa fin. 4 | Puisse-t-il vous avoir fait découvrir de nouveaux concepts du Python, ou l'envie de voir encore plus loin. 5 | 6 | Ce cours ne couvre en effet qu'un nombre restreint de domaines, qui pourraient chacun être plus approfondis. 7 | Il y aurait encore tant à dire, sur les coroutines, sur les utilisations de l'interpréteur, sur les outils, les bibliothèques, *les frameworks*. 8 | À elle seule, la bibliothèque standard regorge encore de nombreuses choses, et je vous invite à voguer dans les pages de sa documentation. 9 | 10 | Nous nous sommes ici surtout intéressés aux pages de documentation sur les types et le modèle de données. 11 | Mais elle comporte bien d'autres sections comme des tutoriaux, des *recettes*, la description de l'API C, etc. 12 | 13 | Cependant, bien que cette documentation soit assez complète, elle ne l'est pas autant que le code source de l'interpréteur CPython. 14 | Écrit en C, son code reste très accessible et permet de mieux comprendre les mécanismes internes du langage. 15 | 16 | Si vous souhaitez en apprendre plus sur la philosophie du langage, ou sur la bonne utilisation des concepts décrits dans le cours, je vous renvoie à [cet article sur le code pythonique](https://zestedesavoir.com/articles/1079/les-secrets-dun-code-pythonique/). 17 | 18 | Je tiens aussi à remercier les différents contributeurs ayant aidé à l'élaboration de ce projet : 19 | 20 | * [**Vayel**](https://zestedesavoir.com/membres/voir/Vayel/), pour ses longues et intensives relectures ; 21 | * [**nohar**](https://zestedesavoir.com/membres/voir/nohar/), pour les précisions apportées tout le long de l'écriture du tutoriel ; 22 | * [**Bibibye**](https://zestedesavoir.com/membres/voir/Bibibye/), pour les nombreuses typographies corrigées ; 23 | * [**yoch**](https://zestedesavoir.com/membres/voir/yoch/), pour ses diverses corrections. 24 | 25 | Notez enfin que ce cours est diffusé sous licence *Creative Commons Attribution-ShareAlike 4.0* et que toute contribution est bienvenue. 26 | Les sources sont disponibles à l'adresse suivante : 27 | -------------------------------------------------------------------------------- /title.md: -------------------------------------------------------------------------------- 1 | % Notions de Python avancées 2 | % [Antoine Rozo (entwanne)](https://github.com/entwanne) 3 | 4 | \newpage 5 | 6 | ![Notions de Python avancées](logo_cours.png) 7 | 8 | **Cours diffusé sous licence [*Creative Commons Attribution-ShareAlike 4.0*](https://creativecommons.org/licenses/by-sa/4.0/).** 9 | --------------------------------------------------------------------------------