Python 3.9 est disponible

Posté par  (site web personnel, Mastodon) . Édité par Benoît Sibaud, palm123, Davy Defaud, ariasuni, Ysabeau 🧶, bobble bubble et Snark. Modéré par claudex. Licence CC By‑SA.
Étiquettes :
49
9
déc.
2020
Python

Python 3.9 est sorti le 5 octobre 2020, après dix‑sept mois de développement.

Voyons ce que cette version apporte comme nouveautés…

Logo Python

Sommaire

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 des sorties

La première chose qu’on peut noter c’est justement la date de sortie. Habituellement, les versions de Python sortent tous les dix‑huit mois. La PEP 602 décide de changer ça et de faire des sorties tous les douze 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 dix‑sept mois :

  • les cinq premiers mois se font sans numéro de version et chevauchent les versions bêta et rc de la version n - 1 ;
  • les sept mois suivants sont consacrés aux versions alpha (ajout de fonctionnalités toujours possible) ;
  • les trois mois suivants sont consacrés aux versions bêta (plus d’ajout de fonctionnalité possible) ;
  • les deux derniers mois sont pour les versions candidates.

Les versions majeures (3.x) sont maintenues pendant cinq ans :

  • corrections et sorties complètes pendant un an et demi, fréquence de sortie mensuelle ;
  • corrections de sécurité uniquement et sorties sous forme de source uniquement pendant trois ans et demi.

PEP 573 — Amélioration de performance pour les extensions C questionnant l’état de leur module

PEP 573 : 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 et développeuses de modules qui le souhaitent d’avoir directement les informations nécessaires (instance, classe, module…).

Ça devrait permettre de corriger les PEP 3 121 et PEP 489 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 et maintenir ;
  • qui peuvent être instanciés plusieurs fois sans souci 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 intégrés — built‑in — 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 — Opérateurs d’union pour les dictionnaires

PEP 584 : « Comment fusionner deux dictionnaires en Python en une instruction ? »

C’est une des questions les plus vieilles et plus lues sur StackOverflow à propos de Python : vieille de plus de douze ans, votée à plus de 5 000, dans les favoris plus de 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 ont fini 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 :

d3 = d1 | d2

Attention toutefois, l’opérateur n’est pas commutatif, c’est‑à‑dire que d1|d2 n’est pas forcément équivalent à d2|d1. Les clés et valeurs de droite écrasant celles de gauche.

PEP 585 — Generics dans les collections pour les typehint sans importation depuis typing

PEP 585 : 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 PEP (484, 526, 544, 560 et 563) et a 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 :

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 :

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.
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 par l’environnement d’exécution 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, à l’exécution, contenir des objets de tout type.

L’importation des types équivalents depuis typing est dépréciée mais ne génère pas d’avertissement.

PEP 593 — Possibilité d’annoter un « typehint » avec une expression quelconque

PEP 593 : avec Annotated du module typing, il est maintenant possible d’annoter une expression quelconque sur un type. Cette annotation peut être lue par un analyseur de type ou par l’environnement d’exécution. Cela permet par exemple à un cadriciel 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 :

@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")]
}

Annotated peut prendre plusieurs annotations (l’ordre est conservé). Toutefois, si une annotation est de type Annotated, la liste résultante sera aplatie :

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 :

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 — Les décorateurs peuvent être des expressions complètes

PEP 614 est une modification mineure, qui ne concernera probablement que ceux créant ou 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 paraît suffisant, mais en fait c’était hyper restrictif.

Prenons l’exemple d’un cadriciel fictif qui permet de brancher des fonctions à des clics sur des boutons d’une interface graphique :

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 :

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 — Intégration des fuseaux horaires de l’IANA dans Python

PEP 615 : un nouveau module intégré a été ajouté en Python 3.9 : zoneinfo.

Le module se base sur les fuseaux horaires définis dans le système, se reposant sur les données de l’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 et de pouvoir jouer avec les dates et leur fuseau horaire 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 de l’IANA.

Le module contient surtout une classe ZoneInfo qui permet :

  • de construire un fuseau horaire basé sur son nom, par exemple Europe/Paris — les fuseaux horaires construits sont mis en cache ;
  • d’être utilisée dans les arguments tzinfo des classes et fonctions du module datetime.
zone = ZoneInfo("Europe/Paris")
dt = datetime(2020, 12, 3, 16, 15, tzinfo=zone)

PEP 616 — removeprefix et removesuffix dans str

PEP 616 : souvent, les développeurs voulant supprimer un préfixe ou un suffixe d’une chaîne de caractères utilisaient str.lstrip et str.rstrip, et se retrouvaient surpris que le paramètre passé soit interprété comme un ensemble de caractères.

