1 Punkte von GN⁺ 1 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Mit dem Feature-Freeze von Python 3.15.0b1 stehen neben Lazy Imports und dem Tachyon-Profiler auch praktische Verbesserungen fest
  • TaskGroup.cancel() in asyncio ermöglicht es, Task-Gruppen elegant abzubrechen, ohne benutzerdefinierte Ausnahmen und contextlib.suppress
  • ContextDecorator wurde so geändert, dass er den gesamten Lebenszyklus von asynchronen Funktionen, Generatoren und asynchronen Iteratoren umschließt
  • Neue Hilfsfunktionen in threading serialisieren den Verbrauch von Iteratoren zwischen Threads oder duplizieren ihn, sodass die Abstraktion ohne Queue erhalten bleibt
  • Für Counter kommt eine XOR-Operation hinzu, und json.loads unterstützt mit array_hook und frozendict unveränderliches JSON-Parsing

Weniger bekannte Änderungen in Python 3.15

  • Mit dem Feature-Freeze von Python 3.15.0b1 stehen die Funktionen fest, die dieses Jahr in Python aufgenommen werden. Zu den großen Änderungen gehören Lazy Imports und der Tachyon-Profiler
  • In Python 3.15 gibt es neben den großen PEPs auch praktische kleinere Funktionsänderungen, darunter Verbesserungen bei asyncio, Context Managern, thread-sicheren Iteratoren, Counter und beim JSON-Parsing

Abbruch von asyncio TaskGroup

  • Eine zentrale Änderung in asyncio ist die neue Möglichkeit, TaskGroup elegant abzubrechen
  • TaskGroup ist eine Form von Structured Concurrency, mit der sich mehrere konkurrierende Aufgaben sauber starten und gemeinsam bis zum Abschluss abwarten lassen
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())




# Waits for all the tasks to complete
  • Vor Python 3.15 musste man, um auf ein Hintergrundsignal zu warten und dann die Ausführung einer TaskGroup zu beenden, eine benutzerdefinierte Ausnahme auslösen und sie mit contextlib.suppress abfangen
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()
  • Das funktionierte, weil bei einer Ausnahme innerhalb der Task-Gruppe die anderen Tasks abgebrochen werden, die benutzerdefinierte Ausnahme Interrupt als Teil einer ExceptionGroup ausgelöst wird und dann von contextlib.suppress herausgefiltert wird
  • Die Funktionsweise von suppress zusammen mit ExceptionGroup wurde zwar in Python 3.12 ergänzt, blieb aber weitgehend unbeachtet
  • TaskGroup.cancel in Python 3.15 macht dasselbe deutlich einfacher
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())

    if await wait_for_signal():
        tg.cancel()
  • TaskGroup.cancel() bricht die Gruppe ab, ohne eine Ausnahme auszulösen. Die Kombination aus separater Ausnahme und suppress ist damit nicht mehr nötig

Verbesserungen bei Context Managern

  • Context Manager konnten seit Python 3.3 auch direkt als Decorator verwendet werden
@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)(...)
  • Ein Context Manager wie duration(), der die Laufzeit eines Blocks ausgibt, lässt sich bequem wie ein Funktions-Decorator verwenden. Bei asynchronen Funktionen, Generatoren und asynchronen Iteratoren funktionierte das jedoch nicht immer korrekt
@duration('async workload')
async def async_workload():
    ...

@duration('generator workload')
def workload():
    while True:
        yield ...
  • Iteratoren, asynchrone Funktionen und asynchrone Iteratoren haben andere Semantiken als normale Funktionen: Beim Aufruf geben sie sofort jeweils ein Generatorobjekt, ein Coroutine-Objekt oder ein asynchrones Generatorobjekt zurück
  • Der bisherige Decorator umfasste nicht den gesamten Lebenszyklus des umschlossenen Ziels und war sofort abgeschlossen, sodass er nicht die tatsächliche gesamte Laufzeit abdeckte
  • In Python 3.15 prüft ContextDecorator nun den Typ der umschlossenen Funktion und wird so angewendet, dass der gesamte Lebenszyklus des jeweiligen Ziels abgedeckt wird
  • Damit lassen sich typische Fallstricke vermeiden, die beim Einsatz von Context Managern als Decorators auftraten, und es ergibt sich eine sauberere Syntax

Thread-sichere Iteratoren

  • Iteratoren gehören zu den grundlegenden Abstraktionen in Python: Sie trennen Datenquelle und Datenverbraucher und ermöglichen dadurch eine sauberere Struktur
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)
  • Diese Abstraktion kann in Threading- oder Free-Threading-Umgebungen aufbrechen. Normale Iteratoren sind nicht thread-sicher, sodass Werte übersprungen werden oder der interne Iteratorzustand beschädigt werden kann
  • threading.serialize_iterator in Python 3.15 umschließt bestehende Iteratoren und serialisiert ihren Verbrauch zwischen 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)
  • Bisher war man zur Synchronisierung des Verbrauchs zwischen Threads meist auf Queue angewiesen. Mit den neuen Hilfsfunktionen lässt sich die bestehende Iterator-Abstraktion auch in Multithread-Code beibehalten, ohne sie zu verändern

