La première version bêta de Python 3.11 est disponible et s'accompagne d'une meilleure gestion des erreurs,
de la prise en charge de toml et d'améliorations pour la programmation asynchrone
Meilleure gestion des erreurs
Python 3.10 nous a donné de meilleurs messages d'erreur à divers égards, mais Python 3.11 vise à les améliorer encore plus. Certaines des choses les plus importantes qui sont ajoutées aux messages d'erreur dans Python 3.11 sont :
Emplacements exacts des erreurs dans les retraçages
Jusqu'à présent, dans un traçage, la seule information que vous obteniez sur l'endroit où une exception a été déclenchée était la ligne. Le problème aurait pu être n'importe où sur la ligne, donc parfois cette information n'était pas suffisante.
Voici un exemple*:
def get_margin(data):
margin = data['profits']['monthly'] / 10 + data['profits']['yearly'] / 2
return margin
data = {
'profits': {
'monthly': 0.82,
'yearly': None,
},
'losses': {
'monthly': 0.23,
'yearly': 1.38,
},
}
print(get_margin(data))
Ce code génère une erreur, car l'un de ces champs dans le dictionnaire est None. Voici ce que nous obtenons*:
Traceback (most recent call last):
File "/Users/tusharsadhwani/code/marvin-python/mytest.py", line 15, in
print(get_margin(data))
File "/Users/tusharsadhwani/code/marvin-python/mytest.py", line 2, in print_margin
margin = data['profits']['monthly'] / 10 + data['profits']['yearly'] / 2
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'
Mais il est impossible de dire par le retraçage lui-même, quelle partie du calcul a causé l'erreur.
Sur la version 3.11 cependant :
Traceback (most recent call last):
File "asd.py", line 15, in
print(get_margin(data))
^^^^^^^^^^^^^^^^
File "asd.py", line 2, in print_margin
margin = data['profits']['monthly'] / 10 + data['profits']['yearly'] / 2
~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'
Il est évident que data['profits']['yearly'] était None.
Pour pouvoir restituer ces informations, les données end_line et end_col ont été ajoutées aux objets de code Python. Vous pouvez également accéder à ces informations directement via la méthode obj.__code__.co_positions().
Les notes pour les exceptions
Pour rendre les traces encore plus riches en contexte, Python 3.11 vous permet d'ajouter des notes aux objets d'exception, qui sont stockées dans les exceptions et affichées lorsque l'exception est déclenchée.
Prenez ce code par exemple, où nous ajoutons des informations importantes sur une logique de conversion de données d'API*:
def get_seconds(data):
try:
milliseconds = float(data['milliseconds'])
except ValueError as exc:
exc.add_note(
"The time field should always be a number, this is a critial bug. "
"Please report this to the backend team immediately."
)
raise # re-raises the exception, instead of silencing it
seconds = milliseconds / 1000
return seconds
get_seconds({'milliseconds': 'foo'}) # 'foo' is not a number!
Cette note ajoutée est imprimée juste en dessous du message d'exception*:
Traceback (most recent call last):
File "asd.py", line 14, in
get_seconds({"milliseconds": "foo"})
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "asd.py", line 3, in get_seconds
milliseconds = float(data["milliseconds"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: 'foo'
The time field should always be a number, this is a critial bug. Please report this to the backend team immediately.
Prise en charge toml intégrée
La bibliothèque standard a maintenant un support intégré pour lire les fichiers TOML, en utilisant le module tomllib*:
import tomllib
with open('.deepsource.toml', 'rb') as file:
data = tomllib.load(file)
tomllib est en fait basé sur une bibliothèque d'analyse TOML open source appelée tomli. Et actuellement, seule la lecture des fichiers TOML est prise en charge. Si vous devez plutôt écrire des données dans un fichier TOML, envisagez d'utiliser le package tomli-w.
Groupes de travail asynchrones
Lorsque vous faites de la programmation asynchrone, vous rencontrez souvent des situations où vous devez déclencher de nombreuses tâches à exécuter simultanément, puis prendre des mesures lorsqu'elles sont terminées. Par exemple, télécharger un tas d'images en parallèle, puis les regrouper dans un fichier zip à la fin.
Pour ce faire, vous devez collecter des tâches et les transmettre à asyncio.gather. Voici un exemple simple de tâches exécutées en parallèle avec la fonction de gather*:
import asyncio
async def simulate_flight(city, departure_time, duration):
await asyncio.sleep(departure_time)
print(f"Flight for {city} departing at {departure_time}PM")
await asyncio.sleep(duration)
print(f"Flight for {city} arrived.")
flight_schedule = {
'boston': [3, 2],
'detroit': [7, 4],
'new york': [1, 9],
}
async def main():
tasks = []
for city, (departure_time, duration) in flight_schedule.items():
tasks.append(simulate_flight(city, departure_time, duration))
await asyncio.gather(*tasks)
print("Simulations done.")
asyncio.run(main())
Mais devoir maintenir une liste des tâches soi-même pour pouvoir les attendre est un peu maladroit. Aussi, une nouvelle API est ajoutée à asyncio appelée Groupes de tâches*:
import asyncio
async def simulate_flight(city, departure_time, duration):
await asyncio.sleep(departure_time)
print(f"Flight for {city} departing at {departure_time}PM")
await asyncio.sleep(duration)
print(f"Flight for {city} arrived.")
flight_schedule = {
'boston': [3, 2],
'detroit': [7, 4],
'new york': [1, 9],
}
async def main():
async with asyncio.TaskGroup() as tg:
for city, (departure_time, duration) in flight_schedule.items():
tg.create_task(simulate_flight(city, departure_time, duration))
print("Simulations done.")
asyncio.run(main())
Lorsque le gestionnaire de contexte asyncio.TaskGroup() se ferme, il s'assure que toutes les tâches créées à l'intérieur ont fini de s'exécuter.
621473
Groupes d'exceptions
Une fonctionnalité similaire a également été ajoutée pour la gestion des exceptions dans les tâches asynchrones, appelées groupes d'exceptions.
Supposons que de nombreuses tâches asynchrones s'exécutent ensemble et que certaines d'entre elles génèrent des erreurs. Actuellement, le système de gestion des exceptions de Python ne fonctionne pas bien dans ce scénario.
Voici une courte démo de ce à quoi cela ressemble avec 3 tâches simultanées qui ont planté*:
import asyncio
def bad_task():
raise ValueError("oops")
async def main():
tasks = []
for _ in range(3):
tasks.append(asyncio.create_task(bad_task()))
await asyncio.gather(*tasks)
asyncio.run(main())
Lorsque vous exécutez ce code*:
$ python asd.py
Traceback (most recent call last):
File "asd.py", line 13, in
asyncio.run(main())
File "/usr/bin/python3.8/lib/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/usr/bin/python3.8/lib/asyncio/base_events.py", line 616, in run_until_complete
return future.result()
File "asd.py", line 9, in main
tasks.append(asyncio.create_task(bad_task()))
File "asd.py", line 4, in bad_task
raise ValueError("oops")
ValueError: oops
Rien n'indique que 3 de ces tâches s'exécutaient ensemble. Dès que la première échoue, elle fait planter tout le programme.
Mais en Python 3.11, le comportement est un peu meilleur*:
import asyncio
async def bad_task():
raise ValueError("oops")
async def main():
async with asyncio.TaskGroup() as tg:
for _ in range(3):
tg.create_task(bad_task())
asyncio.run(main())
$ python asd.py
+ Exception Group Traceback (most recent call last):
| File "
| File "/usr/local/lib/python3.11/asyncio/runners.py", line 181, in run
| return runner.run(main)
| ^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/asyncio/runners.py", line 115, in run
| return self._loop.run_until_complete(task)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/usr/local/lib/python3.11/asyncio/base_events.py", line 650, in run_until_complete
| return future.result()
| ^^^^^^^^^^^^^^^
| File "
| File "/usr/local/lib/python3.11/asyncio/taskgroups.py", line 139, in __aexit__
| raise me from None
| ^^^^^^^^^^^^^^^^^^
| ExceptionGroup: unhandled errors in a TaskGroup (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "
| ValueError: oops
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "
| ValueError: oops
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "
| ValueError: oops
+------------------------------------
L'exception nous indique maintenant que trois erreurs ont été générées, dans une structure appelée ExceptionGroup.
La gestion des exceptions avec ces groupes d'exceptions est également intéressante, vous pouvez soit faire except ExceptionGroup pour intercepter toutes les exceptions en une seule fois :
try:
asyncio.run(main())
except ExceptionGroup as eg:
print(f"Caught exceptions: {eg}")
$ python asd.py
Caught exceptions: unhandled errors in a TaskGroup (3 sub-exceptions)
Ou vous pouvez les intercepter en fonction du type d'exception, en utilisant la nouvelle syntaxe except**:
try:
asyncio.run(main())
except* ValueError as eg:
print(f"Caught ValueErrors: {eg}")
$ python asd.py
Caught ValueErrors: unhandled errors in a TaskGroup (3 sub-exceptions)
Améliorations apportées au module typing
Le module typing a vu beaucoup de mises à jour intéressantes dans cette version. Voici quelques-uns des plus intéressantes :
Génériques variadiques
La prise en charge des génériques variadiques a été ajoutée au module typing dans Python 3.11.
Cela signifie que vous pouvez désormais définir des types génériques pouvant contenir un nombre arbitraire de types. Il est utile pour définir des méthodes génériques pour les données multidimensionnelles.
Par exemple:
from typing import Generic
from typing_extensions import TypeVarTuple, Unpack
Shape = TypeVarTuple('Shape')
class Array(Generic[Unpack]):
...
# holds 1 dimensional data, like a regular list
items: Array = Array()
# holds 3 dimensional data, for example, X axis, Y axis and value
market_prices: Array[int, int, float] = Array()
# This function takes in an `Array` of any shape, and returns the same shape
def double(array: Array[Unpack]) -> Array[Unpack]:
...
# This function takes an N+2 dimensional array and reduces it to an N dimensional one
def get_values(array: Array[int, int, *Shape]) -> Array[*Shape]:
...
# For example:
vector_space: Array[int, int, complex] = Array()
reveal_type(get_values(vector_space)) # revealed type is Array
Les génériques variadiques peuvent être très utiles pour définir des fonctions qui mappent sur des données à N dimensions. Cette fonctionnalité peut être très utile pour la vérification de type des bases de code qui s'appuient sur des bibliothèques de science des données telles que numpy ou tensorflow.
L'équipe responsable du développement de Python explique que : « La nouvelle syntaxe Generic[*Shape] n'est prise en charge que dans Python 3.11. Pour utiliser cette fonctionnalité dans Python 3.10 et versions antérieures, vous pouvez utiliser la fonction intégrée typing.Unpack à la place de Generic[Unpack] ».
singledispatch prend désormais en charge les unions
functools.singledispatch est un moyen pratique de surcharger les fonctions en Python, basé sur des indications de type. Cela fonctionne en définissant une fonction générique et en se servant de @singledispatch. Ensuite, vous pouvez définir des variantes spécialisées de cette fonction, en fonction du type des arguments de la fonction*:
from functools import singledispatch
@singledispatch
def half(x):
"""Returns the half of a number"""
return x / 2
@half.register
def _(x: int):
"""For integers, return an integer"""
return x // 2
@half.register
def _(x: list):
"""For a list of items, get the first half of it."""
list_length = len(x)
return x[: list_length // 2]
# Outputs:
print(half(3.6)) # 1.8
print(half(15)) # 7
print(half([1, 2, 3, 4])) # [1, 2]
En inspectant le type donné aux arguments de la fonction, singledispatch peut créer des fonctions génériques, fournissant une manière non orientée objet de faire la surcharge de fonction.
Mais ce sont toutes de vieilles nouvelles. Ce que Python 3.11 apporte, c'est que maintenant, vous pouvez passer des types d'union pour ces arguments. Par exemple, pour enregistrer une fonction pour tous les types de nombres, vous deviez auparavant le faire séparément pour chaque type, tel que float, complex ou Decimal :
@half.register
def _(x: float):
return x / 2
@half.register
def _(x: complex):
return x / 2
@half.register
def _(x: decimal.Decimal):
return x / 2
Mais maintenant, vous pouvez tous les spécifier dans une Union*:
@half.register
def _(x: float | complex | decimal.Decimal):
return x / 2
Et le code fonctionnera exactement comme prévu.
Le type self
Auparavant, si vous deviez définir une méthode de classe qui renvoyait un objet de la classe elle-même, ajouter des types pour cela était un peu bizarre, cela ressemblerait à ceci*:
from typing import TypeVar
T = TypeVar('T', bound=type)
class Circle:
def __init__(self, radius: int) -> None:
self.radius = radius
@classmethod
def from_diameter(cls: T, diameter) -> T:
circle = cls(radius=diameter/2)
return circle
Pour pouvoir dire qu'une méthode retourne le même type que la classe elle-même, il fallait définir un TypeVar, et dire que la méthode retourne le même type T que la classe actuelle elle-même.
Mais avec le type Self, rien de tout cela n'est nécessaire*:
from typing import Self
class Circle:
def __init__(self, radius: int) -> None:
self.radius = radius
@classmethod
def from_diameter(cls, diameter) -> Self:
circle = cls(radius=diameter/2)
return circle
Required[] and NotRequired[]
TypedDict est vraiment utile pour ajouter des informations de type à une base de code qui utilise beaucoup de dictionnaires pour stocker des données. Voici comment vous pouvez les utiliser*:
from typing import TypedDict
class User(TypedDict):
name: str
age: int
user : User = {'name': "Alice", 'age': 31}
reveal_type(user['age']) # revealed type is 'int'
Cependant, TypedDicts avait une limitation, où vous ne pouviez pas avoir de paramètres facultatifs dans un dictionnaire, un peu comme les paramètres par défaut dans les définitions de fonction.
Par exemple, vous pouvez le faire avec un NamedTuple*:
from typing import NamedTuple
class User(NamedTuple):
name: str
age: int
married: bool = False
marie = User(name='Marie', age=29, married=True)
fredrick = User(name='Fredrick', age=17) # 'married' is False by default
Cela n'était pas possible avec un TypedDict (au moins sans définir plusieurs de ces types TypedDict). Mais maintenant, vous pouvez marquer n'importe quel champ comme NotRequired, pour signaler qu'il est normal que le dictionnaire n'ait pas ce champ*:
from typing import TypedDict, NotRequired
class User(TypedDict):
name: str
age: int
married: NotRequired
marie: User = {'name': 'Marie', 'age': 29, 'married': True}
fredrick : User = {'name': 'Fredrick', 'age': 17} # 'married' is not required
NotRequired est plus utile lorsque la plupart des champs de votre dictionnaire sont obligatoires, avec quelques champs non obligatoires. Mais, dans le cas contraire, vous pouvez dire à TypedDict de traiter chaque champ comme non requis par défaut, puis d'utiliser Required pour marquer les champs réellement requis.
Par exemple, c'est le même que le code précédent*:
from typing import TypedDict, Required
# `total=False` means all fields are not required by default
class User(TypedDict, total=False):
name: Required
age: Required
married: bool # now this is optional
marie: User = {'name': 'Marie', 'age': 29, 'married': True}
fredrick : User = {'name': 'Fredrick', 'age': 17} # 'married' is not required
contextlib.chdir
contextlib a un petit ajout, qui est un gestionnaire de contexte appelé chdir. Tout ce qu'il fait est de remplacer le répertoire de travail actuel par le répertoire spécifié dans le gestionnaire de contexte et de le remettre à ce qu'il était avant lorsqu'il se ferme.
Un cas d'utilisation potentiel peut être de rediriger vers où vous écrivez les journaux*:
import os
def write_logs(logs):
with open('output.log', 'w') as file:
file.write(logs)
def foo():
print("Scanning files...")
files = os.listdir(os.curdir) # lists files in current directory
logs = do_scan(files)
print("Writing logs to /tmp...")
with contextlib.chdir('/tmp'):
write_logs(logs)
print("Deleting files...")
files = os.listdir(os.curdir)
do_delete(files)
De cette façon, vous n'avez pas à vous soucier de modifier et de rétablir manuellement le répertoire actuel, le gestionnaire de contexte le fera pour vous.
Source : annonce Python 3.11 bêta
Voir aussi :
:fleche: La prochaine version de l'interpréteur Python standard, CPython, va s'accompagner d'améliorations significatives des performances, ont annoncé les développeurs durant la PyCon
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.

ils font la promotion de leur incompétence ?!!