Sortie de Python 3.7

Python 3.7 a été publié le 27 juin 2018, soit un an et demi après la précédente version. Celle‐ci vient avec son lot de nouveaux modules et fonctionnalités que nous détaillons dans la suite de la dépêche.

Logo de Python

Sommaire

Python 3.7 en résumé

La fonctionnalité la plus « marquante » est sans doute l’introduction du module dataclasses qui permettra aux développeurs d’écrire des classes avec une syntaxe bien plus concise qu’à l’accoutumée. Une nouvelle fonction native a été introduite à savoir breakpoint() facilitant l’utilisation du débogueur et offrant la possibilité, pour des usages plus avancés, de le personnaliser. Le module asyncio a été fortement amélioré, notamment en simplifiant l’API, en plus de cela, async et await deviennent désormais des mots clefs. Ajoutons à cela, le fait que la conservation de l’ordre d’insertion des dict est désormais officielle d’après les spécifications, et vous aurez une bonne vision des modifications liées à la syntaxe du langage.

Python 3.7 apporte aussi des améliorations sur des fonctionnalités plus avancées, comme la possibilité de personnaliser l’accès aux attributs de module, mais aussi l’introduction du module typing dans le cœur de Python, ainsi que la possibilité d’effectuer une évaluation différée des annotations, ce qui amène à Python des évolutions intéressantes concernant son système de typage.

Enfin, cette version vient avec la possibilité de faire des builds reproductibles, et de très nombreuses améliorations de performance faisant de Python 3.7 la version la plus rapide de Python.

Nouveaux modules

PEP 557 : Dataclasses

La PEP 557 introduit le nouveau module dataclasses qui fournit le décorateur @dataclass permettant d’écrire une « classe de données » de façon plus concise. Les dataclasses peuvent être vues comme un namedtuple mutable avec des valeurs par défaut. Le décorateur de classe repose sur la PEP 526 qui a introduit les annotations de variables. On peut donc désormais écrire ceci en Python 3.7 :

from dataclasses import dataclass

@dataclass
class BankAccount:
    """class that corresponds to a bank account."""
    bank_name: str  # no default value
    owner_name: str
    currency: str = "dollar"  # assign a default value for 'currency'
    value: int = 0

    def tax(self, val) -> int:
        self.value -= val

Cela donne ensuite :

>>> print(BankAccount('BNP', 'toto'))
BankAccount(bank_name='BNP', owner_name='toto', currency='dollar', value=0)
>>> account = BankAccount('LCL', 'tata', 'euros', 1000)
>>> account.tax(5)
>>> print(account.value)
995

L’intérêt d’utiliser une dataclass et non pas un namedtuple, c’est qu’il s’agit ni plus ni moins d’une classe normale en Python. On peut donc utiliser tous les concepts de la programmation objet associés aux classes (héritage, métaclasse, docstrings). On voit dans l’exemple ci-dessus que le décorateur va générer une classe, et va fournir automatiquement le __init__, __repr__, ainsi que la fonction d’égalité __eq__.

>>> print(account)
<class '__main__.BankAccount'>
>>> dir(account1)
['__annotations__', '__class__', '__dataclass_fields__', '__dataclass_params__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bank_name', 'currency', 'owner_name', 'tax', 'value']

Il est possible de passer des paramètres au décorateur pour générer, par exemple, des fonctions de comparaisons avec le paramètre order. Par exemple, la classe suivante permettra de faire des comparaisons.

@dataclass(order=True)
class Item:
    price: float
    product_name: str 

>>> pomme = Item(1.5, 'pomme')
>>> ordinateur = Item(500, 'ordinateur')
>>> pomme < ordinateur
True

Python va comparer les éléments en considérant que les éléments sont des tuples, il va donc comparer les champs un par un.

Il est possible de convertir une instance dataclass en tuple ou dict via les fonctions astuple() ou asdict() disponibles dans le module. Dans l’exemple ci-dessus, cela donne :

from dataclasses import astuple, asdict
>>> print(astuple(pomme))
(1.5, 'pomme')
>>> print(asdict(pomme))
{'price': 1.5, 'product_name': 'pomme'}

Les dataclasses peuvent faire penser à l’utilisation que l’on peut faire des namedtuples, mais ces derniers ont certaines restrictions. De par leur nature, ce sont des objets immuables. Ainsi, si l’on écrit :

from collections import namedtuple
Item = namedtuple('Item', ['price', 'product_name'])
apple = Item(2, 'apple')

Modifier le champ price va provoquer une erreur :

>>> apple.price = 3
AttributeError: can't set attribute

De même, on peut accidentellement comparer deux namedtuples qui ont les mêmes valeurs, mais qui ne représentent pas les mêmes objets.

Stock = namedtuple('Stock', ['quantity', 'name'])
apple_stock = Stock(2, 'apple')
>>> apple_stock == apple
True

L’utilisation de dataclass empêche cela car la comparaison de deux objets de classes différentes renverra toujours False.

