URL: https://linuxfr.org/news/python-3-9-est-disponible Title: Python 3.9 est disponible Authors: Cyrille Pontvieux Benoît Sibaud, palm123, ariasuni, Ysabeau, bobble bubble et Snark Date: 2020-11-03T15:08:31+01:00 License: CC By-SA Tags: python et sortie_version Score: 4 Python **3.9** est sorti le 5 octobre 2020, après 17 mois de développement. Voyons ce que cette version apporte comme nouveautés… ![Python logo](https://www.python.org/static/img/python-logo.png) ---- [Notes de versions](https://www.python.org/downloads/release/python-390/?ref=hvper.com) ---- Cette version comporte pas moins de **neuf nouveautés** par rapport à la version 3.8 sortie le 14 octobre 2019, soit presque un an après. # Calendrier de sortie La première chose qu’on peut noter c’est justement la date de sortie. Habituellement, les versions de Python sortent tous les 18 mois. La [PEP 602](https://www.python.org/dev/peps/pep-0602/) décide de changer ça et de faire des sorties tous les **12 mois** et ceci pour plusieurs raisons : - diminuer la taille des versions - apporter de nouvelles fonctionnalités plus vite - avoir une politique de mise à jour plus lissée (obsolescence graduelle) - avoir un calendrier prévisible des sorties (toujours en octobre) pour caler les réunions développeurs, conventions, intégrations dans les distributions (surtout Fedora apparemment) Toutefois le développement se fait sur **17 mois** : - les **5 premiers mois** se font sans numéro de version et chevauchent les versions _bêtas_ et _rc_ de la version `n-1`, - les **7 mois** suivants sont consacrés aux versions _alpha_ (ajout de fonctionnalités toujours possible), - les **3 mois** suivants sont consacrés aux versions _beta_ (plus d’ajout de fonctionnalité possible), - les **2 derniers mois** sont pour les versions _rc_. Les versions majeures (`3.X`) sont maintenues pendant **5 ans** : - corrections et sorties complètes pendant **1 an ½**, fréquence de sortie mensuelle, - corrections de sécurité uniquement et sorties sous forme de source uniquement pendant **3 ans ½**. # [PEP 573](https://www.python.org/dev/peps/pep-0573/) – Amélioration de performance pour les extensions C questionnant l’état de leur module Les modules Python et les extensions Python ne sont pas gérés de la même façon à ce jour. Les extensions : - n’ont pas de moyen de libérer la mémoire au déchargement de l’extension - ne peuvent pas savoir si elles ont déjà été chargées - n’ont pas conscience de leur module et ne peuvent donc pas être chargées plusieurs fois Il y avait quelques astuces pour pouvoir avoir un état du module (en utilisant des variables globales par exemple) mais l’amélioration apportée dans Python 3.9 permet aux développeurs·ses de modules qui le souhaitent d’avoir les informations nécessaires directement (instance, classe, module…). Ça devrait permettre de corriger les [PEP 3121](https://www.python.org/dev/peps/pep-3121/) et [PEP 489](https://www.python.org/dev/peps/pep-0489/) et d’avoir des modules : - qui prennent moins de mémoire - qui libèrent correctement leurs ressources au déchargement (mémoire, fichier ouvert…) - qui accèdent plus facilement aux informations de contexte - qui sont moins difficiles à lire/maintenir - qui peuvent être instanciés plusieurs fois sans soucis ou au contraire se comporter comme un singleton Pour bénéficier de ces avantages, les modules d’extension C doivent faire de menues modifications dans leur code source. Cela parait suffisamment petit/simple pour que ce soit géré par une macro qui génère du code compatible Python 3.8− et Python 3.9. Les modules *builtin* ont déjà été modifiés en conséquence. D’autres optimisations viendront s’ajouter en Python 3.10 pour les modules d’extension. # [PEP 584](https://www.python.org/dev/peps/pep-0584/) – Opérateurs d’union pour les dictionnaires « Comment fusionner deux dictionnaires en Python en une instruction ? » C’est une des [questions](https://stackoverflow.com/questions/38987/how-do-i-merge-two-dictionaries-in-a-single-expression-in-python-taking-union-o) les plus vieilles et plus lue sur StackOverflow à propos de Python : vieille de plus de **12 ans**, voté à **plus de 5 000**, dans les favoris **plus 1 000** fois, contient **47 réponses** et aucune n’est satisfaisante. Il n’y a pas non plus une façon plus évidente qui se détacherait des autres. En Python, on préfère qu’il n’y ait qu’une façon évidente de faire les choses si possible. C’est tellement courant comme besoin que les développeurs Python on finit par trouver une solution et en plus elle est élégante. Pour faire l’union (ou la fusion) de deux dictionnaires, il a été décidé d’utiliser l’opérateur `|`, déjà utilisé dans les opérations sur les bits ou sur les `set`. Pour fusionner `d1` avec `d2` et obtenir un nouveau dictionnaire, on peut donc faire : ```python d3 = d1 | d2 ``` Attention toutefois, l’opérateur n’est **pas commutable**, c’est-à-dire que `d1 | d2` n’est pas forcément équivalent à `d2 | d1`. Les clés/valeurs de droites écrasants celles de gauche. # [PEP 585](https://www.python.org/dev/peps/pep-0585/) – _Generics_ dans les collections pour les _typehint_ sans import depuis `typing` Python peut gérer du typage à l’analyse de syntaxe (via des outils supplémentaires) mais aussi à l’exécution. La situation actuelle repose sur une succession de PEPs ([484](https://www.python.org/dev/peps/pep-0484/), [526](https://www.python.org/dev/peps/pep-0526/), [544](https://www.python.org/dev/peps/pep-0544/), [560](https://www.python.org/dev/peps/pep-0560/), et [563](https://www.python.org/dev/peps/pep-0563/)) et ont abouti à l’existence d’une hiérarchie de types génériques dupliqués : par exemple `typing.List` et le type interne `list`. Exemple fictif : ```python from typing import List def first_int_elem(l: List) -> int: return int(l[0]) if l else None s = ("1", "2", "3") print(f"{first_int_elem(list(s))=}") ``` On devait utiliser à la fois la `list` interne et la `List` de `typing`. Dans ce cas précis, on pouvait toutefois utiliser directement `list` car on ne précise pas une liste de quoi. Prenons alors un exemple plus précis : ```python from typing import List def first_int_elem(l: List[int]) -> int: return l[0] if l else None s = (1, 2, 3) print(f"{first_int_elem(list(s))=}") ``` Ici, et jusqu’à précédemment, il était impossible de substituer le type `List` de `typing` par la `list` interne, la syntaxe `list[int]` n’étant pas acceptée (`TypeError: 'type' object is not subscriptable`). Ceci est maintenant possible en Python 3.9 pour les types internes suivants : - `tuple` - `list` - `dict` - `set` - `frozenset` - `type` - tout ce qui se trouve dans `collections` - `contextlib.AbstractContextManager` - `contextlib.AbstractAsyncContextManager` - `re.Pattern` - `re.Match` ```Python def first_int_elem(l: list[int]) -> int: return l[0] if l else None s = (1, 2, 3) print(f"{first_int_elem(list(s))=}") ``` Un type peut accepter des paramètres génériques s’il implémente la méthode `__class_getitem__`. Ces paramètres génériques sont conservés au *runtime* dans un attribut `__args__`. L’instanciation d’un type générique ne conserve pas les paramètres génériques, ainsi `list()` et `list[int]()` désigne le même type de liste et peuvent, au *runtime*, contenir des objets de tout type. L’import des types équivalents depuis `typing` est déprécié mais ne génère pas d’avertissement. # [PEP 593](https://www.python.org/dev/peps/pep-0593/) – Possibilité d’annoter un _typehint_ avec une expression quelconque Avec `Annoted` du module `typing` il est maintenant possible d’attacher/annoter une expression quelconque sur un type. Cette annotation peut être lue par un analyseur de type ou au *runtime*. Ça permet par exemple à framework de définir des informations supplémentaires sur des types primitifs (int, str…) À l’exécution `get_type_hints` a été enrichi pour pouvoir lire ces annotations avec `include_extras` ```python @struct2.packed class Student(NamedTuple): name: Annotated[str, struct.ctype("<10s")] get_type_hints(Student) == {'name': str} get_type_hints(Student, include_extras=False) == {'name': str} get_type_hints(Student, include_extras=True) == { 'name': Annotated[str, struct.ctype("<10s")] } ``` `Annoted` peut prendre plusieurs annotations (l’ordre est conservé). Toutefois si une annotation est de type `Annoted`, la liste résultante sera aplatie : ```python Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[ int, ValueRange(3, 10), ctype("char") ] ``` On peut utiliser les annotations avec des paramètres génériques : ```python Typevar T = ... Vec = Annotated[List[Tuple[T, T]], MaxLen(10)] V = Vec[int] V == Annotated[List[Tuple[int, int]], MaxLen(10)] ``` Ces annotations peuvent permettre de simplifier du code et voir émerger de nouveaux cadriciels. # [PEP 614](https://www.python.org/dev/peps/pep-0614/) – Les décorateurs peuvent être des expressions complètes C’est une modification mineure, qui ne concernera probablement que ceux créant/utilisant certains cadriciels, mais ça peut permettre de largement simplifier du code. Quand on applique un décorateur en Python, la syntaxe autorisée était : > `@` + `variable` (+ `.` + `attribut`)* + (`(` + parameters + `)`)? Ça parait suffisant, mais en fait c’était hyper restrictif. Prenons l’exemple d’un cadriciel fictif qui permet de brancher des fonctions sur des clics sur des boutons UI : ```python buttons = [QPushButton(f'Button {i}') for i in range(10)] # Do stuff with the list of buttons... button_0 = buttons[0] @button_0.clicked.connect def spam(): ... button_1 = buttons[1] @button_1.clicked.connect def eggs(): ... ``` On est obligé d’utiliser une variable temporaire car la syntaxe est restrictive pour les décorateurs. À partir de cette version 3.9, on peut utiliser des expressions complètes pour décorer une fonction : ```python buttons = [QPushButton(f'Button {i}') for i in range(10)] # Do stuff with the list of buttons... @buttons[0].clicked.connect def spam(): ... @buttons[1].clicked.connect def eggs(): ... ``` Bref, une bonne chose qui peut simplifier le code. # [PEP 615](https://www.python.org/dev/peps/pep-0615/) – Intégration des [timezones IANA](https://en.wikipedia.org/wiki/Tz_database) dans Python Un nouveau module intégré a été ajout en Python 3.9 : `zoneinfo` Le module se base sur les fuseaux horaires définies dans le système, se reposant sur les données IANA. La plupart des systèmes ont ces informations et sont donc mis à jour régulièrement. Pour d’autres (Windows), cette base de données n’est pas disponible. Dans ce cas le paquet Python `tzdata` contient une base de données à jour et est utilisé si installé. Cela permet, sauf si j’ai mal compris, de se passer du paquet `pytz` est de pouvoir jouer avec les dates et leur `timezone` plus simplement qu’avant. Une variable d’environnement (`PYTHONTZPATH`) ainsi qu’une fonction (`zoneinfo.reset_tzpath`) permettent de changer les chemins de recherche des données IANA. Le module contient surtout une classe `ZoneInfo` qui permet de : - construire une timezone basé sur un nom de zone. Exemple `Europe/Paris`. Les timezones construits sont mis en cache. - être utilisé dans les arguments `tzinfo` des classes/fonctions du module `datetime` ```python zone = ZoneInfo("Europe/Paris") dt = datetime(2020, 12, 3, 16, 15, tzinfo=zone) ``` # [PEP 616](https://www.python.org/dev/peps/pep-0616/) – `removeprefix` et `removesuffix` dans `str` Souvent, les développeurs voulant supprimer un préfixe ou un suffixe d’une chaine utilisaient [`str.lstrip`](https://docs.python.org/3/library/stdtypes.html#str.lstrip) et [`str.rstrip`](https://docs.python.org/3/library/stdtypes.html#str.rstrip), et se retrouvaient surpris que le paramètre passé soit interprété comme un ensemble de caractère. ```python >>> "test_terrible_name".lstrip("test_") # enlève tous les caractères 't' 'e' 's' '_' en début de chaine 'rrible_name' ``` De fait, cela résultait soit en du code lourd, soit à une implémentation souffrant souvent de bugs subtils autour de la gestion de chaine vide. Il était donc nécessaire que Python réponde à ce besoin courant avec une solution fiable. Désormais, [`str.removeprefix`](https://docs.python.org/3/library/stdtypes.html#str.removeprefix) et [`str.removesuffix`](https://docs.python.org/3/library/stdtypes.html#str.removesuffix) remplissent ce vide et permettent d’écrire du code plus élégant. Par exemple, dans le code de Python lui-même (`find_recursionlimit.py`): ```python if test_func_name.startswith("test_"): print(test_func_name[5:]) else: print(test_func_name) ``` devient ```python print(test_func_name.removeprefix("test_")) ``` ou encore (`cookiejar.py`): ```python def strip_quotes(text): if text.startswith('"'): text = text[1:] if text.endswith('"'): text = text[:-1] return text ``` devient ```python def strip_quotes(text): return text.removeprefix('"').removesuffix('"') ``` # [PEP 617](https://www.python.org/dev/peps/pep-0617/) – Nouveau analyseur [PEG](https://en.wikipedia.org/wiki/Parsing_expression_grammar) pour CPython La grammaire de Python était basée sur une grammaire [LL(1)](https://fr.wikipedia.org/wiki/Analyse_LL#Cas_g%C3%A9n%C3%A9ral_pour_une_analyse_LL(1)). Cependant, certaines fonctionnalités de Python n’étaient pas exprimables selon ce modèle, et nécessitaient des bidouillages qui compliquaient la maintenance. Les deux exemples bloquant Python sont les règles où l’ambiguïté ne peut être résolue qu’en regardant les symboles suivants, ou la récursion par la gauche. [Un des problèmes](https://bugs.python.org/issue12782) qui s’est posé (à partir de 2011 !!) était par exemple que le code suivant : ```python with ( open("a_really_long_foo") as foo, open("a_really_long_baz") as baz, open("a_really_long_bar") as bar ): ``` était considéré comme invalide contrairement au reste des constructions Python (il fallait utiliser `\` pour la continuation de ligne). En passant à une grammaire [PEG [en]](https://en.wikipedia.org/wiki/Parsing_expression_grammar), les règles sont plus proches de la façon dont elles vont être parsées. Lors d’ambiguïtés, chaque possibilité est vérifiée et si elle échoue, teste la variante possible suivante. Les grammaires PEG ne gèrent habituellement pas la récursion par la gauche, mais cela a pu être implémenté sans difficulté. Le nouveau parseur est légèrement plus rapide et utilise un peu moins de mémoire que l’ancien. Il permet de rendre le code plus maintenable, ainsi que de résoudre des problèmes (tel que celui avec le `with` exposé ci-dessus) qui n’avaient pu être résolu pendant des années à cause de la complexité induite. Les deux parseurs continuent de coexister et aucune règle de grammaire ne requiert le nouveau parseur (utilisé par défaut). On peut revenir à l’ancien parseur à tout moment avec [`python -X oldparser`](https://docs.python.org/3/using/cmdline.html#id5) ou en utilisant une variable d’environnement [`PYTHONOLDPARSER=1`](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONOLDPARSER). Si rien ne l’empêche, l’ancien parseur sera retiré dans Python version 3.10. Les modules dépréciés de Python 3 qui étaient maintenus pour que des personnes écrivent plus facilement des bibliothèques/programmes compatibles Python 2 et 3 sont supprimés dans cette version.