>>> "test_terrible_name".lstrip("test_")
# enlève tous les caractères 't', 'e', 's' et '_' en début de chaîne
'rrible_name'

De fait, cela résultait soit en du code lourd soit à une implémentation souffrant souvent de bogues subtils autour de la gestion de chaînes vides. Il était donc nécessaire que Python réponde à ce besoin courant avec une solution fiable.

Désormais, str.removeprefix et 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) :

if test_func_name.startswith("test_"):
    print(test_func_name[5:])
else:
    print(test_func_name)

Ce code devient :

print(test_func_name.removeprefix("test_"))

Ou encore le code suivant (cookiejar.py) :

def strip_quotes(text):
    if text.startswith('"'):
        text = text[1:]
    if text.endswith('"'):
        text = text[:-1]
    return text

qui devient :

def strip_quotes(text):
    return text.removeprefix('"').removesuffix('"')

PEP 617 — Nouvel analyseur PEG pour CPython

PEP 617 : la grammaire de Python était basée sur une grammaire 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 qui s’est posé (à partir de 2011 !) était, par exemple, avec le code suivant :

with (
    open("a_really_long_foo") as foo,
    open("a_really_long_bar") as bar,
    open("a_really_long_baz") as baz
):

Ce dernier é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], les règles sont plus proches de la façon dont elles vont être analysé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 nouvel analyseur 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ésolus pendant des années à cause de la complexité induite.

Les deux analyseurs continuent de coexister et aucune règle de grammaire ne requiert le nouvel analyseur (utilisé par défaut).

On peut revenir à l’ancien analyseur à tout moment avec python -X oldparser ou en utilisant une variable d’environnement PYTHONOLDPARSER=1. Si rien ne l’empêche, l’ancien analyseur sera retiré dans Python 3.10.

Les modules dépréciés de Python 3 qui étaient maintenus pour que des personnes écrivent plus facilement des bibliothèques et des programmes compatibles Python 2 et 3 sont supprimés de cette version.

Aller plus loin

  • # coquille

    Posté par  (site web personnel, Mastodon) . Évalué à 4.

    Merci pour cette dépêche bien écrite.

    Petite coquille :

    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.

    s/est de pouvoir/et de pouvoir/

    • [^] # Re: coquille

      Posté par  . Évalué à 2.

      Et aussi :

      l’opérateur n’est pas commutable

      s/commutable/commutatif/

      Les clés/valeurs de droites écrasants celles de gauche

      s/droites/droite/
      s/écrasants/écrasant/

      La situation actuelle repose sur une succession de PEPs (484, 526, 544, 560, et 563) et ont abouti à l’existence d’une hiérarchie

      s/ont abouti/a abouti/

      • [^] # Re: coquille

        Posté par  (site web personnel) . Évalué à 3.

        corrigé, merci

        ウィズコロナ

        • [^] # Re: coquille

          Posté par  . Évalué à 2.

          les versions bêtas (bêta invariable)
          une des questions les plus vieilles et plus lue(s)
          les développeurs Python [on finit] ont fini

          • [^] # Re: coquille

            Posté par  (site web personnel) . Évalué à 2.

            corrigé, merci

            ウィズコロナ

            • [^] # Re: coquille

              Posté par  . Évalué à 2.

              À toutes fins utiles, je signale que le choix de mettre un lien externe en début de titre de section empêche d'utiliser le sommaire pour aller directement lire la section voulue dans la dépêche.

    • [^] # Re: coquillette

      Posté par  . Évalué à 2.

      Oui, merci beaucoup pour cette dépêche :)

      Une autre coquille(tte) est restée dans l'assiette :

      s/Annoted/Annotated/

  • # Et enfin un vrai switch/case...

    Posté par  . Évalué à 0.

    Pour éviter les bidouilles!
    On pourrait ajouter les variables static (là aussi, on peut bidouiller), facilitant l'utilisation purement procédurale du langage…

    Bin non? Pour le 4.0 peut-être?!

  • # Version intéressante

    Posté par  . Évalué à 4.

    Quelques nouveautés qui vont devenir indispensables :
    PEP 584 et PEP 616 : une paire de code boilerplate et imbuvable qui va être remplacé, j'ai hâte !

    Moins intéressé par tout ce qui concerne le typage mais bon, il en faut pour tout le monde.

  • # snake_case

    Posté par  . Évalué à 3. Dernière modification le 13 décembre 2020 à 15:30.

    Quelqu'un sait s'il y a une raison pour ne pas snakecasiser les fonctions du genre removeprefix (hormis rester cohérent avec l'API de string) ? J'ai toujours eu du mal a écrire startswith. Pourtant il me semble que ça fournirait quelque chose de plus de lisible.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.