Nouvelles fonctionnalités intéressantes dans Python 3.7

Nouvelles fonctionnalités intéressantes dans Python 3.7

Python 3.7 est officiellement publié! Cette nouvelle version de Python est en développement depuis September 2016, et maintenant nous pouvons tous profiter des résultats du travail acharné des développeurs principaux.

Qu’apporte la nouvelle version Python? Bien que la documentation donne un bon aperçu des nouvelles fonctionnalités, cet article va plonger profondément dans certaines des plus grandes nouvelles. Ceux-ci inclus:

  • Accès plus facile aux débogueurs grâce à un nouveau + breakpoint () + intégré

  • Création de classe simple à l’aide de classes de données

  • Accès personnalisé aux attributs du module

  • Prise en charge améliorée des indications de type

  • Fonctions de synchronisation plus précises

Plus important encore, Python 3.7 est rapide.

Dans les dernières sections de cet article, vous en apprendrez davantage sur cette vitesse, ainsi que sur certaines des autres fonctionnalités intéressantes de Python 3.7. Vous obtiendrez également des conseils sur la mise à niveau vers la nouvelle version.

Le + breakpoint () + Built-In

Bien que nous puissions nous efforcer d’écrire du code parfait, la simple vérité est que nous ne le faisons jamais. Le débogage est une partie importante de la programmation. Python 3.7 introduit la nouvelle fonction intégrée + breakpoint () +. Cela n’ajoute pas vraiment de nouvelles fonctionnalités à Python, mais rend l’utilisation des débogueurs plus flexible et intuitive.

Supposons que vous ayez le code buggy suivant dans le fichier + bugs.py +:

def divide(e, f):
    return f/e

a, b = 0, 1
print(divide(a, b))

L’exécution du code provoque une + ZeroDivisionError + à l’intérieur de la fonction + divide () +. Supposons que vous souhaitiez interrompre votre code et passer dans un debugger en haut de + divide () +. Vous pouvez le faire en définissant un soi-disant «point d’arrêt» dans votre code:

def divide(e, f):
    # Insert breakpoint here
    return f/e

Un point d’arrêt est un signal dans votre code que l’exécution doit s’arrêter temporairement, afin que vous puissiez regarder l’état actuel du programme. Comment placez-vous le point d’arrêt? En Python 3.6 et inférieur, vous utilisez cette ligne quelque peu cryptique:

def divide(e, f):
    import pdb; pdb.set_trace()
    return f/e

Ici, https://docs.python.org/library/pdb.html [+ pdb +] est le débogueur Python de la bibliothèque standard. Dans Python 3.7, vous pouvez utiliser à la place le nouvel appel de fonction + breakpoint () + comme raccourci:

def divide(e, f):
    breakpoint()
    return f/e

En arrière-plan, + breakpoint () + importe d’abord + pdb + puis appelle + pdb.set_trace () + pour vous. Les avantages évidents sont que + breakpoint () + est plus facile à retenir et que vous n’avez besoin que de taper 12 caractères au lieu de 27. Cependant, le vrai bonus de l’utilisation de + breakpoint () + est sa personnalisation.

Exécutez votre script + bugs.py + avec + breakpoint () +:

$ python3.7 bugs.py
>/home/gahjelle/bugs.py(3)divide()
-> return f/e
(Pdb)

Le script s’arrêtera lorsqu’il atteindra + breakpoint () + et vous déposera dans une session de débogage PDB. Vous pouvez taper + c + et appuyer sur [.keys] # Entrez # pour continuer le script. Reportez-vous au Nathan Jennings ’PDB guide si vous souhaitez en savoir plus sur la PDB et le débogage.

Maintenant, disons que vous pensez avoir corrigé le bogue. Vous souhaitez réexécuter le script mais sans vous arrêter dans le débogueur. Vous pouvez, bien sûr, commenter la ligne + breakpoint () +, mais une autre option consiste à utiliser la variable d’environnement + + PYTHONBREAKPOINT +. Cette variable contrôle le comportement de + breakpoint () +, et la définition de + PYTHONBREAKPOINT = 0 + signifie que tout appel à + ​​breakpoint () + est ignoré:

$ PYTHONBREAKPOINT=0 python3.7 bugs.py
ZeroDivisionError: division by zero

Oups, il semble que vous n’ayez pas corrigé le bogue après tout…

Une autre option consiste à utiliser + PYTHONBREAKPOINT + pour spécifier un débogueur autre que PDB. Par exemple, pour utiliser PuDB (un débogueur visuel dans la console), vous pouvez faire:

$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py

Pour que cela fonctionne, vous devez avoir installé + pudb + (+ pip install pudb +). Python se chargera toutefois d’importer + pudb + pour vous. De cette façon, vous pouvez également définir votre débogueur par défaut. Définissez simplement la variable d’environnement + PYTHONBREAKPOINT + sur votre débogueur préféré. Voir this guide pour des instructions sur la façon de définir une variable d’environnement sur votre système.

