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

Python Enhancement Proposals

PEP 616 – Methoden zum Entfernen von Präfixen und Suffixen in Zeichenketten

Autor:
Dennis Sweeney <sweeney.dennis650 at gmail.com>
Sponsor:
Eric V. Smith <eric at trueblade.com>
Status:
Final
Typ:
Standards Track
Erstellt:
19. Mär 2020
Python-Version:
3.9
Post-History:
20. Mär 2020

Inhaltsverzeichnis

Zusammenfassung

Dies ist ein Vorschlag zur Aufnahme zweier neuer Methoden, removeprefix() und removesuffix(), in die APIs der verschiedenen Zeichenkettenobjekte von Python. Diese Methoden würden ein Präfix bzw. Suffix aus einer Zeichenkette entfernen, falls vorhanden, und würden zu Unicode str-Objekten, binären bytes- und bytearray-Objekten sowie collections.UserString hinzugefügt werden.

Begründung

Es gab wiederholte Probleme auf Python-Ideas [2] [3], Python-Dev [4] [5] [6] [7], dem Bug Tracker und StackOverflow [8], die sich auf Benutzerverwirrung bezüglich der bestehenden str.lstrip- und str.rstrip-Methoden beziehen. Diese Benutzer erwarten typischerweise das Verhalten von removeprefix und removesuffix, sind aber überrascht, dass der Parameter für lstrip als eine Menge von Zeichen interpretiert wird, nicht als eine Teilzeichenkette. Dieses wiederholte Problem ist ein Beweis dafür, dass diese Methoden nützlich sind. Die neuen Methoden ermöglichen eine klarere Umleitung von Benutzern zum gewünschten Verhalten.

Als weiterer Beweis für die Nützlichkeit dieser Methoden berichteten mehrere Benutzer auf Python-Ideas [2] häufig, dass sie ähnliche Funktionen in ihrem Code zur Produktivitätssteigerung einfügen. Die Implementierung enthielt oft subtile Fehler bei der Behandlung von leeren Zeichenketten, so dass eine gut getestete integrierte Methode nützlich wäre.

Die bestehenden Lösungen zur Erzielung des gewünschten Verhaltens bestehen darin, entweder die Methoden wie in der Spezifikation unten zu implementieren oder reguläre Ausdrücke wie im Ausdruck re.sub('^' + re.escape(prefix), '', s) zu verwenden, was weniger auffindbar ist, einen Modulimport erfordert und zu weniger lesbarem Code führt.

Spezifikation

Die eingebaute Klasse str erhält zwei neue Methoden, die sich wie folgt verhalten, wenn type(self) is type(prefix) is type(suffix) is str

def removeprefix(self: str, prefix: str, /) -> str:
    if self.startswith(prefix):
        return self[len(prefix):]
    else:
        return self[:]

def removesuffix(self: str, suffix: str, /) -> str:
    # suffix='' should not call self[:-0].
    if suffix and self.endswith(suffix):
        return self[:-len(suffix)]
    else:
        return self[:]

Wenn die Argumente Instanzen von str-Unterklassen sind, verhalten sich die Methoden so, als ob diese Argumente zuerst in Basis-str-Objekte umgewandelt worden wären, und der Rückgabewert ist immer eine Basis-str.

Methoden mit entsprechender Semantik werden den integrierten bytes- und bytearray-Objekten hinzugefügt. Wenn b entweder ein bytes- oder ein bytearray-Objekt ist, akzeptieren b.removeprefix() und b.removesuffix() jedes bytes-ähnliche Objekt als Argument. Die beiden Methoden werden auch zu collections.UserString mit ähnlichem Verhalten hinzugefügt.

Motivierende Beispiele aus der Python-Standardbibliothek

Die folgenden Beispiele zeigen, wie die vorgeschlagenen Methoden Code zu einem oder mehreren der folgenden machen können:

  1. Weniger fragil

    Der Code hängt nicht davon ab, dass der Benutzer die Länge eines Literals zählt.

  2. Leistungsfähiger

    Der Code erfordert keinen Aufruf der Python-eigenen Funktion len und auch nicht der teureren Methode str.replace().

  3. Beschreibender

    Die Methoden bieten eine höherwertige API für bessere Lesbarkeit des Codes im Gegensatz zur traditionellen Methode des String-Slicings.