PEP 567 : Variables contextualisées

Cette nouvelle fonctionnalité demande une petite introduction.

Si on prend un programme « classique » mono-thread avec une exécution linéaire, toutes les variables sont accessibles simplement en lecture ou écriture et ne sont pas susceptibles de changer. Jusque-là, rien de compliqué.

Maintenant, prenons le cas d’un programme multi-thread. Le contexte d’exécution — et en particulier toutes les variables — est partagé entre toutes les threads. Il est donc difficile pour une thread de manipuler des données privées, réservées à la thread. Python propose une solution pour ce problème avec l’API threading.local qui fournit un objet où la modification et la récupération d’une variable donnée pourra être différente suivant la thread qui l’appelle.

Avec l’arrivée de l’asynchronicité dans l’exécution du code, de nouveaux problèmes se présentent : on peut avoir à l’intérieur de la même thread d’exécution plusieurs fonctions qui s’exécutent de façon asynchrone et qui auraient besoin d’un contexte différent par fonction plutôt que par thread.

C’est ce problème que résout le nouveau module contextvars. Son introduction a été discutée et approuvée via la PEP 567 – Variables contextualisées. Cette PEP est elle-même un sous-ensemble d’une PEP plus ambitieuse, la PEP 550 – Contexte d’exécution. Cette dernière apportait la notion de contexte à un ensemble plus large de construction Python (en particulier les générateurs) mais n’a pas fait l’unanimité parmi les core-développeurs. L’auteur de la PEP, Yury Selivanov, a repris les parties qui faisaient consensus dans la PEP 567 qui a été intégrée dans Python 3.7.

L’exemple suivant utilise asyncio et une liste pour créer trois contextes différents dans init_var(), puis le programme l’appel de la fonction hello() à chaque fois avec une valeur différente de name programmée dans le contexte.

import contextvars
import asyncio
import datetime

all_names = ['Donald', 'Mickey', 'Dominique']

# La variable contextuelle
name = contextvars.ContextVar('name')

def hello():
    date = datetime.datetime.now().strftime('%H:%M:%S')
    print('Bonjour {}, il est {}'.format(name.get(), date))

def init_var(event_loop):
    name.set(all_names.pop())
    event_loop.call_later(1, hello)

# on récupère une boucle d'évènements
event_loop = asyncio.get_event_loop()

# on planifie des appels toutes les secondes
event_loop.call_later(0, init_var, event_loop)
event_loop.call_later(1, init_var, event_loop)
event_loop.call_later(2, init_var, event_loop)

# on prévoit de s'arrêter tout de même
event_loop.call_later(5, event_loop.stop)

# on met tout en route et on attend
event_loop.run_forever()

Le résultat :

>py -3.7 asyncio_example2.py
Bonjour Dominique, il est 13:10:03
Bonjour Mickey, il est 13:10:04
Bonjour Donald, il est 13:10:05

Ajout de l’API importlib.resources pour lire des ressources embarquées

Le terme ressource désigne ici un fichier de données. Avec cette nouvelle API, il est possible d’accéder en lecture à un fichier qui serait embarqué dans un package Python, et donc installé en même temps que le package. Ou bien de donner un accès en lecture à une donnée qui fait partie de l’application distribuée mais n’est pas représentée strictement comme un fichier.

Accéder à des ressources de données est un besoin courant pour certains types d’applications. Par exemple, les applications graphiques peuvent ainsi distribuer aisément leurs icônes et images.

Avant la disponibilité de cette API, embarquer un fichier de données au packaging et y accéder à l’exécution se faisait soit à coups de bidouilles sur la variable __file__ pour localiser le module en cours et en déduire l’emplacement du fichier (la méthode old-school, qui marchait pas toujours dans des environnements complexes), soit en utilisant l’API Basic Resource Access de setuptools, l’outil de packaging de Python.

C’est surtout ce dernier qui est remplacé par la nouvelle API, plus robuste et plus simple à utiliser. Une documentation spécifique est disponible pour migrer de pkg_resources à importlib.resources, en plus de la documentation standard.

À noter la disponibilité d’un portage vers les précédentes versions de Python (2.7, 3.4, 3.5 et 3.6) via le package importlib_resources sur PyPi. C’est d’ailleurs dans ce package que l’API a maturé dans les six derniers mois avant de rejoindre Python 3.7.

Syntaxe et caractéristiques des dictionnaires

async et await deviennent des mots clefs réservés

Pour éviter les problèmes de rétro-compatibilité, async et await n’ont pas été ajoutés à la liste des mots clés réservés lorsqu’ils ont été inclus dans Python 3.5.

Il était donc possible de nommer des variables et des fonctions async ou await.

Dorénavant, ce n’est plus possible en Python 3.7 :

>>> async = 42
  File "<stdin>", line 1
    async = 42
          ^
SyntaxError: invalid syntax
>>> def await():
  File "<stdin>", line 1
    def await():
            ^
SyntaxError: invalid syntax