Weitere Funktionen

  • XOR-Operation für Counter

    • collections.Counter ist eine Klasse, mit der sich diskrete Häufigkeiten einfach zählen lassen. Sie verhält sich ähnlich wie dict[KeyType, int] und bietet mehrere nützliche Operationen
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 unterstützt außerdem die den Mengenoperationen Schnittmenge und Vereinigung entsprechenden Operatoren & und |
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 lässt sich als Menge diskreter Objekte betrachten; das Beispiel kann also wie folgt interpretiert werden
{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}
  • In Python 3.15 kommt nun zusätzlich eine XOR-Operation hinzu
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}
  • Wer die Mengenoperationen von Counter bisher nicht oft verwendet hat, wird vielleicht keinen konkreten Einsatzzweck für XOR im Kopf haben, aber die Erweiterung rundet die Operatoren logisch ab
  • Unveränderliche JSON-Objekte

    • Mit frozendict, das in Python 3.15 ergänzt wurde, lassen sich nun alle JSON-Typen – Arrays, Boolesche Werte, Fließkommazahlen, null, Strings und Objekte – in einer unveränderlichen und hashbaren Form darstellen
    • json.load und json.loads erhalten den Parameter array_hook, der den bisherigen object_hook ergänzt
    • Mit array_hook=tuple und object_hook=frozendict lassen sich JSON-Objekte direkt in unveränderliche Strukturen parsen
