Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 469 – Migration von Dict-Iterationscode nach Python 3

Autor:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Zurückgezogen
Typ:
Standards Track
Erstellt:
18. Apr. 2014
Python-Version:
3.5
Post-History:
18. Apr. 2014, 21. Apr. 2014

Inhaltsverzeichnis

Zusammenfassung

Für Python 3 änderte PEP 3106 das Design des eingebauten Typs dict und der Mapping-API im Allgemeinen, um die separaten listenbasierten und iteratorbasierten APIs in Python 2 durch eine zusammengeführte, speichereffiziente API basierend auf Mengen- und Multimengenansichten zu ersetzen. Dieser neue Stil der Dict-Iteration wurde auch zum dict-Typ von Python 2.7 als neue Menge von Iterationsmethoden hinzugefügt.

Das bedeutet, dass es jetzt 3 verschiedene Arten der Dict-Iteration gibt, die möglicherweise nach Python 3 migriert werden müssen, wenn eine Anwendung den Übergang macht.

  • Listen als mutable Schnappschüsse: d.items() -> list(d.items())
  • Iterator-Objekte: d.iteritems() -> iter(d.items())
  • Setbasierte dynamische Ansichten: d.viewitems() -> d.items()

Es gibt derzeit keine allgemein vereinbarte Best Practice, wie man den gesamten Python 2 Dict-Iterationscode zuverlässig in den gemeinsamen Teil von Python 2 und 3 konvertiert, insbesondere wenn die Testabdeckung des portierten Codes begrenzt ist. Dieses PEP überprüft die verschiedenen Arten, wie die Python 2 Iterations-APIs aufgerufen werden können, und untersucht die verfügbaren Optionen für die Migration dieses Codes nach Python 3 über den gemeinsamen Teil von Python 2.6+ und Python 3.0+.

Das PEP betrachtet auch die Frage, ob es Ergänzungen gibt, die es wert sein könnten, in Python 3.5 vorgenommen zu werden, um den Migrationsprozess für Anwendungscode zu erleichtern, der sich beim Sprung nach Python 3 nicht um die Unterstützung früherer Versionen kümmern muss.

Rücknahme eines PEP

Beim Schreiben des zweiten Entwurfs dieses PEP kam ich zu dem Schluss, dass die Lesbarkeit von hybriden Python 2/3 Mapping-Codes tatsächlich am besten durch bessere Hilfsfunktionen verbessert werden kann, anstatt durch Änderungen an Python 3.5+. Der Hauptwert, den ich jetzt in diesem PEP sehe, ist eine klare Aufzeichnung der empfohlenen Ansätze zur Migration von Mapping-Iterationscode von Python 2 nach Python 3 sowie Vorschläge, wie Dinge lesbar und wartbar bleiben können, wenn hybrider Code geschrieben wird, der beide Versionen unterstützt.

Insbesondere empfehle ich, dass hybrider Code den direkten Aufruf von Mapping-Iterationsmethoden vermeidet und stattdessen, wo möglich, auf eingebaute Funktionen zurückgreift, und einige zusätzliche Hilfsfunktionen für Fälle, die eine einfache Kombination aus einer eingebauten Funktion und einer Mapping-Methode in reinem Python 3-Code wären, aber etwas anders gehandhabt werden müssen, um exakt dieselben Semantik in Python 2 zu erhalten.

Statische Code-Checker wie pylint könnten potenziell um eine optionale Warnung bezüglich der direkten Verwendung der Mapping-Iterationsmethoden in einer hybriden Codebasis erweitert werden.

Mapping-Iterationsmodelle

Python 2.7 bietet drei verschiedene Sätze von Methoden, um die Schlüssel, Werte und Elemente aus einer dict-Instanz zu extrahieren, was 9 von 18 öffentlichen Methoden des dict-Typs ausmacht.

In Python 3 wurde dies auf nur 3 von 11 öffentlichen Methoden reduziert (da auch die Methode has_key entfernt wurde).

Listen als mutable Schnappschüsse

Dies ist der älteste der drei Stile der Dict-Iteration und somit derjenige, der von den Methoden d.keys(), d.values() und d.items() in Python 2 implementiert wird.

Diese Methoden geben alle Listen zurück, die Schnappschüsse des Zustands des Mappings zum Zeitpunkt des Aufrufs der Methode sind. Dies hat einige Konsequenzen:

  • Das Originalobjekt kann frei verändert werden, ohne die Iteration über den Schnappschuss zu beeinflussen.
  • Der Schnappschuss kann unabhängig vom Originalobjekt geändert werden.
  • Der Schnappschuss verbraucht Speicher, der proportional zur Größe des Original-Mappings ist.