Ordre d’insertion dans les dictionnaires

La conservation de l’ordre d’insertion dans les dict est désormais officielle. Ce comportement provient au départ d’un effet de bord de l’amélioration de l’implémentation des dictionnaires (consommation mémoire réduite de 20 à 25 %) réalisée pour la version 3.6. Cette propriété a été dans un premier temps définie en tant que détail d’implémentation de CPython 3.6 afin de ne pas trop perturber la compatibilité arrière et les autres implémentations de Python.

Concrètement, en Python 3.5 :

>>> d = {}
>>> d['a'] = 1
>>> d['z'] = 2
>>> for k in d: print(k)  # affiche toutes les cles du dictionnaire
z
a

L’ordre de parcours des clés du dictionnaire est non-défini à l’avance, et peut bouger au fur à mesure que le dictionnaire évolue :

>>> d['j'] = 3
>>> for k in d: print(k)  # affiche toutes les cles du dictionnaire
z
j
a

Le nouvel élément ajouté a changé l’ordre de parcours.

Maintenant, en Python 3.6 et 3.7 :

>>> d={}
>>> d['a'] = 1
>>> d['z'] = 2
>>> for k in d: print(k)
a
z
>>> d['j'] = 3
>>> for k in d: print(k)
a
z
j

Les clés sont maintenant affichées dans l’ordre où elles ont été ajoutées au dictionnaire. Un effet de bord sympa est que pour les fonctions à arguments par mot-clés, l’ordre est préservé aussi :

L’ancien comportement en Python 3.5 :

>>> def f(**kw):
...   for k in kw: print(k)
>>> f(a=1, j=2, z=3)
z
j
a
>>> f(z=3, a=2, j=1)
z
j
a

On constate que l’ordre d’affichage des clés est celui du dictionnaire utilisé, et va donc varier en fonction des clés du dictionnaire.

Maintenant, en Python 3.6 et 3.7 :

>>> f(a=1, j=2, z=3)
a
j
z
>>> f(z=3, j=2, a=1)
z
j
a

L’ordre d’affichage dépend désormais de l’ordre de création des clés.

C’est pas une killer-feature de Python, mais c’est plutôt confortable.

Note personnelle de l’un des auteurs : ça m’est arrivé plusieurs fois lors de phases de debug d’être perturbé parce que les nouvelles entrées dans un dictionnaire étaient affichées au milieu plutôt qu’à la fin. Ce ne sera plus le cas.

À noter que la classe OrderedDict du module collections devient beaucoup moins utile, mais pas nécessairement obsolète : son auteur Raymond Hettinger explique dans un mail que l’implémentation de OrderedDict est optimisée afin de permettre des ré-ordonnancements rapides et fréquents lors de l’évolution du dictionnaire. Ce n’est pas le cas des dict normaux qui eux sont optimisés pour être compacts en mémoire et rapides lors des accès. Une autre différence est que pour être égaux, deux dict normaux ont besoin d’avoir les mêmes clés et valeurs, alors que deux OrderedDict ajoutent la contrainte que les clés ont été insérées dans le même ordre.

Améliorations

PEP 553 : Introduction de la fonction breakpoint()

La PEP 553 introduit la nouvelle fonction native breakpoint() pour faciliter et uniformiser l’accès au débogueur Python.

Auparavant, pour faire appel à pdb, le débogueur standard de Python, vous deviez écrire import pdb; pdb.set_trace(). Désormais, vous pourrez faire breakpoint() uniquement.

Cela a pour premier avantage de réduire le nombre de caractères à taper, et que ce soit plus facile à retenir, mais le véritable avantage est la possibilité de personnaliser le comportement du débuggeur.

En effet, cette nouvelle fonction native fait appel à la fonction sys.breakpointhook() qui elle-même interroge une nouvelle variable d’environnement nommée PYTHONBREAKPOINT. Selon la valeur choisie, on peut désactiver le débogage (pour le désactiver dans un environnement de prod), utiliser le comportement par défaut avec l’appel à pdb, ou appeler un autre débogueur (celui intégré dans l’IDE utilisé, par exemple).

PEP 560 et PEP 563 : Améliorations autour des annotations

Les annotations des paramètres et valeur de retour de fonctions ont été introduites en même temps que le passage à Python 3.0, par la PEP 3107 en 2006. À l’époque, la syntaxe a été choisie de façon « neutre » afin de ne pas forcer un fonctionnement spécifique alors que l’annotation de type était encore quelque chose de très expérimental (voire inexistant) et novateur. Guido avait bien en tête que cela évoluerait vers de l’annotation de type poussée (cf. son blog) mais il était trop tôt pour figer cela dans le marbre et surtout le passage à Python 3 occupait déjà beaucoup de monde.

Par neutre, on entend qu’on pouvait rajouter des trucs (techniquement des expressions) après les arguments et les valeurs de retour des fonctions mais que ces trucs étaient totalement ignorés par l’implémentation, en dehors du fait qu’ils soient accessibles.