find_recursionlimit.py

  • Aktuell
    if test_func_name.startswith("test_"):
        print(test_func_name[5:])
    else:
        print(test_func_name)
    
  • Verbessert
    print(test_func_name.removeprefix("test_"))
    

deccheck.py

Dies ist ein interessanter Fall, da der Autor die Methode str.replace in einer Situation verwendet hat, in der nur ein Präfix entfernt werden sollte.

  • Aktuell
    if funcname.startswith("context."):
        self.funcname = funcname.replace("context.", "")
        self.contextfunc = True
    else:
        self.funcname = funcname
        self.contextfunc = False
    
  • Verbessert
    if funcname.startswith("context."):
        self.funcname = funcname.removeprefix("context.")
        self.contextfunc = True
    else:
        self.funcname = funcname
        self.contextfunc = False
    
  • Argumentativ weiter verbessert
    self.contextfunc = funcname.startswith("context.")
    self.funcname = funcname.removeprefix("context.")
    

cookiejar.py

  • Aktuell
    def strip_quotes(text):
        if text.startswith('"'):
            text = text[1:]
        if text.endswith('"'):
            text = text[:-1]
        return text
    
  • Verbessert
    def strip_quotes(text):
        return text.removeprefix('"').removesuffix('"')
    

test_i18n.py

  • Aktuell
    creationDate = header['POT-Creation-Date']
    
    # peel off the escaped newline at the end of string
    if creationDate.endswith('\\n'):
        creationDate = creationDate[:-len('\\n')]
    
  • Verbessert
    creationDate = header['POT-Creation-Date'].removesuffix('\\n')
    

Es gab viele weitere solche Beispiele in der Standardbibliothek.

Abgelehnte Ideen

Erweiterung der lstrip- und rstrip-APIs

Da lstrip eine Zeichenkette als Argument nimmt, könnte es als Akzeptanz eines Iterables von Zeichenketten der Länge 1 betrachtet werden. Die API könnte daher verallgemeinert werden, um jedes Iterable von Zeichenketten zu akzeptieren, die nacheinander als Präfixe entfernt würden. Obwohl dieses Verhalten konsistent wäre, wäre es für Benutzer nicht offensichtlich, 'foobar'.lstrip(('foo',)) für den gängigen Anwendungsfall eines einzelnen Präfixes aufrufen zu müssen.

Mehrere Kopien eines Präfixes entfernen

Dies ist das Verhalten, das mit der oben genannten Erweiterung der lstrip/rstrip-API konsistent wäre – wiederholtes Anwenden der Funktion, bis das Argument unverändert ist. Dieses Verhalten kann durch Folgendes aus dem vorgeschlagenen Verhalten erreicht werden.

>>> s = 'Foo' * 100 + 'Bar'
>>> prefix = 'Foo'
>>> while s.startswith(prefix): s = s.removeprefix(prefix)
>>> s
'Bar'

Auslösen einer Ausnahme, wenn nicht gefunden

Es gab den Vorschlag, dass s.removeprefix(pre) eine Ausnahme auslösen sollte, wenn not s.startswith(pre). Dies stimmt jedoch nicht mit dem Verhalten und Gefühl anderer Zeichenkettenmethoden überein. Es könnte ein Schlüsselwortargument required=False hinzugefügt werden, aber dies verstößt gegen das KISS-Prinzip.

Akzeptieren eines Tupels von Affixen

