PEP 765 – return/break/continue, die einen finally-Block verlassen, verbieten
- Autor:
- Irit Katriel <irit at python.org>, Alyssa Coghlan <ncoghlan at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 15. Nov. 2024
- Python-Version:
- 3.14
- Post-History:
- 09. Nov. 2024, 16. Nov. 2024
- Ersetzt:
- 601
- Resolution:
- Discourse-Nachricht
Zusammenfassung
Diese PEP schlägt vor, die Unterstützung für return, break und continue Anweisungen, die einen finally Block verlassen, zurückzuziehen. Dies wurde in der Vergangenheit von PEP 601 vorgeschlagen. Die aktuelle PEP basiert auf empirischen Beweisen bezüglich der Kosten/Nutzen dieses Wandels, die zum Zeitpunkt der Ablehnung von PEP 601 nicht existierten. Sie schlägt auch eine leicht andere Lösung vor, als die von PEP 601 vorgeschlagene.
Motivation
Die Semantik von return, break und continue in einem finally-Block ist für viele Entwickler überraschend. Die Dokumentation erwähnt, dass
- Wenn die
finally-Klausel einebreak-,continue- oderreturn-Anweisung ausführt, werden Ausnahmen nicht erneut ausgelöst. - Wenn eine
finally-Klausel einereturn-Anweisung enthält, ist der zurückgegebene Wert der von derreturn-Anweisung derfinally-Klausel, nicht der Wert von derreturn-Anweisung dertry-Klausel.
Beide Verhaltensweisen verursachen Verwirrung, aber die erste ist besonders gefährlich, da eine geschluckte Ausnahme eher unbemerkt durch Tests schlupfen kann als ein falscher Rückgabewert.
Im Jahr 2019 schlug PEP 601 vor, Python so zu ändern, dass es für einige Releases eine SyntaxWarning ausgibt und diese dann in eine SyntaxError umwandelt. Sie wurde zugunsten der Ansicht, dass dies ein Stilproblem sei, das von Linter und PEP 8 behandelt werden sollte, abgelehnt. Tatsächlich empfiehlt PEP 8 nun, keine Kontrollflussanweisungen in einem finally-Block zu verwenden, und Linter wie Pylint, Ruff und flake8-bugbear markieren sie als Problem.
Begründung
Eine aktuelle Analyse von realem Code zeigt, dass
- Diese Funktionen sind selten (2 pro Million LOC in den Top 8.000 PyPI-Paketen, 4 pro Million LOC in einer zufälligen Auswahl von Paketen). Dies könnte auf die Linter zurückzuführen sein, die dieses Muster kennzeichnen.
- Die meisten Verwendungen sind fehlerhaft und führen zu unbeabsichtigten Ausnahmen-schluckenden Fehlern.
- Code-Besitzer sind typischerweise empfänglich für die Behebung von Fehlern und finden dies einfach zu bewerkstelligen.
Weitere Details finden Sie im Anhang.
Diese neuen Daten deuten darauf hin, dass es den Python-Nutzern zugutekäme, wenn Python selbst sie von dieser schädlichen Funktion weglenken würde.
Eines der Argumente, das in der PEP 601-Diskussion vorgebracht wurde, war, dass Sprachfunktionen orthogonal sein und ohne kontextabhängige Einschränkungen kombiniert werden sollten. In der Zwischenzeit wurde jedoch PEP 654 implementiert, und sie verbietet return, break und continue in einer except*-Klausel, da die Semantik dieser Klausel die Eigenschaft verletzen würde, dass except*-Klauseln *parallel* arbeiten, sodass der Code einer Klausel nicht die Ausführung einer anderen unterdrücken sollte. In diesem Fall haben wir akzeptiert, dass eine Kombination von Funktionen schädlich genug sein kann, um ihre Zulassung zu verbieten.
Spezifikation
Die Änderung besteht darin, in der Sprachspezifikation festzulegen, dass der Compiler von Python eine SyntaxWarning oder SyntaxError ausgeben kann, wenn ein return, break oder continue den Kontrollfluss innerhalb eines finally-Blocks nach außen übertragen würde.
Diese Beispiele können eine SyntaxWarning oder SyntaxError ausgeben
def f():
try:
...
finally:
return 42
for x in o:
try:
...
finally:
break # (or continue)
Diese Beispiele würden keine Warnung oder Fehlermeldung ausgeben
try:
...
finally:
def f():
return 42
try:
...
finally:
for x in o:
break # (or continue)
CPython gibt in Version 3.14 eine SyntaxWarning aus, und wir lassen offen, ob und wann dies zu einer SyntaxError wird. Wir legen hier jedoch fest, dass eine SyntaxError durch die Sprachsyntax erlaubt ist, damit andere Python-Implementierungen diese umsetzen können.
Die CPython-Implementierung gibt die SyntaxWarning während der AST-Konstruktion aus, um sicherzustellen, dass die Warnung während der statischen Analyse und Kompilierung angezeigt wird, aber nicht während der Ausführung von vorkompiliertem Code. Wir erwarten, dass die Warnung von einem Projektverantwortlichen gesehen wird (wenn er statische Analysen ausführt oder eine CI, die keine vorkompilierten Dateien hat). Endbenutzer eines Projekts sehen jedoch nur dann eine Warnung, wenn sie die Vorkompilierung bei der Installation überspringen, Installationswarnungen überprüfen oder statische Analysen über ihre Abhängigkeiten durchführen.
Abwärtskompatibilität
Aus Gründen der Abwärtskompatibilität schlagen wir vor, dass CPython nur eine SyntaxWarning ausgibt, ohne konkreten Plan, diese zu einem Fehler hochzustufen. Code, der mit -We ausgeführt wird, könnte nach der Einführung nicht mehr funktionieren.
Sicherheitsimplikationen
Die Warnung/der Fehler hilft Programmierern, einige schwer zu findende Fehler zu vermeiden, und hat daher einen Sicherheitsvorteil. Wir sind uns keiner Sicherheitsprobleme im Zusammenhang mit der Ausgabe einer neuen SyntaxWarning oder SyntaxError bewusst.
Wie man das lehrt
Die Änderung wird in der Sprachspezifikation und in der Dokumentation "What's New" dokumentiert. Die SyntaxWarning wird Benutzer darauf aufmerksam machen, dass ihr Code geändert werden muss. Die empirischen Beweise zeigen, dass die notwendigen Änderungen typischerweise recht einfach sind.
Abgelehnte Ideen
Emit SyntaxError in CPython
PEP 601 schlug vor, dass CPython für einige Releases eine SyntaxWarning und danach eine SyntaxError ausgibt. Wir lassen offen, ob und wann dies in CPython zu einer SyntaxError wird, da wir glauben, dass eine SyntaxWarning den größten Nutzen mit geringerem Risiko bieten würde.
Semantik ändern
Es wurde vorgeschlagen, die Semantik von Kontrollflussanweisungen in finally so zu ändern, dass eine laufende Ausnahme Vorrang vor ihnen hat. Mit anderen Worten, ein return, break oder continue wäre erlaubt und würde den finally-Block verlassen, aber die Ausnahme würde trotzdem ausgelöst werden.
Dies wurde aus zwei Gründen abgelehnt. Erstens würde es die Semantik von funktionierendem Code auf eine Weise ändern, die schwer zu debuggen ist: Ein finally, das mit der Absicht geschrieben wurde, alle Ausnahmen zu schlucken (unter korrekter Verwendung der dokumentierten Semantik), würde nun zulassen, dass die Ausnahme weiterpropagiert. Dies kann nur in seltenen Grenzfällen zur Laufzeit geschehen und ist nicht garantiert, in Tests erkannt zu werden. Selbst wenn der Code falsch ist und einen Fehler beim Schlucken von Ausnahmen hat, könnte es für Benutzer schwierig sein zu verstehen, warum ein Programm unter 3.14 angefangen hat, Ausnahmen auszulösen, während es dies unter 3.13 nicht tat. Im Gegensatz dazu wird eine SyntaxWarning wahrscheinlich während der Tests gesehen, sie würde auf den genauen Ort des Problems im Code hinweisen und das Programm nicht am Laufen hindern.
Der zweite Einwand betraf die vorgeschlagene Semantik. Die Motivation für die Zulassung von Kontrollflussanweisungen ist nicht, dass dies nützlich wäre, sondern der Wunsch nach Orthogonalität der Funktionen (was, wie wir in der Einleitung erwähnt haben, im Fall von except*-Klauseln bereits verletzt wird). Die vorgeschlagene Semantik ist jedoch kompliziert, da sie nahelegt, dass return, break und continue sich normal verhalten, wenn finally ohne laufende Ausnahme ausgeführt wird, aber zu etwas wie einem einfachen raise werden, wenn eine vorhanden ist. Es ist schwer zu behaupten, dass die Funktionen orthogonal sind, wenn die Anwesenheit der einen die Semantik der anderen verändert.
Anhang
return in finally als schädlich erachtet
Unten ist eine gekürzte Version eines Forschungsberichts von Irit Katriel, der am 9. Nov. 2024 gepostet wurde. Er beschreibt eine Untersuchung der Verwendung von return, break und continue in einer finally-Klausel im realen Code und befasst sich mit den Fragen: Benutzen die Leute es? Wie oft benutzen sie es falsch? Wie viel Umbruch würde die vorgeschlagene Änderung verursachen?
Methode
Die Analyse basiert auf den 8.000 beliebtesten PyPI-Paketen, gemessen an der Anzahl der Downloads in den letzten 30 Tagen. Sie wurden am 17.-18. Oktober heruntergeladen, mit einem Skript von Guido van Rossum, das wiederum auf Hugo van Kemenades Tool basiert, das eine Liste der beliebtesten Pakete erstellt.
Nach dem Download wurde ein zweites Skript verwendet, um für jede Datei einen AST zu erstellen und ihn zu durchlaufen, um break-, continue- und return-Anweisungen zu identifizieren, die sich direkt innerhalb eines finally-Blocks befinden.
Anschließend habe ich den aktuellen Quellcode für jedes Vorkommen gefunden und ihn kategorisiert. Für Fälle, in denen der Code falsch zu sein schien, habe ich ein Issue im Bugtracker des Projekts erstellt. Die Antworten auf diese Issues sind ebenfalls Teil der in dieser Untersuchung gesammelten Daten.
Ergebnisse
Ich habe beschlossen, keine Liste der falschen Verwendungen aufzunehmen, aus Sorge, dass dies den Bericht wie eine beschämende Übung aussehen lassen würde. Stattdessen werde ich die Ergebnisse allgemein beschreiben, aber erwähnen, dass einige der gefundenen Probleme in sehr beliebten Bibliotheken auftreten, darunter eine Cloud-Sicherheitsanwendung. Für diejenigen, die dazu neigen, sollte es nicht schwer sein, meine Analyse zu reproduzieren, da ich Links zu den von mir verwendeten Skripten im Abschnitt "Methode" bereitgestellt habe.
Die untersuchten Projekte enthielten insgesamt 120.964.221 Zeilen Python-Code, und darin fand das Skript 203 Instanzen von Kontrollflussanweisungen in einem finally-Block. Die meisten waren return, eine Handvoll waren break, und keine waren continue. Von diesen
- 46 sind korrekt und treten in Tests auf, die auf dieses Muster als Funktion abzielen (z.B. Tests für Linter, die es erkennen).
- 8 scheinen korrekt zu sein – entweder schlucken sie absichtlich Ausnahmen oder treten auf, wo keine aktive Ausnahme auftreten kann. Trotz ihrer Korrektheit ist es nicht schwer, sie umzuschreiben, um das schlechte Muster zu vermeiden, und es würde den Code klarer machen: Das bewusste Schlucken von Ausnahmen kann expliziter mit
except BaseException:erfolgen, und einreturn, das keine Ausnahmen schluckt, kann nach demfinally-Block verschoben werden. - 149 waren eindeutig falsch und können zu unbeabsichtigtem Schlucken von Ausnahmen führen. Diese werden im nächsten Abschnitt analysiert.
Die Fehlerfälle
Viele der Fehlerfälle folgten diesem Muster
try:
...
except SomeSpecificError:
...
except Exception:
logger.log(...)
finally:
return some_value
Code wie dieser ist offensichtlich falsch, weil er absichtlich Exception-Unterklassen protokolliert und schluckt, während er BaseExceptions stillschweigend schluckt. Die Absicht ist hier entweder, dass BaseExceptions weiterpropagiert werden, oder (falls der Autor sich des BaseException-Problems nicht bewusst ist), alle Ausnahmen zu protokollieren und zu schlucken. Selbst wenn das except Exception jedoch zu except BaseException geändert würde, hätte dieser Code immer noch das Problem, dass der finally-Block alle Ausnahmen schluckt, die aus dem except-Block ausgelöst werden, und dies ist wahrscheinlich nicht die Absicht (wenn doch, kann dies mit einem weiteren try-except BaseException explizit gemacht werden).
Eine weitere Variante des Problems, das in echtem Code gefunden wurde, sieht so aus
try:
...
except:
return NotImplemented
finally:
return some_value
Hier scheint die Absicht zu sein, NotImplemented zurückzugeben, wenn eine Ausnahme ausgelöst wird, aber der return im finally-Block würde den im except-Block überschreiben.
Hinweis
Nach der Diskussion wiederholte ich die Analyse an einer zufälligen Auswahl von PyPI-Paketen (um Code von *durchschnittlichen* Programmierern zu analysieren). Die Stichprobe enthielt insgesamt 77.398.892 Codezeilen mit 316 Instanzen von return/break/continue in finally. Das sind also etwa 4 Instanzen pro Million Codezeilen.
Autorreaktionen
Von den 149 fehlerhaften Instanzen von return oder break in einer finally-Klausel waren 27 veraltet, in dem Sinne, dass sie nicht im Haupt-/Master-Branch der Bibliothek vorkommen, da der Code inzwischen gelöscht oder korrigiert wurde. Die restlichen 122 befinden sich in 73 verschiedenen Paketen, und ich habe in jedem ein Issue erstellt, um die Autoren auf die Probleme aufmerksam zu machen. Innerhalb von zwei Wochen erhielten 40 der 73 Issues eine Reaktion von den Code-Betreuern
- 15 Issues hatten einen PR geöffnet, um das Problem zu beheben.
- 20 erhielten Reaktionen, die das Problem als etwas anerkannten, das es wert ist, untersucht zu werden.
- 3 schlossen das Issue als "funktioniert wie vorgesehen", da der Code nicht mehr gewartet wird, daher wird er nicht behoben.
- 2 schlossen das Issue als "funktioniert wie vorgesehen", einer sagte, dass sie alle Ausnahmen schlucken wollen, aber der andere schien sich des Unterschieds zwischen
ExceptionundBaseExceptionnicht bewusst zu sein.
Ein Issue wurde mit einem bereits bestehenden offenen Issue über mangelnde Reaktion auf Strg+C verknüpft, was eine Verbindung vermuten lässt.
Zwei der Issues wurden als "good first issue" gekennzeichnet.
Die korrekten Verwendungen
Die 8 Fälle, in denen die Funktion korrekt zu sein scheint (in Nicht-Test-Code), verdienen ebenfalls Aufmerksamkeit. Diese stellen den "Umbruch" dar, der durch die Sperrung der Funktion entstehen würde, denn hier muss funktionierender Code geändert werden. Ich habe die Autoren in diesen Fällen nicht kontaktiert, daher müssen wir die Schwierigkeit, diese Änderungen selbst vorzunehmen, einschätzen. In dem vollständigen Bericht ist gezeigt, dass die erforderliche Änderung in jedem Fall gering ist.
Diskussion
Das Erste, was zu beachten ist, ist, dass return/break/continue in einem finally-Block nichts ist, was wir oft sehen: 203 Instanzen in über 120 Millionen Codezeilen. Das ist möglicherweise den Lintern zu verdanken, die davor warnen.
Die zweite Beobachtung ist, dass die meisten Verwendungen falsch waren: 73% in unserer Stichprobe (149 von 203).
Schließlich waren die Autorenreaktionen überwältigend positiv. Von den 40 innerhalb von zwei Wochen erhaltenen Antworten erkannten 35 das Problem an, und 15 davon erstellten sogar einen PR, um es zu beheben. Nur zwei hielten den Code für in Ordnung, und drei erklärten, dass der Code nicht mehr gewartet wird, daher würden sie ihn nicht überprüfen.
Die 8 Instanzen, in denen der Code anscheinend wie beabsichtigt funktioniert, lassen sich nicht schwer umschreiben.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0765.rst
Zuletzt geändert: 2025-03-20 10:09:14 GMT