Les annotations étaient décrites dans la grammaire en tant que :

    def foo(a: expression, b: expression = 5, *args: expression, **args: expression) -> expression:
        ...

Les deux exemples d’utilisation potentielle donnés dans la PEP étaient :

    def compile(source: "something compilable",
                filename: "where the compilable thing comes from",
                mode: "is this a single statement or a suite?"):
        ...

    def haul(item: Haulable, *vargs: PackAnimal) -> Distance:
        ...

Évidemment, c’était surtout le cas haul() qui motivait l’évolution.

L’aspect « neutre » signifiait qu’en dehors de la syntaxe du langage et d’une fonction pour rapatrier la chaîne de l’annotation, on ne touche pas au fonctionnement interne du langage.

Avançons de huit années, jusqu’en 2014. Durant ces huit ans, des outils se sont emparés des possibilités ouvertes par l’annotation de type et permettent ainsi de vérifier statiquement la cohérence des types d’un ensemble de module Python. Le plus connu alors est mypy développé par l’équipe de Dropbox dans laquelle travaille Guido. Lorsqu’on l’exécute sur un ensemble de code Python, il fournit un rapport sur la cohérence globale des types déclarés dans les fonctions et leur utilisation (de la même façon que les compilateurs C++/Java/…).

En 2014, Guido propose une nouvelle PEP, la PEP 484Indication de types, pour standardiser la façon dont on décrit le type d’un paramètre dans les annotations. Le but est de permettre aux différents outils travaillant avec les annotations de type d’utiliser un vocabulaire commun.

Cette PEP ajoute un module typing à Python 3.5, qui permet de définir des types complexes à utiliser lors de l’annotation de type. Le module est provisionnel, ce qui veut dire qu’il est proposé en l’état mais qu’il pourrait encore évoluer.

Concrètement, on peut maintenant écrire :

    def adder(val: int, l: Sequence[int]) -> List[int]:
        ret = []
        for v in l:
            ret.append( v+val )
        return ret

L’annotation val: int était déjà possible avant. La nouveauté de la PEP 484 concerne tous les types élaborés comme Sequence, List, Dict, Any ainsi que toutes les constructions pour fabriquer des types plus spécifiques ou plus génériques.

Le code ci-dessus nécessite d’être préfixé par la ligne suivante :

    from typing import Sequence, List

En effet, les annotations sont des expressions ordinaires qui respectent la syntaxe Python.

On note aussi la syntaxe inhabituelle, <container class> [ <type> ]. Il fallait une syntaxe lisible (pour rester dans l’esprit du langage) mais distincte de ce qui existe déjà, et qui ne crée pas de confusion pour le parser Python. Il y a eu pas mal de débat pour arriver à ce choix.

Cette syntaxe est possible grâce à l’utilisation d’une méthode __getitem__() dans la méta-classe . Si vous ne savez pas ce qu’est une méta-classe, ne vous en faite pas, c’est très très spécifique. En gros, une méta-classe fonctionne pour une classe de la même façon qu’une classe pour une instance : elle permet de contrôler la création de la classe et de lui adjoindre des méthodes génériques disponibles pour toutes les classes qui y réfèrent. Ça permet notamment d’adjoindre et de partager des comportements génériques par des classes sans passer par le système classique d’héritage. On pourrait écrire trois pages sur le sujet sans le couvrir réellement.

Toujours est-il qu’on reste dans l’esprit d’une modification « neutre » du langage au sens où on utilise deux fonctionnalités déjà présentes, un module et des méta-classes pour proposer l’annotation de type.

La prise en charge des annotations a continuée d’évoluer, avec dans Python 3.6 la PEP 526Une syntaxe pour les annotations de variables, qui permet désormais d’annoter le type d’une variable en plus des arguments d’une fonction.

Concrètement, cela prend la forme de :

    primes: List[int] = []

    captain: str  # Note: no initial value!

    class Starship:
        stats: ClassVar[Dict[str, int]] = {}

C’est particulièrement utile lors de l’initialisation de conteneurs vides, pour lesquels l’analyseur de type ne peut savoir qu’elle sera les types stockés.

C’est une standardisation dans le langage d’une fonctionnalité déjà présente dans mypy qui permettait la même chose mais en s’appuyant sur des commentaires :

    primes = []  # type: List[int]

    class Starship:
        stats = {}  # type: ClassVar[Dict[str, int]]

Pour en revenir aux annotations, la neutralité et la généricité ont commencé à montrer leur limites lorsqu’on en fait un usage intensif. C’est ce qui est arrivé ces dernières années, et qui ont motivé deux PEP implémentées dans Python 3.7 .

L’utilisation poussée du module typing a montré qu’il était plutôt lent, du fait de l’utilisation intensive des méta-classes et notamment de l’héritage multiple de méta-classes. C’est pourquoi Python 3.7 introduit la PEP 560 – Gestion dans le cœur de Python du module typing et des types génériques. Comme le dit son nom, cette PEP intervient dans le cœur de l’interpréteur CPython, sur des aspects techniques et hard-core pour accélérer et simplifier le module typing .