Die semantisch äquivalenten Operationen in Python 3 sind list(d.keys()), list(d.values()) und list(d.iteritems()).

Iterator-Objekte

In Python 2.2 erhielten dict-Objekte Unterstützung für das damals neue Iterator-Protokoll, was eine direkte Iteration über die im Dictionary gespeicherten Schlüssel ermöglichte und somit die Notwendigkeit vermied, eine Liste zu erstellen, nur um die Dictionary-Inhalte einzeln zu durchlaufen. iter(d) bietet direkten Zugriff auf das Iterator-Objekt für die Schlüssel.

Python 2 bietet auch eine Methode d.iterkeys(), die im Wesentlichen synonym zu iter(d) ist, sowie die Methoden d.itervalues() und d.iteritems().

Diese Iteratoren bieten Live-Ansichten des zugrunde liegenden Objekts und können daher fehlschlagen, wenn die Menge der Schlüssel im zugrunde liegenden Objekt während der Iteration geändert wird.

>>> d = dict(a=1)
>>> for k in d:
...     del d[k]
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

Als Iteratoren ist die Iteration über diese Objekte eine Einwegoperation: Sobald der Iterator erschöpft ist, müssen Sie zum ursprünglichen Mapping zurückkehren, um erneut zu iterieren.

In Python 3 funktioniert die direkte Iteration über Mappings genauso wie in Python 2. Es gibt keine methodenbasierten Äquivalente – die semantischen Äquivalente von d.itervalues() und d.iteritems() in Python 3 sind iter(d.values()) und iter(d.items()).

Die Kompatibilitätsmodule six und future.utils bieten beide die Hilfsfunktionen iterkeys(), itervalues() und iteritems(), die effiziente Iterator-Semantik in Python 2 und 3 bereitstellen.

Setbasierte dynamische Ansichten

Das in Python 3 als methodenbasierte API bereitgestellte Modell sind mengenbasierte dynamische Ansichten (technisch gesehen Multimengen im Fall der values()-Ansicht).

In Python 3 bieten die von d.keys(), d.values() und d. items() zurückgegebenen Objekte eine Live-Ansicht des aktuellen Zustands des zugrunde liegenden Objekts, anstatt einen vollständigen Schnappschuss des aktuellen Zustands zu erstellen, wie es in Python 2 der Fall war. Diese Änderung ist unter vielen Umständen sicher, bedeutet aber, dass, wie bei der direkten Iterations-API, das Hinzufügen oder Entfernen von Schlüsseln während der Iteration vermieden werden muss, um den folgenden Fehler zu vermeiden.

>>> d = dict(a=1)
>>> for k, v in d.items():
...     del d[k]
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

Im Gegensatz zur Iterations-API sind diese Objekte iterierbar und keine Iteratoren: Sie können sie mehrmals durchlaufen, und jedes Mal durchlaufen sie das gesamte zugrunde liegende Mapping.

Diese Semantik ist auch in Python 2.7 als die Methoden d.viewkeys(), d.viewvalues() und d.viewitems() verfügbar.

Das Kompatibilitätsmodul future.utils bietet auch die Hilfsfunktionen viewkeys(), viewvalues() und viewitems(), wenn es unter Python 2.7 oder Python 3.x ausgeführt wird.

Direkte Migration nach Python 3

Das Migrationstool 2to3 handhabt direkte Migrationen nach Python 3 gemäß den oben beschriebenen semantischen Äquivalenten.

  • d.keys() -> list(d.keys())
  • d.values() -> list(d.values())
  • d.items() -> list(d.items())
  • d.iterkeys() -> iter(d.keys())
  • d.itervalues() -> iter(d.values())
  • d.iteritems() -> iter(d.items())
  • d.viewkeys() -> d.keys()
  • d.viewvalues() -> d.values()
  • d.viewitems() -> d.items()

Anstelle von 9 unterschiedlichen Mapping-Methoden für die Iteration gibt es jetzt nur noch die 3 Ansichtsmethoden, die sich auf einfache Weise mit den beiden relevanten eingebauten Funktionen kombinieren lassen, um alle Verhaltensweisen abzudecken, die als dict-Methoden in Python 2.7 verfügbar sind.

Beachten Sie, dass in vielen Fällen d.keys() durch einfaches d ersetzt werden kann, aber das Migrationstool 2to3 versucht diese Ersetzung nicht.

Das Migrationstool 2to3 bietet auch **keine** automatische Unterstützung für die Migration von Referenzen auf diese Objekte als gebundene oder ungebundene Methoden – es automatisiert nur Konvertierungen, bei denen die API sofort aufgerufen wird.

Migration zum gemeinsamen Teil von Python 2 und 3