La nouvelle fonction + breakpoint () + ne fonctionne pas seulement avec les débogueurs. Une option pratique pourrait être de simplement démarrer un shell interactif dans votre code. Par exemple, pour démarrer une session IPython, vous pouvez utiliser ce qui suit:

$ PYTHONBREAKPOINT=IPython.embed python3.7 bugs.py
IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: print(e/f)
0.0

Vous pouvez également créer votre propre fonction et appeler + breakpoint () +. Le code suivant imprime toutes les variables de la portée locale. Ajoutez-le à un fichier appelé + bp_utils.py +:

from pprint import pprint
import sys

def print_locals():
    caller = sys._getframe(1)  # Caller is 1 frame up.
    pprint(caller.f_locals)

Pour utiliser cette fonction, définissez + PYTHONBREAKPOINT + comme précédemment, avec la notation + +module>. <function> +:

$ PYTHONBREAKPOINT=bp_utils.print_locals python3.7 bugs.py
{'e': 0, 'f': 1}
ZeroDivisionError: division by zero

Normalement, + breakpoint () + sera utilisé pour appeler des fonctions et des méthodes qui n’ont pas besoin d’arguments. Cependant, il est également possible de passer des arguments. Remplacez la ligne + breakpoint () + dans + bugs.py + par:

breakpoint(e, f, end="<-END\n")
*Remarque:* Le débogueur PDB par défaut lèvera un `+ TypeError +` à cette ligne car `+ pdb.set_trace () +` ne prend aucun argument positionnel.

Exécutez ce code avec + breakpoint () + se faisant passer pour la fonction + print () + pour voir un exemple simple des arguments passés:

$ PYTHONBREAKPOINT=print python3.7 bugs.py
0 1<-END
ZeroDivisionError: division by zero

Voir PEP 553 ainsi que la documentation de https://docs.python.org/3.7/library/functions.html#breakpoint [` + breakpoint () + ] et https://docs.python.org/3.7/library/sys.html#sys.breakpointhook [ + sys.breakpointhook () + `] pour plus d’informations.

Classes de données

Le nouveau module + dataclasses + rend plus pratique l’écriture de vos propres classes, comme des méthodes spéciales comme + . init () +, + . repr () + `et + . eq () + sont ajoutés automatiquement. En utilisant le décorateur `+ @ dataclass +, vous pouvez écrire quelque chose comme:

from dataclasses import dataclass, field

@dataclass(order=True)
class Country:
    name: str
    population: int
    area: float = field(repr=False, compare=False)
    coastline: float = 0

    def beach_per_person(self):
        """Meters of coastline per person"""
        return (self.coastline * 1000)/self.population

Ces neuf lignes de code représentent un peu de code standard et de bonnes pratiques. Réfléchissez à ce qu’il faudrait pour implémenter + Country + en classe régulière: la méthode + . init () +, une + repr +, six méthodes de comparaison différentes ainsi que la méthode + .beach_per_person () + ' méthode. Vous pouvez développer la boîte ci-dessous pour voir une implémentation de `+ Pays + qui est à peu près équivalente à la classe de données:

Après la création, une classe de données est une classe normale. Vous pouvez, par exemple, hériter d’une classe de données de manière normale. Le but principal des classes de données est de faciliter l’écriture rapide et facile de classes robustes, en particulier de petites classes qui stockent principalement des données.

Vous pouvez utiliser la classe de données + Country + comme n’importe quelle autre classe:

>>>

>>> norway = Country("Norway", 5320045, 323802, 58133)
>>> norway
Country(name='Norway', population=5320045, coastline=58133)

>>> norway.area
323802

>>> usa = Country("United States", 326625791, 9833517, 19924)
>>> nepal = Country("Nepal", 29384297, 147181)
>>> nepal
Country(name='Nepal', population=29384297, coastline=0)

>>> usa.beach_per_person()
0.06099946957342386

>>> norway.beach_per_person()
10.927163210085629

Notez que tous les champs + .name +, + .population +, + .area + et + .coastline + sont utilisés lors de l’initialisation de la classe (bien que + .coastline + soit facultatif, comme le montre le exemple du Népal enclavé). La classe + Country + a un https://dbader.org/blog/python-repr-vs-str [+ repr +] raisonnable, tandis que la définition des méthodes fonctionne de la même manière que pour les classes ordinaires.

Par défaut, l’égalité des classes de données peut être comparée. Puisque nous avons spécifié + order = True + dans le décorateur + @ dataclass +, la classe + Country + peut également être triée:

>>>

>>> norway == norway
True

>>> nepal == usa
False

>>> sorted((norway, usa, nepal))
[Country(name='Nepal', population=29384297, coastline=0),
 Country(name='Norway', population=5320045, coastline=58133),
 Country(name='United States', population=326625791, coastline=19924)]

