- Avec le gel des fonctionnalités de Python 3.15.0b1, des améliorations pratiques ont aussi été confirmées, au-delà des imports différés et du profileur Tachyon
- TaskGroup.cancel() dans
asyncio permet d’annuler proprement un groupe de tâches sans exception personnalisée ni contextlib.suppress
- ContextDecorator a été modifié pour envelopper l’intégralité du cycle de vie des fonctions asynchrones, générateurs et itérateurs asynchrones
- Les nouveaux utilitaires de threading permettent de sérialiser ou de dupliquer la consommation d’itérateurs entre threads sans recourir à une Queue ni casser l’abstraction
- Un opérateur xor a été ajouté à
Counter, et json.loads prend désormais en charge le parsing JSON immuable avec array_hook et frozendict
Des changements moins connus dans Python 3.15
- Avec le gel des fonctionnalités de Python 3.15.0b1, les fonctionnalités qui entreront dans Python cette année sont désormais fixées. Parmi les grands changements figurent les imports différés et le profileur Tachyon
- Python 3.15 inclut aussi de petites évolutions fonctionnelles moins visibles que les grands PEP, avec des améliorations côté
asyncio, gestionnaires de contexte, itérateurs sûrs pour les threads, Counter et parsing JSON
Annulation de TaskGroup dans asyncio
- L’évolution centrale dans
asyncio est l’ajout d’un mécanisme permettant d’annuler proprement un TaskGroup
TaskGroup est une forme de concurrence structurée, qui permet de créer proprement plusieurs travaux concurrents et d’attendre qu’ils soient tous terminés
async with asyncio.TaskGroup() as tg:
tg.create_task(run())
tg.create_task(run())
# Waits for all the tasks to complete
- Avant Python 3.15, pour interrompre l’exécution d’un
TaskGroup après avoir attendu un signal en arrière-plan, il fallait lever une exception personnalisée puis la filtrer avec contextlib.suppress
class Interrupt(Exception):
...
with suppress(Interrupt):
async with asyncio.TaskGroup() as tg:
tg.create_task(run())
tg.create_task(run())
if await wait_for_signal():
raise Interrupt()
- Cette approche fonctionne parce que, si une exception se produit dans le groupe de tâches, les autres tâches sont annulées, puis l’exception personnalisée
Interrupt est levée comme élément d’un ExceptionGroup, avant d’être filtrée par contextlib.suppress
- La manière dont
suppress fonctionne avec ExceptionGroup a été ajoutée dans Python 3.12, mais est passée relativement inaperçue
- Dans Python 3.15, TaskGroup.cancel rend la même opération bien plus simple
async with asyncio.TaskGroup() as tg:
tg.create_task(run())
tg.create_task(run())
if await wait_for_signal():
tg.cancel()
TaskGroup.cancel() annule le groupe sans lever d’exception, ce qui élimine le besoin d’une exception dédiée combinée à suppress
Améliorations des gestionnaires de contexte
- Depuis Python 3.3, les gestionnaires de contexte pouvaient aussi être utilisés directement comme décorateurs
@contextmanager
def duration(message: str) -> Iterator[None]:
start = time.perf_counter()
try:
yield
finally:
print(f"{message} elapsed {time.perf_counter() - start:.2f} seconds")
@duration('workload')
def workload():
...
# Or simple as a wrapper
duration('stuff')(other_workload)(...)
- Des gestionnaires de contexte comme
duration(), qui affichent le temps d’exécution d’un bloc, sont pratiques car ils peuvent s’utiliser comme décorateurs de fonction, mais ils ne fonctionnaient pas toujours correctement avec les fonctions asynchrones, les générateurs et les itérateurs asynchrones
@duration('async workload')
async def async_workload():
...
@duration('generator workload')
def workload():
while True:
yield ...
- Les itérateurs, fonctions asynchrones et itérateurs asynchrones ont une sémantique différente des fonctions ordinaires : lors de l’appel, ils renvoient immédiatement respectivement un objet générateur, un objet coroutine ou un objet générateur asynchrone
- L’ancien décorateur ne couvrait pas tout le cycle de vie de l’élément enveloppé et se terminait immédiatement, sans englober la durée réelle d’exécution
- Dans Python 3.15,
ContextDecorator vérifie le type de la fonction qu’il enveloppe et a été modifié pour que le décorateur couvre l’intégralité du cycle de vie de la cible
- Cela permet d’éviter un piège fréquent lorsqu’on utilise un gestionnaire de contexte comme décorateur, tout en gardant une syntaxe plus propre
Itérateurs thread-safe
- Les itérateurs sont l’une des abstractions fondamentales de Python, car ils séparent la source des données de leur consommateur et permettent une structure plus propre
lazy from typing import Iterator
def stream_events(...) -> Iterator[str]:
while True:
yield blocking_get_event(...)
events = stream_events(...)
for event in events:
consume(event)
- Cette abstraction peut se briser dans un environnement à threads ou en free-threading : les itérateurs de base ne sont pas thread-safe, ce qui peut provoquer des valeurs sautées ou corrompre leur état interne
- Dans Python 3.15, threading.serialize_iterator enveloppe un itérateur existant pour sérialiser sa consommation entre plusieurs threads
import threading
events = threading.serialize_iterator(stream_events(...))
with ThreadPoolExecutor() as executor:
fut1 = executor.submit(consume, events)
fut2 = executor.submit(consume, events)
source1, source2 = threading.concurrent_tee(squares(10), n=2)
with ThreadPoolExecutor() as executor:
fut1 = executor.submit(consume, source1)
fut2 = executor.submit(consume, source2)
- Jusqu’ici, on s’appuyait surtout sur Queue pour synchroniser la consommation entre threads, mais ces nouveaux utilitaires permettent de conserver l’abstraction existante des itérateurs dans du code multithread sans avoir à la modifier
Fonctionnalités supplémentaires
-
Opérateur xor pour Counter
- collections.Counter est une classe qui permet de compter facilement des occurrences discrètes. Elle se comporte un peu comme
dict[KeyType, int] tout en offrant plusieurs opérations utiles
c = Counter(a=3, b=1)
d = Counter(a=1, b=2)
print(f"{c + d = }") # add two counters together: c[x] + d[x]
print(f"{c - d = }") # subtract (keeping only positive counts)
Counter(a=4, b=3)
Counter(a=1, b=0)
Counter dispose aussi des opérateurs & et |, correspondant respectivement à l’intersection et à l’union
print(f"{c & d = }") # intersection: min(c[x], d[x])
print(f"{c | d = }") # union: max(c[x], d[x])
Counter(a=1, b=1)
Counter(a=3, b=2)
Counter peut être vu comme un ensemble d’objets discrets, et l’exemple peut s’interpréter ainsi
{a_0, a_1, a_2, b_0} & {a_0, b_0, b_1} == {a_0, b_0}
{a_0, a_1, a_2, b_0} | {a_0, b_0, b_1} == {a_0, a_1, a_2, b_0, b_1}
- Python 3.15 ajoute désormais un opérateur xor
c = Counter(a=3, b=1)
d = Counter(a=1, b=2)
c ^ d == c | d - c & d == Counter(a=3, b=2) - Counter(a=1, b=1) == Counter(a=2, b=1)
{a_0, a_1, a_2, b_0} ^ {a_0, b_0, b_1} == {a_1, a_2, b_1}
- Si vous n’utilisiez pas souvent les opérations ensemblistes de
Counter, il n’est pas évident d’imaginer un cas d’usage concret pour xor, mais il s’agit d’un ajout qui complète l’ensemble des opérateurs
-
Objets JSON immuables
- Avec l’ajout de frozendict dans Python 3.15, tous les types JSON — tableaux, booléens, nombres réels, null, chaînes et objets — peuvent désormais être représentés sous une forme immuable et hachable
- Un paramètre
array_hook a été ajouté à json.load et json.loads, en complément du object_hook existant
- En combinant
array_hook=tuple et object_hook=frozendict, il devient possible de parser directement des objets JSON en structures immuables
json.loads('{"a": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})
1 commentaires
Réactions sur Hacker News
Dans les exemples, on voit
lazy from typing import Iterator, donc je me demande si Python a enfin des imports paresseuxJ’ai l’impression d’avoir raté ce changement, et je me demande si ça arrive avec Python 3.15 ou si c’était déjà présent avant
Pour que ça marche, il faudrait une évaluation paresseuse des annotations, et sauf erreur ce n’est pas activé par défaut
def __getattr__(name: str) -> object:au niveau du modulePersonnellement, je l’attends avec impatience. Rien que cette semaine, j’ai vu un processus Python dépasser sa limite mémoire et finir en out of memory simplement parce qu’un import de module non utilisé avait été ajouté à une application
importà l’intérieur d’une fonction. La bibliothèque n’est alors pas importée avant l’appel de cette fonctionAvec l’ajout de
frozendicten 3.15, on peut désormais représenter tous les types JSON — tableaux, booléens, nombres à virgule flottante, null, chaînes, objets — sous une forme immuable et hachableCette dernière brique me plaît vraiment beaucoup
Je trouve très bien que Python 3.15 ajoute des primitives de synchronisation d’itérateurs : https://docs.python.org/3.15/library/threading.html#iterator...
Mon package
threaded-generatorfait justement ça avec threads/processus + générateurs + files, donc ça devrait bien le compléter : https://pypi.org/project/threaded-generator/Il était dit qu’il était difficile d’imaginer un usage des opérations ensemblistes de
Counter, en particulier xor, mais il suffit de penser à la différence symétriquehttps://en.wikipedia.org/wiki/Symmetric_difference
Counter, cela donne une différence symétrique de multiensembles, et il n’existe pas de définition naturelle pour çaSi j’ai bien compris la proposition, ce serait défini comme la valeur absolue de la différence entre les comptes de chaque élément, mais cette opération n’est même pas associative. Si on ne regardait que la parité, on pourrait l’interpréter comme une addition dans
F_2, ce qui serait plus naturel, mais même là je ne vois pas vraiment à quoi ça servirait en pratiqueL’un des exemples avec
Counterest faux. Vérifié à la fois sur 3.13 et sur 3.15.0aLe résultat de
Counter(a=3, b=1) - Counter(a=1, b=2)estCounter({'a': 2})Counterafin de produire des multiensembles ; l’addition et la soustraction additionnent ou soustraient les comptes des éléments correspondants, tandis que l’intersection et l’union renvoient respectivement les comptes minimum et maximumChaque opération accepte des entrées avec des comptes négatifs, mais dans la sortie, les résultats dont le compte est inférieur ou égal à 0 sont exclus. En tout cas, joli Counter-example ;-)
J’ai été vraiment passionné par Python pendant 10 ans et j’ai adoré travailler avec, mais dans le monde postérieur aux codebots IA, j’ai déjà supprimé plus de 100 000 lignes cette année pour les réécrire dans des langages plus rapides. En ce moment, je migre surtout vers Go
Une possibilité serait peut-être de prototyper en Python puis de convertir ensuite
Si tu écris du code de traitement du signal avec des filtres, des fenêtres, du chevauchement, etc., il n’existe pratiquement pas de solution simple avec les bibliothèques actuelles
Il y a une bonne interview sur la structure interne et le fonctionnement de Python, notamment autour du free-threading : https://alexalejandre.com/programming/interview-with-ngoldba...
Ah, mon cher Python. Je t’ai utilisé pendant presque 15 ans. Tu me manques, mais je ne t’utilise plus. Ce n’est pas ta faute, c’est juste que ma vie a changé
Les itérateurs, fonctions asynchrones et itérateurs asynchrones avaient une sémantique différente de celle des fonctions ordinaires, ce qui les rendait peu compatibles avec les décorateurs. À l’appel, ils renvoient immédiatement un objet générateur, une coroutine ou un objet générateur asynchrone, de sorte que le décorateur se termine tout de suite au lieu d’englober tout le cycle de vie qu’il est censé envelopper
En 3.15,
ContextDecoratorchange pour vérifier le type de fonction qu’il enveloppe afin que le décorateur couvre bien tout le cycle de vie ; j’aime beaucoup l’idée, mais le fait de modifier subtilement le comportement des usages existants sans mécanisme d’activation explicite me semble assez risqué. Il faudrait vraiment qu’un cas du type « chauffage de la barre d’espace » dépende intentionnellement de l’ancien comportement cassé pour que ça pose problème, mais si c’est le cas, cela pourrait casser de manière inattendueCe sont souvent ces petites fonctionnalités qui finissent par être les plus utiles. J’ai particulièrement envie d’essayer les nouvelles ajouts à la bibliothèque standard dans mon projet actuel