Bei der Migration zum gemeinsamen Teil von Python 2 und 3 sind die obigen Transformationen im Allgemeinen nicht geeignet, da sie entweder zur Erstellung einer redundanten Liste in Python 2 führen, in mindestens einigen Fällen unerwartet unterschiedliche Semantik aufweisen oder beides.

Da die meisten Codes, die im gemeinsamen Teil von Python 2 und 3 laufen, mindestens Python 2.6 unterstützen, hängt der derzeit empfohlene Ansatz für die Konvertierung von Mapping-Iterationsoperationen von zwei Hilfsfunktionen für die effiziente Iteration über Mapping-Werte und Mapping-Element-Tupel ab.

  • d.keys() -> list(d)
  • d.values() -> list(itervalues(d))
  • d.items() -> list(iteritems(d))
  • d.iterkeys() -> iter(d)
  • d.itervalues() -> itervalues(d)
  • d.iteritems() -> iteritems(d)

Sowohl six als auch future.utils bieten entsprechende Definitionen von itervalues() und iteritems() (zusammen mit im Wesentlichen redundanten Definitionen von iterkeys()). Das Erstellen eigener Definitionen dieser Funktionen in einem benutzerdefinierten Kompatibilitätsmodul ist ebenfalls relativ einfach.

try:
    dict.iteritems
except AttributeError:
    # Python 3
    def itervalues(d):
        return iter(d.values())
    def iteritems(d):
        return iter(d.items())
else:
    # Python 2
    def itervalues(d):
        return d.itervalues()
    def iteritems(d):
        return d.iteritems()

Der größte Verlust an Lesbarkeit entsteht derzeit bei der Konvertierung von Code, der tatsächlich Listen-basierte Schnappschüsse benötigt, die in Python 2 standardmäßig waren. Dieser Verlust an Lesbarkeit könnte wahrscheinlich durch die Bereitstellung von listvalues- und listitems-Hilfsfunktionen gemindert werden, wodurch die betroffenen Konvertierungen vereinfacht werden können zu

  • d.values() -> listvalues(d)
  • d.items() -> listitems(d)

Die entsprechenden Kompatibilitätsfunktionsdefinitionen sind genauso einfach wie ihre Iterator-Gegenstücke.

try:
    dict.iteritems
except AttributeError:
    # Python 3
    def listvalues(d):
        return list(d.values())
    def listitems(d):
        return list(d.items())
else:
    # Python 2
    def listvalues(d):
        return d.values()
    def listitems(d):
        return d.items()

Mit dieser erweiterten Menge an Kompatibilitätsfunktionen würde Python 2-Code dann in "idiomatischen" hybriden 2/3-Code wie folgt konvertiert werden:

  • d.keys() -> list(d)
  • d.values() -> listvalues(d)
  • d.items() -> listitems(d)
  • d.iterkeys() -> iter(d)
  • d.itervalues() -> itervalues(d)
  • d.iteritems() -> iteritems(d)

Dies ist gut lesbar im Vergleich zum idiomatischen reinen Python 3-Code, der die Mapping-Methoden und Builtins direkt verwendet.

  • d.keys() -> list(d)
  • d.values() -> list(d.values())
  • d.items() -> list(d.items())
  • d.iterkeys() -> iter(d)
  • d.itervalues() -> iter(d.values())
  • d.iteritems() -> iter(d.items())

Es ist auch bemerkenswert, dass bei der Verwendung dieses Ansatzes hybrider Code die Mapping-Methoden niemals direkt aufruft: Er ruft stattdessen immer eine eingebaute Funktion oder eine Hilfsfunktion auf, um die exakt gleichen Semantiken sowohl unter Python 2 als auch unter Python 3 zu gewährleisten.

Migration von Python 3 zum gemeinsamen Teil mit Python 2.7

Während die Mehrheit der Migrationen derzeit von Python 2 entweder direkt nach Python 3 oder zum gemeinsamen Teil von Python 2 und Python 3 erfolgt, gibt es auch einige Migrationen neuerer Projekte, die in Python 3 beginnen und später Python 2-Unterstützung hinzufügen, entweder aufgrund von Benutzeranfragen oder um auf Python 2-Bibliotheken zugreifen zu können, die noch nicht in Python 3 verfügbar sind (und deren Portierung nach Python 3 oder die Erstellung einer Python 3-kompatiblen Ersetzung keine triviale Aufgabe ist).

