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

Python Enhancement Proposals

PEP 618 – Optionale Längenprüfung für zip

Autor:
Brandt Bucher <brandt at python.org>
Sponsor:
Antoine Pitrou <antoine at python.org>
BDFL-Delegate:
Guido van Rossum <guido at python.org>
Status:
Final
Typ:
Standards Track
Erstellt:
01-Mai-2020
Python-Version:
3.10
Post-History:
01-Mai-2020, 10-Mai-2020, 16-Jun-2020
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Zusammenfassung

Dieses PEP schlägt die Hinzufügung eines optionalen booleschen Schlüsselwortparameters strict zur integrierten Funktion zip vor. Wenn aktiviert, wird ein ValueError ausgelöst, wenn eines der Argumente vor den anderen erschöpft ist.

Motivation

Aus der persönlichen Erfahrung des Autors und einer Umfrage in der Standardbibliothek ist klar, dass viel (wenn nicht die meiste) zip-Nutzung Iterables betrifft, die gleich lang sein müssen. Manchmal wird diese Invariante aus dem Kontext des umgebenden Codes bewiesen, aber oft werden die zu zippenden Daten vom Aufrufer übergeben, separat bezogen oder auf irgendeine Weise generiert. In jedem dieser Fälle bedeutet das Standardverhalten von zip, dass fehlerhaftes Refactoring oder Logikfehler leicht zu stillschweigendem Datenverlust führen können. Diese Fehler sind nicht nur schwer zu diagnostizieren, sondern oft auch schwer überhaupt zu erkennen.

Es ist einfach, einfache Fälle zu finden, in denen dies ein Problem darstellen könnte. Zum Beispiel kann der folgende Code gut funktionieren, wenn items eine Sequenz ist, aber stillschweigend gekürzte, inkonsistente Ergebnisse liefern, wenn items vom Aufrufer zu einem konsumierbaren Iterator umgestaltet wird.

def apply_calculations(items):
    transformed = transform(items)
    for i, t in zip(items, transformed):
        yield calculate(i, t)

Es gibt mehrere andere Möglichkeiten, wie zip häufig verwendet wird. Idiomatische Tricks sind besonders anfällig, da sie oft von Benutzern angewendet werden, die kein vollständiges Verständnis dafür haben, wie der Code funktioniert. Ein Beispiel ist das Entpacken in zip, um verschachtelte Iterables verzögert „entzippen“ oder „transponieren“ zu können.

>>> x = [[1, 2, 3], ["one" "two" "three"]]
>>> xt = list(zip(*x))

Eine andere ist das „Chunking“ von Daten in gleich große Gruppen.

>>> n = 3
>>> x = range(n ** 2),
>>> xn = list(zip(*[iter(x)] * n))

Im ersten Fall ist nicht-rechteckige Daten normalerweise ein Logikfehler. Im zweiten Fall sind Daten, deren Länge kein Vielfaches von n ist, oft ebenfalls ein Fehler. Beide Idiome lassen jedoch die Endteile der fehlerhaften Eingabe stillschweigend weg.

Vielleicht am überzeugendsten ist die Verwendung von zip im ast-Modul der Standardbibliothek, das einen Fehler in literal_eval verursachte, der Teile fehlerhafter Knoten stillschweigend verwarf.

>>> from ast import Constant, Dict, literal_eval
>>> nasty_dict = Dict(keys=[Constant(None)], values=[])
>>> literal_eval(nasty_dict)  # Like eval("{None: }")
{}

Tatsächlich hat der Autor Dutzende weiterer Aufrufstellen in Pythons Standardbibliothek und Werkzeugen gezählt, bei denen die Aktivierung dieser neuen Funktion sofort angemessen wäre.

Begründung

Einige Kritiker behaupten, dass konstante boolesche Schalter ein „Code-Smell“ seien oder gegen Pythons Designphilosophie verstoßen. Python enthält jedoch derzeit mehrere Beispiele für boolesche Schlüsselwortparameter bei integrierten Funktionen, die typischerweise mit Compile-Zeit-Konstanten aufgerufen werden.

  • compile(..., dont_inherit=True)
  • open(..., closefd=False)
  • print(..., flush=True)
  • sorted(..., reverse=True)

Viele weitere gibt es in der Standardbibliothek.

Die Idee und der Name für diesen neuen Parameter wurden ursprünglich von Ram Rachum vorgeschlagen. Der Thread erhielt über 100 Antworten, wobei die Alternative „equal“ eine ähnliche Unterstützung erhielt.

Der Autor hat keine starke Präferenz zwischen den beiden Optionen, obwohl „equal equals“ in Prosa etwas unbeholfen ist. Es kann auch (fälschlicherweise) eine Art von „Gleichheit“ zwischen den gezippten Elementen implizieren.

>>> z = zip([2.0, 4.0, 6.0], [2, 4, 8], equal=True)

Spezifikation

Wenn die integrierte Funktion zip mit dem schlüsselwortexklusiven Argument strict=True aufgerufen wird, löst der resultierende Iterator einen ValueError aus, wenn die Argumente unterschiedliche Längen haben. Dieser Fehler tritt an dem Punkt auf, an dem die Iteration heute normalerweise stoppen würde.

