1 points par GN⁺ 2 시간 전 | 1 commentaires | Partager sur WhatsApp
  • 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

 
GN⁺ 2 시간 전
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 paresseux
    J’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

    • C’est une fonctionnalité de la 3.15 : https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • Je ne vois pas bien l’intérêt de ces imports paresseux ici. Si on utilise cette valeur dans des annotations de type au niveau du module, il faut quand même faire l’import, non ?
      Pour que ça marche, il faudrait une évaluation paresseuse des annotations, et sauf erreur ce n’est pas activé par défaut
    • Sur les versions précédentes de Python, on pouvait déjà contourner ça en implémentant def __getattr__(name: str) -> object: au niveau du module
    • C’est l’une des fonctionnalités phares de Python 3.15, donc j’imagine que c’est pour ça qu’elle manque dans cet article. C’est même la première mentionnée dans le document What's New, donc on peut clairement la considérer comme majeure
      Personnellement, 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
    • Python permet quasiment depuis le premier jour de faire des imports paresseux en plaçant l’instruction import à l’intérieur d’une fonction. La bibliothèque n’est alors pas importée avant l’appel de cette fonction
  • Avec l’ajout de frozendict en 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 hachable
    Cette 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-generator fait 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étrique
    https://en.wikipedia.org/wiki/Symmetric_difference

    • Oui, mais appliqué à Counter, cela donne une différence symétrique de multiensembles, et il n’existe pas de définition naturelle pour ça
      Si 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 pratique
  • L’un des exemples avec Counter est faux. Vérifié à la fois sur 3.13 et sur 3.15.0a
    Le résultat de Counter(a=3, b=1) - Counter(a=1, b=2) est Counter({'a': 2})

    • Oui, j’ai vu ça aussi. D’après la documentation, plusieurs opérations mathématiques sont fournies pour combiner des objets Counter afin 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 maximum
      Chaque 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

    • Au début, ça peut sembler simple, mais je me demande comment tu comptes assurer la maintenance de ces projets à l’avenir, surtout quand il faudra ajouter des fonctionnalités plus complexes
      Une possibilité serait peut-être de prototyper en Python puis de convertir ensuite
    • Go est vraiment peu convaincant pour le calcul scientifique ou le machine learning. L’écosystème de bibliothèques n’est pas là, et même avec l’aide d’un LLM, l’expérience autour des wrappers de C API reste faible
      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
    • Je cherche toujours pour Go un framework web complet du niveau de Django. Si quelque chose comme ça apparaît, je risque d’adopter Go immédiatement
    • Je me demande d’ailleurs pourquoi tu avais commencé à utiliser Python au départ. Et qu’est-ce que tu recommanderais à quelqu’un qui ne connaît absolument rien à la programmation ?
    • Intéressant. Si ça ne te dérange pas, je suis curieux de savoir si c’était des projets professionnels ou personnels
  • 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é

    • Le Python moderne d’aujourd’hui reste un vrai plaisir à utiliser, aussi bien au travail que sur des projets perso
    • Quelqu’un est-il en train de créer un langage proche de Python mais plus puissant, qui s’intègre bien avec Python tout en étant moins pesant ?
  • 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, ContextDecorator change 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 inattendue

    • L’équipe cœur de Python semble penser qu’il est peu probable que des gens dépendent de l’ancien comportement : https://github.com/python/cpython/pull/136212#issuecomment-4...
    • Quel serait le pire scénario ? Que des développeurs restent sur d’anciennes versions de Python à cause d’un changement incompatible ? Comme si ça allait arriver
  • Ce 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