Le tri s’effectue sur les valeurs des champs, d’abord + .name + puis + .population +, et ainsi de suite. Cependant, si vous utilisez + field () +, vous pouvez customize quels champs seront utilisés dans la comparaison. Dans l’exemple, le champ + .area + a été omis du `+ repr + 'et des comparaisons.

*Remarque:* Les données par pays proviennent du https://www.cia.gov/library/publications/the-world-factbook/[CIA World Factbook] avec des chiffres de population estimés pour juillet 2017.

Avant de réserver vos prochaines vacances à la plage en Norvège, voici ce que dit le Factbook sur le Climat norvégien: «Tempéré le long de la côte, modifié par le courant de l’Atlantique Nord; intérieur plus froid avec augmentation des précipitations et étés plus froids; pluvieux toute l’année sur la côte ouest.

Les classes de données font certaines des mêmes choses que https://dbader.org/blog/writing-clean-python-with-namedtuples [+ namedtuple +]. Pourtant, ils puisent leur plus grande inspiration dans le + projet attrs +. Consultez notre full guide to data classes pour plus d’exemples et d’informations supplémentaires, ainsi que PEP 557 pour la description officielle.

Personnalisation des attributs de module

Les attributs sont partout en Python! Bien que les attributs de classe soient probablement les plus connus, les attributs peuvent en fait être placés sur n’importe quoi, y compris les fonctions et les modules. Plusieurs des fonctionnalités de base de Python sont implémentées en tant qu’attributs: la plupart des fonctionnalités d’introspection, des chaînes de doc et des espaces de noms. Les fonctions à l’intérieur d’un module sont disponibles en tant qu’attributs de module.

Les attributs sont le plus souvent récupérés en utilisant la notation par points: + thing.attribute +. Cependant, vous pouvez également obtenir des attributs nommés lors de l’exécution en utilisant + getattr () +:

import random

random_attr = random.choice(("gammavariate", "lognormvariate", "normalvariate"))
random_func = getattr(random, random_attr)

print(f"A {random_attr} random value: {random_func(1, 1)}")

L’exécution de ce code produira quelque chose comme:

A gammavariate random value: 2.8017715125270618

Pour les classes, appeler + thing.attr + recherchera d’abord + attr + défini sur + thing +. S’il n’est pas trouvé, alors la méthode spéciale + thing . getattr (" attr ") + est appelée. (Il s’agit d’une simplification. Voir this article pour plus de détails.) La méthode + . getattr () + peut être utilisée pour personnaliser l’accès aux attributs sur les objets.

Jusqu’à Python 3.7, la même personnalisation n’était pas facilement disponible pour les attributs de module. Cependant, PEP 562 introduit + getattr () + sur les modules, ainsi qu’une fonction correspondante + dir () +. La fonction spéciale + dir () + permet de personnaliser le résultat de l’appel à https://realpython.com/python-modules-packages/#the-dir-function [+ dir () + sur un module].

Le PEP lui-même donne quelques exemples de la façon dont ces fonctions peuvent être utilisées, notamment l’ajout d’avertissements de dépréciation aux fonctions et le chargement paresseux de sous-modules lourds. Ci-dessous, nous allons construire un système de plugin simple qui permet d’ajouter des fonctions à un module de manière dynamique. Cet exemple tire parti des packages Python. Voir this article si vous avez besoin d’une mise à jour sur les packages.

Créez un nouveau répertoire, + plugins +, et ajoutez le code suivant à un fichier, + plugins/ init . Py +:

from importlib import import_module
from importlib import resources

PLUGINS = dict()

def register_plugin(func):
    """Decorator to register plug-ins"""
    name = func.__name__
    PLUGINS[name] = func
    return func

def __getattr__(name):
    """Return a named plugin"""
    try:
        return PLUGINS[name]
    except KeyError:
        _import_plugins()
        if name in PLUGINS:
            return PLUGINS[name]
        else:
            raise AttributeError(
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

def __dir__():
    """List available plug-ins"""
    _import_plugins()
    return list(PLUGINS.keys())

def _import_plugins():
    """Import all resources to register plug-ins"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

Avant de regarder ce que fait ce code, ajoutez deux autres fichiers dans le répertoire + plugins +. Voyons d’abord + plugins/plugin_1.py +:

from . import register_plugin

@register_plugin
def hello_1():
    print("Hello from Plugin 1")

Ensuite, ajoutez un code similaire dans le fichier + plugins/plugin_2.py +:

from . import register_plugin

@register_plugin
def hello_2():
    print("Hello from Plugin 2")

@register_plugin
def goodbye():
    print("Plugin 2 says goodbye")

Ces plugins peuvent désormais être utilisés comme suit:

>>>

>>> import plugins
>>> plugins.hello_1()
Hello from Plugin 1

>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.goodbye()
Plugin 2 says goodbye

Cela peut ne pas sembler révolutionnaire (et ce n’est probablement pas le cas), mais regardons ce qui s’est réellement passé ici. Normalement, pour pouvoir appeler + plugins.hello_1 () +, la fonction + hello_1 () + doit être définie dans un module + plugins + ou explicitement importée dans + init . Py + dans un `+ plugins + `package. Ici, ce n’est ni l’un ni l’autre!

Au lieu de cela, + hello_1 () + est défini dans un fichier arbitraire à l’intérieur du package + plugins +, et + hello_1 () + devient une partie du package + plugins + en s’enregistrant lui-même à l’aide du `+ @ register_plugin + `https://realpython.com/primer-on-python-decorators/[decorator].

La différence est subtile. Au lieu que le package dicte les fonctions disponibles, les fonctions individuelles s’enregistrent comme faisant partie du package. Cela vous donne une structure simple où vous pouvez ajouter des fonctions indépendamment du reste du code sans avoir à conserver une liste centralisée des fonctions disponibles.

Faisons un rapide examen de ce que + getattr () + fait dans le code + plugins/ init . Py +. Lorsque vous avez demandé + plugins.hello_1 () +, Python recherche d’abord une fonction + hello_1 () + dans le fichier + plugins/ init . Py +. Comme une telle fonction n’existe pas, Python appelle à la place + getattr (" hello_1 ") +. Rappelez-vous le code source de la fonction + getattr () +:

def __getattr__(name):
    """Return a named plugin"""
    try:
        return PLUGINS[name]        # 1) Try to return plugin
    except KeyError:
        _import_plugins()           # 2) Import all plugins
        if name in PLUGINS:
            return PLUGINS[name]    # 3) Try to return plugin again
        else:
            raise AttributeError(   # 4) Raise error
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

+ getattr () + contient les étapes suivantes. Les numéros de la liste suivante correspondent aux commentaires numérotés du code:

  1. Tout d’abord, la fonction essaie avec optimisme de renvoyer le plugin nommé à partir du dictionnaire + PLUGINS +. Cela réussira si un plugin nommé + nom + existe et a déjà été importé.

  2. Si le plugin nommé ne se trouve pas dans le dictionnaire + PLUGINS +, nous nous assurons que tous les plugins sont importés.

  3. Renvoyez le plugin nommé s’il est devenu disponible après l’importation.

  4. Si le plugin n’est pas dans le dictionnaire + PLUGINS + après avoir importé tous les plugins, nous lançons un + AttributeError + disant que + nom + n’est pas un attribut (plugin) sur le module actuel.

Comment le dictionnaire + PLUGINS + est-il rempli? La fonction + _import_plugins () + importe tous les fichiers Python à l’intérieur du package + plugins +, mais ne semble pas toucher + PLUGINS +:

def _import_plugins():
    """Import all resources to register plug-ins"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

N’oubliez pas que chaque fonction de plugin est décorée par le décorateur + @ register_plugin +. Ce décorateur est appelé lorsque les plugins sont importés et est celui qui remplit réellement le dictionnaire + PLUGINS +. Vous pouvez le voir si vous importez manuellement l’un des fichiers de plug-in:

>>>

>>> import plugins
>>> plugins.PLUGINS
{}

>>> import plugins.plugin_1
>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>}

Poursuivant l’exemple, notez que l’appel de + dir () + sur le module importe également les plugins restants:

>>>

>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>,
 'hello_2': <function hello_2 at 0x7f29d4341620>,
 'goodbye': <function goodbye at 0x7f29d43416a8>}

+ dir () + répertorie généralement tous les attributs disponibles sur un objet. Normalement, l’utilisation de + dir () + sur un module produit quelque chose comme ceci:

>>>

>>> import plugins
>>> dir(plugins)
['PLUGINS', '__builtins__', '__cached__', '__doc__',
 '__file__', '__getattr__', '__loader__', '__name__',
 '__package__', '__path__', '__spec__', '_import_plugins',
 'import_module', 'register_plugin', 'resources']

Bien que ces informations puissent être utiles, nous souhaitons davantage exposer les plugins disponibles. Dans Python 3.7, vous pouvez personnaliser le résultat de l’appel de + dir () + sur un module en ajoutant une fonction spéciale + dir () +. Pour + plugins/ init . Py +, cette fonction s’assure d’abord que tous les plugins ont été importés puis liste leurs noms:

def __dir__():
    """List available plug-ins"""
    _import_plugins()
    return list(PLUGINS.keys())

Avant de quitter cet exemple, veuillez noter que nous avons également utilisé une autre nouvelle fonctionnalité intéressante de Python 3.7. Pour importer tous les modules dans le répertoire + plugins +, nous avons utilisé le nouveau module https://docs.python.org/3.7/library/importlib.html#module-importlib.resources [+ importlib.resources +]. Ce module donne accès aux fichiers et aux ressources à l’intérieur des modules et des packages sans avoir besoin de hacks + file + (qui ne fonctionnent pas toujours) ou + pkg_resources + (ce qui est lent). Les autres fonctionnalités de + importlib.resources + seront liées: # autres-fonctionnalités-plutôt-cool [mises en évidence plus loin].

Améliorations de frappe

Type hints and annotations ont été en constante évolution tout au long de la série de versions Python 3. Le système de frappe de Python est maintenant assez stable. Néanmoins, Python 3.7 apporte quelques améliorations à la table: meilleures performances, prise en charge principale et références directes.

Python n’effectue aucune vérification de type lors de l’exécution (sauf si vous utilisez explicitement des packages comme + enforce +). Par conséquent, l’ajout d’indices de type à votre code ne devrait pas affecter ses performances.

Malheureusement, ce n’est pas tout à fait vrai car la plupart des indices de type nécessitent le module + typing +. Le module + typing + est l’un des slowest modules de la bibliothèque standard. PEP 560 ajoute une prise en charge de base pour la saisie dans Python 3.7, ce qui accélère considérablement le module + typing +. Les détails de cela ne sont généralement pas nécessaires à connaître. Penchez-vous simplement en arrière et profitez des performances accrues.

Bien que le système de type de Python soit raisonnablement expressif, un problème qui pose problème concerne les références directes. Les indications de type - ou plus généralement les annotations - sont évaluées lors de l’importation du module. Par conséquent, tous les noms doivent déjà être définis avant d’être utilisés. Ce qui suit n’est pas possible:

class Tree:
    def __init__(self, left: Tree, right: Tree) -> None:
        self.left = left
        self.right = right

L’exécution du code déclenche une + NameError + car la classe + Tree + n’est pas encore (complètement) définie dans la définition de la méthode + . init () +:

Traceback (most recent call last):
  File "tree.py", line 1, in <module>
    class Tree:
  File "tree.py", line 2, in Tree
    def __init__(self, left: Tree, right: Tree) -> None:
NameError: name 'Tree' is not defined

Pour surmonter cela, vous auriez dû écrire à la place " Tree " en tant que chaîne littérale:

class Tree:
    def __init__(self, left: "Tree", right: "Tree") -> None:
        self.left = left
        self.right = right

Voir PEP 484 pour la discussion originale.

Dans un futur Python 4.0, de telles références dites avancées seront autorisées. Cela sera géré en n’évaluant pas les annotations jusqu’à ce que cela soit explicitement demandé. PEP 563 décrit les détails de cette proposition. Dans Python 3.7, les références directes sont déjà disponibles en tant que https://docs.python.org/library/future.html [+ future + import]. Vous pouvez maintenant écrire ce qui suit:

from __future__ import annotations

class Tree:
    def __init__(self, left: Tree, right: Tree) -> None:
        self.left = left
        self.right = right

Notez qu’en plus d’éviter la syntaxe quelque peu maladroite " Tree ", l’évaluation différée des annotations accélérera également votre code, car les indications de type ne sont pas exécutées. Les références directes sont déjà prises en charge par + mypy +.

De loin, l’utilisation la plus courante des annotations est l’indication de type. Néanmoins, vous avez un accès complet aux annotations lors de l’exécution et pouvez les utiliser comme bon vous semble. Si vous gérez directement des annotations, vous devez traiter explicitement les références de transfert possibles.

Créons des exemples certes stupides qui montrent quand les annotations sont évaluées. Tout d’abord, nous le faisons à l’ancienne, les annotations sont donc évaluées au moment de l’importation. Soit + anno.py + contient le code suivant:

def greet(name: print("Now!")):
    print(f"Hello {name}")

Notez que l’annotation de + nom + est + print () +. C’est seulement pour voir exactement quand l’annotation est évaluée. Importez le nouveau module:

>>>

>>> import anno
Now!

>>> anno.greet.__annotations__
{'name': None}

>>> anno.greet("Alice")
Hello Alice

Comme vous pouvez le voir, l’annotation a été évaluée au moment de l’importation. Notez que + nom + finit annoté avec + Aucun + car c’est la valeur de retour de + print () +.

Ajoutez l’import + future + pour activer l’évaluation différée des annotations:

from __future__ import annotations

def greet(name: print("Now!")):
    print(f"Hello {name}")

L’importation de ce code mis à jour n’évaluera pas l’annotation:

>>>

>>> import anno

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

>>> anno.greet("Marty")
Hello Marty

Notez que + Now! + N’est jamais imprimé et l’annotation est conservée comme un littéral de chaîne dans le dictionnaire + annotations +. Afin d’évaluer l’annotation, utilisez + typing.get_type_hints () + ou + eval () +:

>>>

>>> import typing
>>> typing.get_type_hints(anno.greet)
Now!
{'name': <class 'NoneType'>}

>>> eval(anno.greet.__annotations__["name"])
Now!

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

Notez que le dictionnaire + annotations + n’est jamais mis à jour, vous devez donc évaluer l’annotation chaque fois que vous l’utilisez.

Précision de synchronisation

Dans Python 3.7, le module https://docs.python.org/library/time.html [+ time +] gagne de nouvelles fonctions comme décrit dans https://www.python.org/dev/peps/pep- 0564/[PEP 564]. En particulier, les six fonctions suivantes sont ajoutées:

  • + clock_gettime_ns () +: Renvoie l’heure d’une horloge spécifiée

  • + clock_settime_ns () +: Définit l’heure d’une horloge spécifiée

  • + monotonic_ns () +: Renvoie l’heure d’une horloge relative qui ne peut pas reculer (par exemple en raison de l’heure d’été)

  • + perf_counter_ns () + : Renvoie la valeur d’un compteur de performances - une horloge spécialement conçue pour mesurer de courts intervalles

  • + process_time_ns () +: Renvoie la somme du temps CPU du système et de l’utilisateur du processus en cours (hors temps de veille)

  • + time_ns () + : Renvoie le nombre de nanosecondes depuis le 1er janvier 1970

Dans un sens, aucune nouvelle fonctionnalité n’a été ajoutée. Chaque fonction est similaire à une fonction déjà existante sans le suffixe + _ns +. La différence étant que les nouvelles fonctions renvoient un nombre de nanosecondes en tant que + int + au lieu d’un nombre de secondes en tant que + float +.

Pour la plupart des applications, la différence entre ces nouvelles fonctions nanosecondes et leur ancienne contrepartie ne sera pas appréciable. Cependant, les nouvelles fonctions sont plus faciles à raisonner car elles reposent sur + int + au lieu de + float +. Les nombres à virgule flottante sont https://docs.python.org/tutorial/floatingpoint.html par nature imprécis]:

>>>

>>> 0.1 + 0.1 + 0.1
0.30000000000000004

>>> 0.1 + 0.1 + 0.1 == 0.3
False

Ce n’est pas un problème avec Python mais plutôt une conséquence de la nécessité pour les ordinateurs de représenter des nombres décimaux infinis en utilisant un nombre fini de bits.

Un Python + float + suit la IEEE 754 standard et utilise 53 bits significatifs. Le résultat est que tout temps supérieur à environ 104 jours (2⁵³ ou environ 9 quadrillions de nanosecondes) ne peut pas être exprimé comme un flotteur avec une précision en nanosecondes. En revanche, un https://stackoverflow.com/a/9860611 Python [+ int + est illimité], donc un nombre entier de nanosecondes aura toujours une précision en nanosecondes indépendante de la valeur temporelle.

Par exemple, + time.time () + renvoie le nombre de secondes depuis le 1er janvier 1970. Ce nombre est déjà assez grand, donc la précision de ce nombre est au niveau microseconde. Cette fonction est celle qui présente la plus grande amélioration de sa version + _ns +. La résolution de + time.time_ns () + est environ 3 fois meilleure que pour `+ time.time () + ' .