json.loads('{"a": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})

1 Kommentare

 
GN⁺ 1 시간 전
Hacker-News-Kommentare
  • Im Beispiel wird es als lazy from typing import Iterator verwendet, da fragt man sich, ob Python jetzt endlich Lazy Imports bekommen hat.
    Das ist wohl eine Änderung, die mir entgangen ist; ich würde gern wissen, ob das erst ab Python 3.15 gilt oder schon in früheren Versionen vorhanden war.

    • Ist eine Funktion von 3.15: https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • Ich verstehe nicht so recht, worin hier der Vorteil von Lazy Imports liegt. Wenn man den Wert ohnehin in einem Type Hint auf Modulebene verwendet, muss man ihn dann nicht doch importieren?
      Dafür bräuchte man doch Lazy Evaluation von Annotations, und soweit ich weiß, ist das nicht standardmäßig aktiviert.
    • In früheren Python-Versionen konnte man das bereits umgehen, indem man auf Modulebene def __getattr__(name: str) -> object: implementiert.
    • Das ist wohl eines der Aushängeschilder von Python 3.15 und fehlt deshalb in diesem Artikel. Im "What's New"-Dokument wird es sogar als Erstes erwähnt, also kann man es definitiv als zentrales Feature ansehen.
      Ich freue mich persönlich sehr darauf. Erst diese Woche habe ich gesehen, wie ein Python-Prozess wegen Out of Memory an die Speichergrenze kam, nur weil ein Import für ein Modul hinzugefügt wurde, das die Anwendung in der Praxis gar nicht verwendet.
    • Python konnte Lazy Imports im Grunde schon seit fast dem ersten Tag, indem man import-Anweisungen innerhalb von Funktionen platziert. Die Bibliothek wird dann erst importiert, wenn die Funktion tatsächlich aufgerufen wird.
  • Mit frozendict in 3.15 lassen sich jetzt alle JSON-Typen – also Arrays, Booleans, Fließkommazahlen, null, Strings und Objekte – in unveränderlicher und hashbarer Form darstellen.
    Gerade dieses letzte Feature gefällt mir wirklich sehr.

  • Gut finde ich auch, dass Python 3.15 Iterator-Synchronisations-Primitiven hinzufügt: https://docs.python.org/3.15/library/threading.html#iterator...
    Mein Paket threaded-generator macht mit Threads/Prozessen + Generatoren + Queues genau das bereits, daher scheint das eine gute Ergänzung zu sein: https://pypi.org/project/threaded-generator/

  • Es wurde gesagt, dass man sich bei den Mengenoperationen von Counter, insbesondere xor, schwer einen Einsatzzweck vorstellen kann, aber man muss nur auf die symmetrische Differenz schauen.
    https://en.wikipedia.org/wiki/Symmetric_difference

    • Stimmt schon, aber auf Counter angewandt wird daraus die symmetrische Differenz von Multimengen, und dafür gibt es keine wirklich natürliche Definition.
      Wenn ich den Vorschlag richtig verstanden habe, soll sie als Absolutwert der Differenz der Häufigkeiten jedes Elements definiert werden, aber das ist nicht einmal assoziativ. Wenn man nur die Parität betrachtet, lässt sich das natürlicher als Addition in F_2 interpretieren, aber selbst dann ist nicht klar, wofür man das praktisch einsetzen würde.
  • Eines der Counter-Beispiele ist falsch. Ich habe es sowohl in 3.13 als auch in 3.15.0a überprüft.
    Das Ergebnis von Counter(a=3, b=1) - Counter(a=1, b=2) ist Counter({'a': 2}).

    • Das ist mir auch aufgefallen. Laut Dokumentation gibt es mehrere mathematische Operationen, um Counter-Objekte zu Multimengen zu kombinieren; Addition und Subtraktion addieren bzw. subtrahieren die Häufigkeiten entsprechender Elemente, während Schnittmenge und Vereinigungsmenge jeweils die minimale bzw. maximale Häufigkeit zurückgeben.
      Jede Operation akzeptiert zwar auch Eingaben mit negativen Häufigkeiten, schließt im Ergebnis aber Werte mit Häufigkeit 0 oder kleiner aus. So oder so ist das ein hübsches Counter-Beispiel ;-)
  • Ich war zehn Jahre lang wirklich tief in Python drin und habe sehr gern damit gearbeitet, aber in der Welt nach AI-Codebots habe ich allein dieses Jahr schon mehr als 100.000 Zeilen gelöscht und in schnellere Sprachen portiert. Momentan geht vieles vor allem nach Go.

    • Am Anfang mag das einfach sein, aber ich frage mich, wie du dir künftig die Wartung dieser Projekte vorstellst, insbesondere wenn komplexere Features hinzukommen.
      Eine Möglichkeit wäre, erst in Python zu prototypen und dann zu konvertieren.
    • Für wissenschaftliches Rechnen oder Machine Learning ist Go wirklich schwach. Es fehlt an Bibliotheken, und selbst mit Hilfe von LLMs ist das Wrapping von C-APIs eher dürftig.
      Wenn man einmal Signalverarbeitungs-Code mit Filtern, Windowing und Overlap schreibt, merkt man schnell, dass sich das mit den aktuellen Bibliotheken kaum einfach umsetzen lässt.
    • Ich suche für Go weiterhin nach einem umfassenden Web-Framework im Stil von Django. Wenn so etwas erscheint, wäre ich vermutlich sofort dabei.
    • Mich würde interessieren, warum du ursprünglich überhaupt bei Python gelandet bist. Was würdest du jemandem empfehlen, der noch gar nichts vom Programmieren versteht?
    • Interessant. Wenn du magst, würde mich interessieren, ob das berufliche Projekte oder private Projekte waren.
  • Es gibt ein gutes Interview über die Interna und den Betrieb von Python, insbesondere im Zusammenhang mit Free-Threading: https://alexalejandre.com/programming/interview-with-ngoldba...

  • Ach, mein geliebtes Python. Ich habe dich fast 15 Jahre lang benutzt. Ich vermisse dich, aber inzwischen nutze ich dich nicht mehr. Es ist nicht deine Schuld, sondern das Leben hat sich verändert.

    • Das moderne Python von heute nutze ich sowohl im Job als auch in privaten Projekten weiterhin mit großer Freude.
    • Baut eigentlich jemand eine stärkere Python-ähnliche Sprache, die gut mit Python zusammenspielt, aber weniger Ballast mitbringt?
  • Iteratoren, asynchrone Funktionen und asynchrone Iteratoren hatten eine andere Semantik als normale Funktionen und passten deshalb nicht gut zu Decorators. Beim Aufruf geben sie sofort ein Generatorobjekt, eine Coroutine-Funktion bzw. ein asynchrones Generatorobjekt zurück, sodass der Decorator sofort endet, statt den gesamten Lebenszyklus dessen zu umhüllen, was er dekoriert.
    In 3.15 prüft ContextDecorator nun den Typ der umhüllten Funktion und wurde so geändert, dass der Decorator den gesamten Lebenszyklus abdeckt. Die Idee gefällt mir sehr, aber dass damit ohne Opt-in-Mechanismus das Verhalten bestehender Verwendungen subtil verändert wird, wirkt ziemlich riskant. Das ist zwar so ein Fall von „Spacebar Heating“, bei dem nur dann etwas schiefgeht, wenn jemand den früheren kaputten Zustand absichtlich genutzt hat, aber falls das tatsächlich vorkam, könnte es unerwartet brechen.

    • Das Python-Core-Team scheint die Wahrscheinlichkeit für gering zu halten, dass jemand vom bisherigen Verhalten abhängig ist: https://github.com/python/cpython/pull/136212#issuecomment-4...
    • Was wäre schon der schlimmste Fall? Dass Entwickler wegen einer inkompatiblen Änderung bei alten Python-Versionen bleiben? So etwas passiert doch nie.
  • Oft sind es gerade solche kleinen Features, die sich am Ende als am nützlichsten erweisen. Vor allem würde ich gern die neuen Ergänzungen der Standardbibliothek im aktuellen Projekt ausprobieren.