De son côté, la PEP 563 – Évaluation différée des annotations, vise à corriger une erreur de jeunesse sur les annotations : celles-ci sont évaluées lors du chargement des modules Python. Pour un programme qui contient beaucoup d’annotations, il y a donc un coût en temps, au démarrage du programme, alors même que les annotations ne sont pas utilisées. L’autre inconvénient est que l’évaluation directe ne permettait pas les références par avance (forward references), où on définit plus bas dans le code un type qu’on souhaite utiliser dès maintenant pour l’annotation.

La proposition de la PEP est de stocker la chaîne de caractère de l’expression dans le champ __annotation__, plutôt que directement le résultat de l’expression évaluée. Pour les outils de vérification de type, ça ne change rien car il était déjà possible de stocker une chaîne de caractère, pour résoudre justement le problème des références par avance.

Pour d’autres usages des annotations, il faudra forcer un appel eval(ann, globals, locals) pour obtenir l’expression de l’annotation. À noter que l’expression n’est plus évaluée dans son périmètre d’origine (à l’analyse du module) mais dans le périmètre qui appelle eval, ce qui peut avoir des conséquences si l’expression utilisait des variables du périmètre local. Pour cette raison, c’est un changement incompatible avec le passé.

Du fait de cette incompatibilité, beaucoup de précautions sont prises pour l’activation de cette fonctionnalité :

  • pour Python 3.7, ce n’est pas actif par défaut et il faudra un import explicite pour en bénéficier :
    from __future__ import annotations
  • pour Python 3.8, l’utilisation d’annotations incompatibles avec la PEP 563 déclenchera un PendingDeprecationWarning, signifiant en gros « attention, bientôt, l’usage sera obsolète ». On parle ici juste de l’usage d’expression dans les annotations qui dépendent du périmètre local, le reste fonctionne très bien en l’état ;
  • pour Python 3.9, on passera à un DeprecationWarning, signifiant cette fois, « maintenant, cet usage est obsolète » ;
  • et pour Python 4.0, une erreur sera levée à la compilation.

À noter qu’avec ces deux PEP, les annotations de type rentrent dans le cœur du langage Python, même si elles restent totalement optionnelles.

Amélioration du module asyncio

Le module asyncio a été introduit dans Python 3.4 pour gérer les entrées/sorties asynchrones, les boucle d’évènements, les co-routines et les tâches. Dans Python 3.7, asyncio a reçu plusieurs nouvelles fonctionnalités ainsi que des améliorations d’utilisabilité et de performance.

Parmi les nouveautés, on peut citer la nouvelle fonction asyncio.run() qui simplifie l’appel de co-routine depuis un code synchrone :

import asyncio

async def bonjour():
    print('Bonjour les Moules< !')

asyncio.run(bonjour())

Autre nouveauté, asyncio accepte maintenant les variables de contexte. Voir la PEP 567 pour plus de détails.

La nouvelle fonction asyncio.create_task() a été ajoutée comme raccourci de asyncio.get_event_loop().create_task().

La nouvelle fonction asyncio.current_task() retourne la tâche courante et la nouvelle fonction asyncio.all_tasks() retourne l’ensemble des tâches existantes dans une boucle donnée. Les méthodes Task.current_task() et Task.all_tasks() sont devenues obsolètes.

La nouvelle fonction asyncio.get_running_loop() retourne la boucle courante et lance une RuntimeError si il n’y a pas de boucle s’exécutant actuellement. À la différence de asyncio.get_event_loop() qui va créer une nouvelle boucle plutôt que de lancer une exception.

La nouvelle méthode StreamWriter.wait_closed() permet d’attendre jusqu’à la fermeture d’un flux. StreamWriter.is_closing() peut être utilisé pour savoir si le flux est en cours de fermeture.

La nouvelle méthode loop.sock_sendfile() permet l’envoi de fichier en utilisant os.sendfile quand c’est possible.

Les nouvelles méthodes Task.get_loop() et Future.get_loop() retournent leur boucle d’origine. Server.get_loop() permet de faire la même chose pour les objets asyncio.Server.

Il est maintenant possible de contrôler le lancement des instances asyncio.Server. Avant, le serveur se lançait immédiatement après sa création. Le nouvel argument start_serving de loop.create_server() et loop.create_unix_server(), ainsi que Server.start_serving(), et Server.serve_forever() peut être utilisé pour découpler l’instanciation du serveur et le lancement du serveur. La nouvelle méthode Server.is_serving() retourne True si le serveur est lancé.

Les objets retournés par loop.call_later() ont maintenant une méthode when() qui retourne leur heure d’exécution estimée.

Les exceptions lancées lorsqu’une tâche est annulée ne sont plus journalisées.

Plusieurs APIs asyncio sont devenues obsolètes.

PEP 564 : Gestion des nanosecondes pour le module time