Qu’est-ce qu’une nanoseconde au fait? Techniquement, c’est un milliardième de seconde, ou + 1e-9 + seconde si vous préférez la notation scientifique. Ce ne sont que des chiffres et ne fournissent pas vraiment d’intuition. Pour une meilleure aide visuelle, voir Grace Hopper merveilleux demonstration of the nanosecond.

Soit dit en passant, si vous avez besoin de travailler avec des heures avec une précision en nanosecondes, la bibliothèque standard + datetime + ne le coupera pas. Il ne gère explicitement que les microsecondes:

>>>

>>> from datetime import datetime, timedelta
>>> datetime(2018, 6, 27) + timedelta(seconds=1e-6)
datetime.datetime(2018, 6, 27, 0, 0, 0, 1)

>>> datetime(2018, 6, 27) + timedelta(seconds=1e-9)
datetime.datetime(2018, 6, 27, 0, 0)

Au lieu de cela, vous pouvez utiliser le + astropy + project. Son package + astropy.time + représente les datetimes à l’aide de deux objets + float + qui garantissent une "précision sous-nanoseconde dans le temps couvrant l’ère de l’univers. . "

>>>

>>> from astropy.time import Time, TimeDelta
>>> Time("2018-06-27")
<Time object: scale='utc' format='iso' value=2018-06-27 00:00:00.000>

>>> t = Time("2018-06-27") + TimeDelta(1e-9, format="sec")
>>> (t - Time("2018-06-27")).sec
9.976020010071807e-10

La dernière version de + astropy + est disponible en Python 3.5 et versions ultérieures.

Autres fonctionnalités assez cool

Jusqu’à présent, vous avez vu les nouvelles concernant les nouveautés de Python 3.7. Cependant, il existe de nombreux autres changements qui sont également assez cool. Dans cette section, nous examinerons brièvement certains d’entre eux.

L’ordre des dictionnaires est garanti

L’implémentation CPython de Python 3.6 a ordonné des dictionnaires. (PyPy a également cela.) Cela signifie que les éléments dans les dictionnaires sont itérés dans le même ordre qu’ils ont été insérés. Le premier exemple utilise Python 3.5 et le second utilise Python 3.6:

>>>

>>> {"one": 1, "two": 2, "three": 3}  # Python <= 3.5
{'three': 3, 'one': 1, 'two': 2}

>>> {"one": 1, "two": 2, "three": 3}  # Python >= 3.6
{'one': 1, 'two': 2, 'three': 3}

En Python 3.6, cet ordre n’était qu’une bonne conséquence de cette implémentation de + dict +. Dans Python 3.7, cependant, les dictionnaires préservant leur ordre d’insertion font partie de la spécification language. En tant que tel, il peut désormais être utilisé dans des projets qui prennent en charge uniquement Python> = 3.7 (ou CPython> = 3.6).