Abwärtskompatibilität

Diese Änderung ist vollständig abwärtskompatibel. zip akzeptiert derzeit keine Schlüsselwortargumente, und das „nicht-strikte“ Standardverhalten, wenn strict weggelassen wird, bleibt unverändert.

Referenzimplementierung

Der Autor hat eine C-Implementierung entworfen.

Eine ungefähre Python-Übersetzung ist

def zip(*iterables, strict=False):
    if not iterables:
        return
    iterators = tuple(iter(iterable) for iterable in iterables)
    try:
        while True:
            items = []
            for iterator in iterators:
                items.append(next(iterator))
            yield tuple(items)
    except StopIteration:
        if not strict:
            return
    if items:
        i = len(items)
        plural = " " if i == 1 else "s 1-"
        msg = f"zip() argument {i+1} is shorter than argument{plural}{i}"
        raise ValueError(msg)
    sentinel = object()
    for i, iterator in enumerate(iterators[1:], 1):
        if next(iterator, sentinel) is not sentinel:
            plural = " " if i == 1 else "s 1-"
            msg = f"zip() argument {i+1} is longer than argument{plural}{i}"
            raise ValueError(msg)

Abgelehnte Ideen

Füge itertools.zip_strict hinzu

Dies ist die Alternative mit der größten Unterstützung in der Python-Ideas-Mailingliste, daher verdient sie eine detaillierte Diskussion. Sie hat keine disqualifizierenden Fehler und könnte als Ersatz gut genug funktionieren, falls dieses PEP abgelehnt wird.

In diesem Sinne soll dieser Abschnitt darlegen, warum die Hinzufügung eines optionalen Parameters zu zip eine geringere Änderung ist, die letztendlich besser dazu beiträgt, die Probleme zu lösen, die dieses PEP motivieren.

Präzedenzfall

Es scheint, dass ein großer Teil der Motivation hinter dieser Alternative darin besteht, dass zip_longest bereits in itertools existiert. zip_longest ist jedoch in vielerlei Hinsicht ein wesentlich komplexeres, spezialisierteres Werkzeug: Es übernimmt die Verantwortung für das Auffüllen fehlender Werte, eine Aufgabe, um die sich keine der beiden anderen Varianten kümmern muss.

Wenn sowohl zip als auch zip_longest nebeneinander in itertools oder als integrierte Funktionen existieren würden, dann wäre die Hinzufügung von zip_strict an derselben Stelle ein noch stärkeres Argument. Die neue „strikte“ Variante ist konzeptionell der zip-Funktion in Bezug auf Schnittstelle und Verhalten jedoch *viel* näher als zip_longest, während sie immer noch nicht die hohe Hürde erreicht, eine eigene integrierte Funktion zu sein. Angesichts dieser Situation scheint es am natürlichsten, dass zip diese neue Option direkt erweitert.

Benutzerfreundlichkeit

Wenn zip in der Lage ist, diese Art von Fehler zu verhindern, wird es für Benutzer viel einfacher, die Prüfung an Aufrufstellen mit dieser Eigenschaft zu aktivieren. Vergleichen Sie dies mit dem Importieren eines Drop-in-Ersatzes für ein integriertes Werkzeug, das sich etwas umständlich anfühlt, nur um eine knifflige Bedingung zu prüfen, die „immer“ wahr sein sollte.

Einige haben auch argumentiert, dass eine neue Funktion, die in der Standardbibliothek versteckt ist, irgendwie „auffindbarer“ ist als ein Schlüsselwortparameter der integrierten Funktion selbst. Der Autor stimmt dieser Einschätzung nicht zu.

Wartungskosten

Obwohl die Implementierung bei Verbesserungen der Benutzerfreundlichkeit nur eine sekundäre Rolle spielen sollte, ist es wichtig zu erkennen, dass die Hinzufügung eines neuen Werkzeugs wesentlich komplizierter ist als die Modifizierung eines bestehenden. Die CPython-Implementierung, die diesem PEP beiliegt, ist einfach und hat keine messbaren Leistungsauswirkungen auf das Standardverhalten von zip, während die Hinzufügung eines völlig neuen Werkzeugs zu itertools entweder erfordern würde:

  • Viel der bestehenden zip-Logik zu duplizieren, wie es zip_longest bereits tut.
  • zip, zip_longest oder beides erheblich zu refaktorieren, um eine gemeinsame oder geerbte Implementierung zu teilen (was die Leistung beeinträchtigen kann).

Mehrere „Modi“ zum Umschalten hinzufügen

Diese Option macht nur dann mehr Sinn als ein binärer Schalter, wenn wir drei oder mehr Modi erwarten. Die „offensichtlichen“ drei Optionen für diese aufgezählten oder konstanten Modi wären „shortest“ (das aktuelle zip-Verhalten), „strict“ (das vorgeschlagene Verhalten) und „longest“ (das itertools.zip_longest-Verhalten).