Après avoir permis à Python 3.3 d’avoir une horloge monotone, après avoir amélioré les méthodes de mesures des micro-benchmarks, Victor Stinner nous propose cette fois d’améliorer la précision des mesures de temps en Python.

La PEP 564 introduit la gestion des nanosecondes dans le module time. La précision des horloges dans les systèmes d’exploitation modernes peut excéder la précision du nombre flottant de secondes retourné par la fonction time.time(), ainsi que ses différentes variantes.

Pour éviter la perte de précision, cette PEP ajoute six nouvelles fonctions liées à la précision nanosecondes :

  • time.clock_gettime_ns() ;
  • time.clock_settime_ns() ;
  • time.perf_counter_ns() ;
  • time.process_time_ns() ;
  • time.time_ns().

Des mesures montrent que la résolution de time.time_ns() est désormais trois fois plus précise que celle de time.time(). Sur la machine utilisée pour les benchmarks, le plus petit intervalle mesuré s’approche des 300 ns sous Windows et fait moins de 100 ns sous Linux.

Message d’erreur ImportError

L’erreur ImportError va désormais afficher un message d’erreur plus explicite en affichant le nom du module et le chemin du fichier utilisé lorsque l’expression from ... import ... échoue.

Concrètement, avec Python 3.6 :

>py -3.6
>>> from itertools import toto
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'toto'

Avec Python 3.7, c’est mieux :

>py -3.7
>>> from itertools import toto
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'toto' from 'itertools' (unknown location)

PEP 538 : Forçage de la locale C en UTF-8

La PEP 538 propose d’ajuster le comportement de CPython en présence de la locale C vide ou introuvable. Dans ce cas, CPython se comportera comme si l’environnement était le suivant :

LC_CTYPE=C.UTF-8
PYTHONIOENCODING=utf-8:surrogateescape

Un très gros travail a été fait dans le cycle de Python 3 pour attacher un encodage précis à toute chaîne de caractère : dans les noms de fichiers, dans les variables d’environnements, etc. Il reste encore quelques cas subtils, et c’est l’objet de cette PEP rédigée par Nick Coghlan.

Dans les cas où la locale C est complètement vide, CPython se considérait en ASCII 7 bits et pouvait générer facilement des erreurs de conversion. En forçant l’UTF-8 dans ces situations, CPython devient à même de mieux gérer le problème et de mieux interagir avec les autres modules qui s’attendent tous à travailler en encodage 8 bits. Lorsque CPython réalise cet ajustement, il force la variable d’environnement LC_TYPE ce qui a pour effet important que toutes les bibliothèques chargées dynamiquement depuis l’interpréteur et tous les processus lancés depuis l’interpréteur héritent de ce changement. Cela permet notamment de configurer correctement GNU readline dont le comportement dépend de la locale en cours.

À l’intérieur de la PEP, Nick liste des cas concrets où cela peut se présenter aujourd’hui et où cette PEP fait la différence :

  • lors de l’utilisation de technologie de conteneurs : Docker, Kubernetes, OpenShift mais aussi GNOME Flatpak ou Ubuntu Snappy ;
  • lors d’un ssh vers un serveur, lorsque la locale du PC initiant la connexion est exportée en tant que variable d’environnement mais n’existe pas sur le serveur.

Un pas de plus vers l’universalité de l’unicode !

PEP 540 : Forçage de l’UTF-8 à l’exécution

La PEP 540 adresse un problème similaire à la PEP précédente, mais avec des cas d’usages légèrement différents. Deux changements sont proposés :

  1. Permettre le forçage de l’utilisation d’UTF-8 par CPython, en ignorant la locale. Ceci peut être activé soit en passant un paramètre au lancement de l’interpréteur ( -X UTF8 ), soit en fixant la variable d’environnement PYTHONUTF8 à 1 ;
  2. Activer ce forçage lorsque la locale est fixée à C (posix). Ceci rejoint le comportement de la PEP précédente.

Les effets du forçage d’UTF-8 :

  • sys.getfilesystemencoding() retourne systématiquement UTF-8, en ignorant la locale ;
  • locale.getpreferredencoding() retourne systématiquement UTF-8, en ignorant la locale et le paramètre do_setlocale ;
  • sys.stdin, sys.stdout, and sys.stderr utilisent l’encodage UTF-8 avec la gestion d’erreur surrogateescape pour stdin/stdout, backslashreplace pour stderr.

Ces deux PEP permettent d’ajuster le comportement de CPython lorsque la locale est configurée différemment pour un système. La différence principale entre les deux approches est que le forçage d’UTF-8 n’affecte que l’interpréteur en cours, il n’est pas hérité par les autres modules dynamiques du même environnement ou par les sous-processus lancés. Les deux PEP sont donc complémentaires.

PEP 552 : Des .pyc basés sur les hash