"+ Async " et " attendre +" sont des mots clés

Python 3.5 a introduit coroutines avec + async + et + attendent + syntaxe. Pour éviter les problèmes de compatibilité descendante, + async + et + wait + 'n’ont pas été ajoutés à la liste des mots clés réservés. En d’autres termes, il était toujours possible de définir des variables ou des fonctions nommées `+ async + et `+ wait + '.

En Python 3.7, ce n’est plus possible:

>>>

>>> async = 1
  File "<stdin>", line 1
    async = 1
          ^
SyntaxError: invalid syntax

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

Lifting du visage «+ asyncio +»

La bibliothèque standard + asyncio + a été initialement introduite dans Python 3.4 pour gérer la concurrence de manière moderne en utilisant des boucles d’événements, des coroutines et des futures. Voici une le introduction simple.

En Python 3.7, le module + asyncio + obtient un major face lift, y compris de nombreuses nouvelles fonctions, la prise en charge des variables de contexte (voir link: # context-variables [ci-dessous]), et amélioration des performances. Il convient de noter en particulier + asyncio.run () +, qui simplifie l’appel des coroutines à partir du code synchrone. À l’aide de https://docs.python.org/3.7/library/asyncio-task.html#asyncio.run [+ asyncio.run () +], vous n’avez pas besoin de créer explicitement la boucle d’événements. Un programme Hello World asynchrone peut maintenant être écrit:

import asyncio

async def hello_world():
    print("Hello World!")

asyncio.run(hello_world())

Variables de contexte

Les variables de contexte sont des variables qui peuvent avoir des valeurs différentes selon leur contexte. Ils sont similaires au stockage local de threads dans lequel chaque thread d’exécution peut avoir une valeur différente pour une variable. Cependant, avec les variables de contexte, il peut y avoir plusieurs contextes dans un thread d’exécution. Le principal cas d’utilisation des variables de contexte est le suivi des variables dans les tâches asynchrones simultanées.

L’exemple suivant construit trois contextes, chacun avec sa propre valeur pour la valeur + nom +. La fonction + accueillir () + pourra plus tard utiliser la valeur de + nom + dans chaque contexte:

import contextvars

name = contextvars.ContextVar("name")
contexts = list()

def greet():
    print(f"Hello {name.get()}")

# Construct contexts and set the context variable name
for first_name in ["Steve", "Dina", "Harry"]:
    ctx = contextvars.copy_context()
    ctx.run(name.set, first_name)
    contexts.append(ctx)

# Run greet function inside each context
for ctx in reversed(contexts):
    ctx.run(greet)

L’exécution de ce script accueille Steve, Dina et Harry dans l’ordre inverse:

$ python3.7 context_demo.py
Hello Harry
Hello Dina
Hello Steve

Importation de fichiers de données avec "` + importlib.resources + `"

Un défi lors de l’empaquetage d’un projet Python est de décider quoi faire avec les ressources du projet comme les fichiers de données nécessaires au projet. Quelques options ont été couramment utilisées:

  • Codez en dur un chemin d’accès au fichier de données.

  • Placez le fichier de données à l’intérieur du paquet et localisez-le en utilisant + fichier +.

  • Utilisez https://setuptools.readthedocs.io/en/latest/pkg_resources.html [+ setuptools.pkg_resources +] pour accéder à la ressource de fichier de données.

Chacun d’eux a ses défauts. La première option n’est pas portable. L’utilisation de + fichier + est plus portable, mais si le projet Python est installé, il peut se retrouver dans un zip et ne pas avoir d’attribut + fichier +. La troisième option résout ce problème, mais est malheureusement très lente.

Une meilleure solution est le nouveau module https://docs.python.org/3.7/library/importlib.html#module-importlib.resources [+ importlib.resources +] dans la bibliothèque standard. Il utilise la fonctionnalité d’importation existante de Python pour importer également des fichiers de données. Supposons que vous ayez une ressource dans un package Python comme ceci:

data/
│
├── alice_in_wonderland.txt
└── __init__.py

Notez que + data + doit être un Python package. Autrement dit, le répertoire doit contenir un fichier + init . Py + (qui peut être vide). Vous pouvez ensuite lire le fichier + alice_in_wonderland.txt + comme suit:

>>>

>>> from importlib import resources
>>> with resources.open_text("data", "alice_in_wonderland.txt") as fid:
...     alice = fid.readlines()
...
>>> print("".join(alice[:7]))
CHAPTER I. Down the Rabbit-Hole

Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do: once or twice she had peeped into the
book her sister was reading, but it had no pictures or conversations in
it, "and what is the use of a book," thought Alice "without pictures or
conversations?"

Une fonction https://docs.python.org/3.7/library/importlib.html#importlib.resources.open_binary [+ resources.open_binary () +] similaire est disponible pour ouvrir des fichiers en mode binaire. Dans le lien précédent: # customization-of-module-attributes [exemple “plugins as module attributes”], nous avons utilisé + importlib.resources + pour découvrir les plugins disponibles en utilisant + resources.contents () +. Voir Barry Warsaw’s PyCon 2018 talk pour plus d’informations.

Il est possible d’utiliser + importlib.resources + dans Python 2.7 et Python 3.4+ via un backport. Un guide sur la migration de + pkg_resources + vers + importlib.resources + est disponible.

Developer Tricks

Python 3.7 a ajouté plusieurs fonctionnalités destinées à vous en tant que développeur. Vous avez le lien: # the-breakpoint-built-in [déjà vu le nouveau + breakpoint () + built-in]. De plus, quelques nouveaux https://docs.python.org/3.7/using/cmdline.html#id5 [+ -X + options de ligne de commande] ont été ajoutés à l’interpréteur Python.

Vous pouvez facilement vous faire une idée du temps nécessaire aux importations dans votre script, en utilisant + -X importtime +:

$ python3.7 -X importtime my_script.py
import time: self [us] | cumulative | imported package
import time:      2607 |       2607 | _frozen_importlib_external
...
import time:       844 |      28866 |   importlib.resources
import time:       404 |      30434 | plugins

La colonne + cumulative + affiche le temps cumulé d’importation (en microsecondes). Dans cet exemple, l’importation de + plugins + a pris environ 0,03 secondes, dont la plupart ont été dépensées pour importer + importlib.resources +. La colonne + self + affiche le temps d’importation à l’exclusion des importations imbriquées.

Vous pouvez maintenant utiliser + -X dev + pour activer le "mode de développement". Le mode de développement ajoutera certaines fonctionnalités de débogage et vérifications d’exécution qui sont considérées comme trop lentes pour être activées par défaut. Il s’agit notamment d’activer https://docs.python.org/library/faulthandler.html#module-faulthandler [+ faulthandler +] pour afficher une trace sur les accidents graves, ainsi que d’autres avertissements et crochets de débogage.

Enfin, + -X utf8 + active le mode UTF-8. (Voir PEP 540.) Dans ce mode, + UTF-8 + sera utilisé pour l’encodage de texte indépendamment des paramètres régionaux actuels.

Optimisations

Chaque nouvelle version de Python est livrée avec un ensemble d’optimisations. Dans Python 3.7, il existe des accélérations importantes, notamment:

  • Il y a moins de surcharge pour appeler de nombreuses méthodes dans la bibliothèque standard.

  • Les appels de méthode sont généralement jusqu’à 20% plus rapides.

  • Le temps de démarrage de Python lui-même est réduit de 10 à 30%.

  • L’importation de + taper + est 7 fois plus rapide.

De plus, de nombreuses optimisations plus spécialisées sont incluses. Voir this list pour un aperçu détaillé.

Le résultat de toutes ces optimisations est que Python 3.7 est rapide. Il s’agit simplement de la fastest version de CPython publiée jusqu’à présent.

Alors, dois-je mettre à niveau?

Commençons par la réponse simple. Si vous souhaitez essayer l’une des nouvelles fonctionnalités que vous avez vues ici, vous devez pouvoir utiliser Python 3.7. L’utilisation d’outils tels que https://github.com/pyenv/pyenv [+ pyenv +] ou Anaconda facilite l’installation simultanée de plusieurs versions de Python. . Il n’y a aucun inconvénient à installer Python 3.7 et à l’essayer.

Maintenant, pour les questions plus compliquées. Devez-vous mettre à niveau votre environnement de production vers Python 3.7? Devez-vous faire dépendre votre propre projet de Python 3.7 pour profiter des nouvelles fonctionnalités?

Avec la mise en garde évidente que vous devez toujours faire des tests approfondis avant de mettre à niveau votre environnement de production, il y a très peu de choses dans Python 3.7 qui vont casser le code précédent (+ async + et `+ wait + 'devenir des mots-clés est un exemple cependant). Si vous utilisez déjà un Python moderne, la mise à niveau vers 3.7 devrait être assez fluide. Si vous voulez être un peu conservateur, vous voudrez peut-être attendre la sortie de la première version de maintenance — Python 3.7.1 — https://www.python.org/dev/peps/pep-0537/#maintenance-releases [prévu à titre indicatif en juillet 2018].

Faire valoir que vous ne devriez faire que votre projet 3.7 est plus difficile. La plupart des nouvelles fonctionnalités de Python 3.7 sont disponibles sous forme de rétroportages vers Python 3.6 (classes de données, + importlib.resources +) ou de commodités (démarrage et appels de méthode plus rapides, débogage plus facile et options + -X +). Dans ce dernier cas, vous pouvez en profiter en exécutant Python 3.7 vous-même tout en gardant votre code compatible avec Python 3.6 (ou inférieur).

Les principales fonctionnalités qui verrouillent votre code sur Python 3.7 sont le lien: # customization-of-module-attributes [+ getattr () + on modules], link: # typing-enhancements [références avant dans les astuces de type] et le link: # timing-precision [nanoseconde + fonctions temps +]. Si vous en avez vraiment besoin, vous devriez aller de l’avant et répondre à vos besoins. Sinon, votre projet sera probablement plus utile aux autres s’il peut être exécuté sur Python 3.6 plus longtemps.

Consultez le Porting to Python 3.7 guide pour plus de détails à connaître lors de la mise à niveau.