Es könnte praktisch sein, das obige Beispiel test_concurrent_futures.py als name.removesuffix(('Mixin', 'Tests', 'Test')) zu schreiben, daher gab es den Vorschlag, dass die neuen Methoden ein Tupel von Zeichenketten als Argument akzeptieren könnten, ähnlich der startswith() API. Innerhalb des Tupels würde nur der erste passende Affix entfernt werden. Dies wurde aus folgenden Gründen abgelehnt.

  • Dieses Verhalten kann überraschend oder visuell verwirrend sein, insbesondere wenn ein Präfix leer ist oder eine Teilzeichenkette eines anderen Präfixes ist, wie in 'FooBar'.removeprefix(('', 'Foo')) == 'FooBar' oder 'FooBar text'.removeprefix(('Foo', 'FooBar ')) == 'Bar text'.
  • Die API für str.replace() akzeptiert nur ein einzelnes Paar von Ersetzungszeichenketten, hat aber die Zeit überdauert, indem sie der Versuchung widerstand, im Angesicht von mehrdeutigen Mehrfachersetzungen zu raten.
  • Es mag in Zukunft einen überzeugenden Anwendungsfall für ein solches Feature geben, aber eine Verallgemeinerung, bevor das grundlegende Feature im wirklichen Leben verwendet wird, wäre leicht permanent falsch zu machen.

Alternative Methodennamen

Mehrere alternative Methodennamen wurden vorgeschlagen. Einige sind unten aufgeführt, zusammen mit Kommentaren, warum sie zugunsten von removeprefix abgelehnt werden sollten (die gleichen Argumente gelten für removesuffix).

  • ltrim, trimprefix, etc.

    „Trim“ macht in anderen Sprachen (z. B. JavaScript, Java, Go, PHP) das, was die strip-Methoden in Python tun.

  • lstrip(string=...)

    Dies würde die Hinzufügung einer neuen Methode vermeiden, aber für ein anderes Verhalten ist es besser, zwei verschiedene Methoden zu haben als eine Methode mit einem Schlüsselwortargument, das das Verhalten auswählt.

  • remove_prefix:

    Alle anderen Methoden der String-API, z. B. str.startswith(), verwenden kleinschreibung anstelle von klein_schreibung_mit_unterstrichen.

  • removeleft, leftremove oder lremove

    Die Deutlichkeit von „Präfix“ wird bevorzugt.

  • cutprefix, deleteprefix, withoutprefix, dropprefix, etc.

    Viele davon wären akzeptabel gewesen, aber „remove“ ist unmissverständlich und passt dazu, wie man das Verhalten „remove the prefix“ auf Englisch beschreiben würde.

  • stripprefix:

    Benutzer könnten davon profitieren, sich daran zu erinnern, dass „strip“ bedeutet, mit Zeichensätzen zu arbeiten, während andere Methoden mit Teilzeichenketten arbeiten. Daher sollte die Wiederverwendung von „strip“ hier vermieden werden.

Wie man das lehrt

Unter den Verwendungen der String-Methoden partition(), startswith() und split() oder der eingebauten Funktionen enumerate() oder zip() ist ein gemeinsames Thema, dass wenn ein Anfänger manuell eine Zeichenkette indiziert oder slicet, er überlegen sollte, ob es eine höherwertige Methode gibt, die besser kommuniziert, *was* der Code tun soll, und nicht nur *wie* der Code es tun soll. Die vorgeschlagenen Methoden removeprefix() und removesuffix() erweitern die höherwertige String-"Werkzeugkiste" und ermöglichen weiterhin diese Art von Skepsis gegenüber manuellem Slicing.

Die Hauptgelegenheit für Benutzerverwirrung wird die Vermischung von lstrip/rstrip mit removeprefix/removesuffix sein. Es kann daher hilfreich sein, die folgenden Unterschiede zwischen den Methoden hervorzuheben (wie es die Dokumentation tun wird).

  • (l/r)strip:
    • Das Argument wird als Zeichensatz interpretiert.
    • Die Zeichen werden wiederholt vom entsprechenden Ende der Zeichenkette entfernt.
  • remove(prefix/suffix):
    • Das Argument wird als ununterbrochene Teilzeichenkette interpretiert.
    • Es wird höchstens eine Kopie des Präfixes/Suffixes entfernt.

Referenzimplementierung

Siehe den Pull Request auf GitHub [1].

Geschichte der wichtigsten Überarbeitungen

  • Version 3: Entfernen des Tupel-Verhaltens.
  • Version 2: Geänderter Name in removeprefix/removesuffix; Unterstützung für Tupel als Argumente hinzugefügt
  • Version 1: Erster Entwurf mit cutprefix/cutsuffix

Referenzen


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

Zuletzt geändert: 2025-02-01 08:55:40 GMT