La compilation des modules python en bytecode est stockée dans un fichier <nom du module>.pyc dans le répertoire __pycache__. En informatique, lorsqu’on parle de cache, il se pose toujours la question délicate de l’invalidation du cache et Python ne fait pas exception. Python a choisi jusqu’à présent de stocker des tampons de date (timestamp) dans le .pyc. Lors de l’exécution, l’interpréteur regarde la date de modification du module Python, la date stockée dans le .pyc et si cette dernière est plus ancienne, il recompile le module. L’approche a plutôt bien marché pour les 24 dernières années, mais il se trouve des cas où ce n’est pas approprié :

  1. lorsque le tampon de date des fichiers n’est pas complètement fiable ou précis (salut NFS, salut les partages réseaux Windows !) ;
  2. lorsqu’on veut créer des builds reproductibles, c’est-à-dire construire un logiciel de façon à ce que pour des données d’entrées identiques, les données de sorties soient identiques. Faire dépendre le choix de recompiler un module d’une date de modification d’un fichier altère cette reproductibilité.

La solution proposée et implémentée par la PEP 552 est de stocker un hash du fichier source dans le .pyc, en lieu et place du tampon de date. L’algorithme de hash choisi est SipHash, parce qu’il est déjà disponible dans l’interpréteur.

L’interpréteur CPython ne générera spontanément jamais de .pyc basé sur les hashes. Pour obtenir de tels .pyc, il faut déclencher soi-même la compilation avec les modules compileall ou py_compile.

Deux types de .pyc basés sur les hashes ont été définis :

  • les vérifiés, pour lesquels CPython va recalculer le hash du module source et vérifier si un changement a eu lieu ;
  • les non-vérifiés, pour lesquels CPython se contente de charger le module. Cela correspond à des environnements ou un autre programme est chargé de vérifier et générer les .pyc en fonction de l’évolution des sources.

PEP 562 : Extension de l’accès aux attributs de module

Avec la PEP 562, Python 3.7 permet de définir une fonction __getattr__() pour les modules. Cette fonction sera appelée lorsque l’attribut d’un module n’est pas trouvé, selon le même principe que __getattr__() pour les classes. Il est également possible de définir la fonction __dir__() qui sera appelée lorsque l’utilisateur liste le contenu du module (avec dir()).

Un exemple typique d’usage de ces deux fonctionnalités est la possibilité de gérer l’obsolescence de certaines fonctions du module, ou encore de permettre un chargement à la demande (lazy loading).

Exemple d’émission de warning pour des fonctions obsolètes :

# monmodule.py
from warnings import warn
deprecated_names = ["old_function", ...]

def _deprecated_old_function(arg, other):
    ...

def new_function():
    ...

def __getattr__(name):
    if name in deprecated_names:
        warn(f"{name} is deprecated", DeprecationWarning)
        return globals()[f"_deprecated_{name}"]
    raise AttributeError(f"module {__name__} has no attribute {name}")

def __dir__():
    return ["new_function"]+ deprecated_names

# main.py
from monmodule import old_function  # fonctionne, mais émet un warning
print(dir(monmodule))  # affiche ["new_function", "old_function" ]
# avant python 3.7, cela aurait affiché ["_deprecated_old_function", "new_function", ...]

PEP 565 : Meilleure gestion des alertes d’API obsolètes

Avec la PEP 565, Python 3.7 fait des ajustements à la marge sur la gestion des alertes (warnings) lors de l’usage d’API obsolètes. Une gestion appropriée de ce type d’alerte n’est pas facile à trouver et les développeurs de Python cherchent le bon niveau de compromis.

Python dispose de trois types principaux d’alertes à lever pour les API qui sont sur le chemin de l’obsolescence :

  • PendingDeprecationWarning : utilisé pour les API qui deviendront prochainement obsolètes ;
  • DeprecationWarning : utilisé pour les API qui sont obsolètes et disparaitront dans le futur ;
  • FutureWarning : utilisé pour les constructions syntaxiques dont la construction va changer dans le futur.

Ces alertes sont levées sous forme de messages sur la sortie d’erreur mais n’affectent pas le programme outre-mesure. L’étape après DeprecationWarning est de lever une exception, qui cette fois arrête le programme en cours d’exécution.

Jusqu’à Python 2.6 et 3.1 , le comportement par défaut était de cacher les PendingDeprecationWarning, et d’afficher les autres. Un mode développeur pouvait être activé avec -Wdefault, dans lequel Python affichait aussi les PendingDeprecationWarning, ce mode étant notamment activé par le module unittest.

L’idée était que les développeurs utilisent le mode -Wdefault, et que les utilisateurs d’un programme ou d’une bibliothèque en Python voient uniquement les FutureWarning et DeprecationWarning.

À l’usage, les développeurs de Python ont constaté qu’un nombre croissant d’utilisateurs de bibliothèques ou programmes écrits en Python étaient confrontés à des alertes DeprecationWarning, pour lesquelles ils ne pouvaient rien : elles étaient levées par des dépendances de leur code, sur lesquelles ils n’avaient pas de contrôle. Cela correspondait en général à une situation où une bibliothèque avait été validée et livrée avec une version de Python plus ancienne, dans laquelle cette alerte n’existait pas encore.

La décision a donc été prise pour Python 2.7 et 3.2 d’ignorer les alertes DeprecatonWarning par défaut, tout comme les PendingDeprecationWarning. Il faut activer le mode -Wdefault pour revoir ces deux types d’alertes.

Cependant, ce changement a rendu ces alertes trop discrètes et des développeurs ont été surpris de la disparition de certaines API dans de nouvelles versions de Python alors que leur disparition avait été préparée.

La PEP 565 propose un compromis plus subtil pour gérer la situation : afficher les DeprecationWarning lors de l’exécution du module principal. Le module principal s’entend comme le module __main__ pour un script d’un seul fichier ou le code entré dans l’interpréteur interactif.

D’autres changements plus légers sont inclus dans cette PEP :

  • la signification de FutureWarning est étendue aux alertes destinées aux utilisateurs d’une application, pour les prévenir d’une utilisation en passe de devenir obsolète (on peut imaginer par exemple un réglage particulier d’une application qui n’a plus de sens) ;
  • la documentation sur les alertes devient beaucoup plus détaillée, avec la liste complète des options en ligne de commande pour les contrôler, le détail du filtre par défaut et pas mal d’exemples pour montrer comment tester la génération des warnings ;
  • il y a maintenant une recommandation officielle à destination des développeurs d’outils de tests, d’activer les alertes développeur lors de l’exécution d’une suite de tests ;
  • de même, les développeurs de ligne de commande interactive (shell), tel que par exemple IPython, sont priés de configurer les alertes afin d’afficher les DeprecationWarning pour les commandes entrées interactivement.

Le nouveau mode d’exécution « dev »

La ligne de commande a maintenant une option en plus, -X dev qui active le mode développement de CPython (également activable via la variable d’environnement PYTHONDEVMODE). Dans ce mode, CPython réalise des vérifications supplémentaires à l’exécution, qui sont trop consommatrices pour être activées par défaut :

  • affichage de tous les types d’alertes (y compris le DeprecationWarning cité plus haut) ;
  • activation des traceurs mémoires pour les allocations, cf PyMem_SetupDebugHooks() ;
  • activation du module faulthandler qui permet d’afficher la pile d’appel lors d’un crash ;
  • activation du mode debug de asyncio.

Amélioration de l’API C :

PEP 539 : Introduction d’une nouvelle API C pour le stockage interne à un thread

La PEP 539 propose une nouvelle API pour le stockage de données privées pour un thread (Thread Local Storage ou TLS). Il existait déjà une telle API dans CPython, mais celle-ci souffrait du problème suivant : la clé utilisée pour chercher des valeurs spécifiques à un thread était un int. C’est contraire à la norme POSIX pthreads, qui exige un type opaque pthread_key_t. Cela n’est pas un problème pour les plate-formes où le type pthread_key_t peut se convertir facilement en int, comme Linux (où c’est un unsigned int) ou Windows (où c’est un DWORD). Cependant, d’autres plate-formes existent, qui sont compatibles POSIX, et pour lesquelles l’API existante ne peut pas fonctionner : Cygwin et CloudAPI par exemple.

L’API actuelle de stockage interne à un thread est donc marquée en tant qu’obsolète et une nouvelle API compatible POSIX la remplace.

Communauté

PEP 545 : Officialisation des traductions de la documentation

La PEP 545 décrit le processus de traduction de la documentation Python. Trois traductions sont désormais hébergées sur python.org :

La PEP permet de mettre en valeur les efforts fait par différentes communautés pour traduire la documentation, et permettra d’uniformiser le travail fait sur d’autres langues : espagnol, chinois, etc.

On en profite pour faire appel à des contributeurs : la traduction française est aujourd’hui à 29 % et pourrait recevoir plus de contributeurs. Ça se passe sur GitHub.

Liens complémentaires

Toutes les nouvelles PEP introduites :

  • PEP 538 : Force l’utilisation du locale C historique à un locale UTF-8.
  • PEP 539 : Nouvelle API C pour ajouter une mémoire locale de thread, ou Thread Local Storage (TLS).
  • PEP 540 : Ajout d’un nouveau mode UTF-8
  • PEP 545 : Officialisation du processus de traduction de la documentation
  • PEP 552 : Reproductibilité des fichiers .pyc
  • PEP 557 : Introduction des dataclasses
  • PEP 560 : Gestion dans le cœur de Python du module typing et des types génériques
  • PEP 562 : Extension de l’accès aux attributs de module
  • PEP 563 : Évaluation différée des annotations
  • PEP 564 : Gestion des nanosecondes pour le module time
  • PEP 565 : Meilleure gestion des alertes d’API obsolètes
  • PEP 567 : Variables contextualisées

Commentaires :
voir le flux atom
ouvrir dans le navigateur

(Source: LinuxFr.org : les dépêches)
Logo