In diesen Fällen ist die Python 2.7-Kompatibilität oft ausreichend, und die nur für 2.7+ verfügbaren Ansichtsbasierten Hilfsfunktionen von future.utils ermöglichen es, die reinen Zugriffe auf die Python 3 Mapping-Ansichtsmethoden durch Code zu ersetzen, der mit sowohl Python 2.7 als auch Python 3 kompatibel ist (beachten Sie, dass dies die einzige Migrationsübersicht in diesem PEP ist, die Python 3-Code links von der Konvertierung hat).

  • d.keys() -> viewkeys(d)
  • d.values() -> viewvalues(d)
  • d.items() -> viewitems(d)
  • list(d.keys()) -> list(d)
  • list(d.values()) -> listvalues(d)
  • list(d.items()) -> listitems(d)
  • iter(d.keys()) -> iter(d)
  • iter(d.values()) -> itervalues(d)
  • iter(d.items()) -> iteritems(d)

Wie bei Migrationen von Python 2 zum gemeinsamen Teil ist zu beachten, dass der hybride Code letztendlich die Mapping-Methoden nie direkt aufruft – er ruft nur Builtins und Hilfsfunktionen auf, wobei letztere die semantischen Unterschiede zwischen Python 2 und Python 3 adressieren.

Mögliche Änderungen an Python 3.5+

Der wichtigste Vorschlag, der zur potenziellen Erleichterung der Migration von bestehendem Python 2-Code nach Python 3 gemacht wird, ist die Wiederherstellung einiger oder aller alternativen Iterations-APIs in der Python 3 Mapping-API. Insbesondere schlug der erste Entwurf dieses PEP vor, die folgenden Konvertierungen beim Übergang zum gemeinsamen Teil von Python 2 und Python 3.5+ zu ermöglichen:

  • d.keys() -> list(d)
  • d.values() -> list(d.itervalues())
  • d.items() -> list(d.iteritems())
  • d.iterkeys() -> d.iterkeys()
  • d.itervalues() -> d.itervalues()
  • d.iteritems() -> d.iteritems()

Mögliche Abhilfen für die zusätzliche Sprachkomplexität in Python 3, die durch die Wiederherstellung dieser Methoden entsteht, umfassen deren sofortige Deprecierung sowie die potenzielle Ausblendung aus der dir()-Funktion (oder vielleicht sogar die Definition einer Möglichkeit, pydoc über Funktionsdeprecierungen zu informieren).

In dem Fall jedoch, in dem die Listen-Ausgabe tatsächlich gewünscht wird, ist das Endergebnis dieses Vorschlags tatsächlich weniger lesbar als eine entsprechend definierte Hilfsfunktion, und die Funktions- und Methodenformen der Iterator-Versionen sind aus Lesbarkeitsperspektive praktisch gleichwertig.

Sofern ich nichts Kritisches übersehen habe, scheinen leicht verfügbare Hilfsfunktionen listvalues() und listitems() die Lesbarkeit von hybridem Code besser zu verbessern als alles, was wir der Python 3.5+ Mapping-API wieder hinzufügen könnten, und werden keine langfristigen Auswirkungen auf die Komplexität von Python 3 selbst haben.

Diskussion

Die Tatsache, dass 5 Jahre nach der Python 3-Migration immer noch Benutzer die Dict-API-Änderungen als signifikante Migrationsbarriere betrachten, deutet darauf hin, dass es Probleme mit zuvor empfohlenen Ansätzen gibt. Dieses PEP versucht, diese Probleme zu untersuchen und Fälle zu isolieren, in denen frühere Ratschläge (sofern vorhanden) problematisch sein könnten.

Meine Einschätzung (weitgehend basierend auf Feedback von Twisted-Entwicklern) ist, dass Probleme am wahrscheinlichsten auftreten, wenn versucht wird, d.keys(), d.values() und d.items() in hybriden Code zu verwenden. Obwohl es oberflächlich betrachtet so aussieht, als ob es Fälle gäbe, in denen die semantischen Unterschiede ignoriert werden könnten, ist die Änderung von „mutable snapshot“ zu „dynamic view“ in der Praxis signifikant genug, dass es wahrscheinlich besser ist, einfach die Verwendung von Listen- oder Iterator-Semantik für hybriden Code zu erzwingen und die Verwendung der Ansichtssemantik für reinen Python 3-Code zu überlassen.

Dieser Ansatz schafft auch Regeln, die einfach und sicher genug sind, dass sie in Code-Modernisierungs-Skripten, die auf den gemeinsamen Teil von Python 2 und Python 3 abzielen, automatisiert werden können, genauso wie 2to3 sie beim Targeting von reinem Python 3-Code automatisch konvertiert.

Danksagungen

Dank der Leute am Twisted-Sprint-Tisch bei PyCon für eine sehr angeregte Diskussion über diese Idee (und mehrere andere Themen), und besonders an Hynek Schlawack für die Moderation, als es etwas zu hitzig wurde :)

Danke auch an JP Calderone und Itamar Turner-Trauring für ihr Feedback per E-Mail, sowie an die Teilnehmer der python-dev review der ersten Version des PEP.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0469.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT