- 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
Hacker-News-Kommentare
Im Beispiel wird es als
lazy from typing import Iteratorverwendet, 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.
Dafür bräuchte man doch Lazy Evaluation von Annotations, und soweit ich weiß, ist das nicht standardmäßig aktiviert.
def __getattr__(name: str) -> object:implementiert.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.
import-Anweisungen innerhalb von Funktionen platziert. Die Bibliothek wird dann erst importiert, wenn die Funktion tatsächlich aufgerufen wird.Mit
frozendictin 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-generatormacht 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
Counterangewandt 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_2interpretieren, 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)istCounter({'a': 2}).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.
Eine Möglichkeit wäre, erst in Python zu prototypen und dann zu konvertieren.
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.
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.
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
ContextDecoratornun 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.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.