Es scheint jedoch nicht, dass die Hinzufügung von Verhaltensweisen außer dem aktuellen Standard und dem vorgeschlagenen „strict“-Modus den zusätzlichen Aufwand wert ist. Der klarste Kandidat, „longest“, würde einen neuen Parameter fillvalue erfordern (der für beide anderen Modi bedeutungslos ist). Dieser Modus wird auch bereits perfekt von itertools.zip_longest abgedeckt, und seine Hinzufügung würde zwei Möglichkeiten schaffen, dasselbe zu tun. Es ist nicht klar, welche die „offensichtliche“ Wahl wäre: der mode-Parameter auf der integrierten Funktion zip oder das seit langem bestehende Namensvetter-Werkzeug in itertools.

Eine Methode oder alternativen Konstruktor zum zip-Typ hinzufügen

Betrachten Sie die folgenden beiden Optionen, die beide vorgeschlagen wurden:

>>> zm = zip(*iters).strict()
>>> zd = zip.strict(*iters)

Es ist nicht offensichtlich, welche erfolgreich sein wird oder wie die andere scheitern wird. Wenn zip.strict als Methode implementiert wird, wird zm erfolgreich sein, aber zd wird auf eine von mehreren verwirrenden Arten fehlschlagen.

  • Ergebnisse liefern, die nicht in ein Tupel verpackt sind (wenn iters nur ein Element enthält, einen zip-Iterator).
  • Einen TypeError für einen falschen Argumenttyp auslösen (wenn iters nur ein Element enthält, keinen zip-Iterator).
  • Einen TypeError für eine falsche Anzahl von Argumenten auslösen (andernfalls).

Wenn zip.strict als classmethod oder staticmethod implementiert wird, wird zd erfolgreich sein und zm wird stillschweigend nichts liefern (was das Problem ist, das wir überhaupt vermeiden wollen).

Dieser Vorschlag wird durch die Tatsache weiter kompliziert, dass der tatsächliche zip-Typ von CPython derzeit ein undokumentiertes Implementierungsdetail ist. Das bedeutet, dass die Wahl eines der obigen Verhaltensweisen die aktuelle Implementierung effektiv „festschreiben“ wird (oder zumindest erfordert, dass sie emuliert wird) für die Zukunft.

Das Standardverhalten von zip ändern

Es ist nichts „falsch“ am Standardverhalten von zip, da es viele Fälle gibt, in denen es tatsächlich die richtige Art ist, mit ungleich langen Eingaben umzugehen. Es ist zum Beispiel äußerst nützlich, wenn man mit unendlichen Iteratoren arbeitet.

itertools.zip_longest existiert bereits, um die Fälle zu bedienen, in denen die „zusätzlichen“ Enddaten immer noch benötigt werden.

Einen Callback akzeptieren, um verbleibende Elemente zu behandeln

Obwohl diese Lösung im Grunde alles leisten kann, was ein Benutzer benötigt, macht sie die Behandlung der häufigeren Fälle (wie das Ablehnen von Längenunterschieden) unnötig kompliziert und unklar.

Einen AssertionError auslösen

Es gibt keine integrierten Funktionen oder Typen, die als Teil ihrer API einen AssertionError auslösen. Darüber hinaus besagt die offizielle Dokumentation einfach (in ihrer Gesamtheit):

Ausgelöst, wenn eine assert-Anweisung fehlschlägt.

Da diese Funktion nichts mit Pythons assert-Anweisung zu tun hat, wäre das Auslösen eines AssertionError hier unangemessen. Benutzer, die eine Prüfung wünschen, die im optimierten Modus deaktiviert ist (wie eine assert-Anweisung), können stattdessen strict=__debug__ verwenden.

Eine ähnliche Funktion zu map hinzufügen

Dieses PEP schlägt keine Änderungen an map vor, da die Verwendung von map mit mehreren Iterable-Argumenten eher selten ist. Die Entscheidung dieses PEPs soll jedoch als Präzedenzfall für eine zukünftige Diskussion (falls sie stattfindet) dienen.

Wenn abgelehnt, ist die Funktion realistischerweise nicht weiter verfolgenswert. Wenn akzeptiert, sollte eine solche Änderung an map kein eigenes PEP erfordern (obwohl, wie bei allen Verbesserungen, ihr Nutzen sorgfältig abgewogen werden sollte). Der Konsistenz halber sollte sie die gleiche API und Semantik wie hier für zip diskutiert befolgen.

Nichts tun

Diese Option ist vielleicht die unattraktivste.

Stillschweigend abgeschnittene Daten sind eine besonders tückische Art von Fehler, und das manuelle Schreiben einer robusten Lösung, die dies richtig macht, ist nicht trivial. Die realen motivierenden Beispiele aus Pythons eigener Standardbibliothek sind ein Beweis dafür, dass es *sehr* leicht ist, in die Art von Falle zu tappen, die diese Funktion vermeiden soll.

Referenzen

Beispiele

Hinweis

Diese Auflistung ist nicht erschöpfend.


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

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