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

Python Enhancement Proposals

PEP 712 – Hinzufügen eines „converter“-Parameters zu dataclasses.field

Autor:
Joshua Cannon <joshdcannon at gmail.com>
Sponsor:
Eric V. Smith <eric at trueblade.com>
Discussions-To:
Discourse thread
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
01-Jan-2023
Python-Version:
3.13
Post-History:
27-Dez-2022, 19-Jan-2023, 23-Apr-2023
Resolution:
Discourse-Nachricht

Inhaltsverzeichnis

Ablehnungsbescheid

Die Gründe für die Ablehnung durch den Steering Council 2024 umfassen:

  • Wir fanden keine Beweise für einen starken Konsens, dass diese Funktion in der Standardbibliothek benötigt wurde, obwohl einige Befürworter sie zur Reduzierung ihrer Abhängigkeit von Drittanbieterpaketen befürworteten. Für diejenigen, die eine solche Funktionalität benötigen, halten wir bestehende Drittanbieterbibliotheken wie attrs und Pydantic (auf die in PEP verwiesen wird) für akzeptable Alternativen.
  • Diese Funktion scheint uns wie eine Ansammlung von dem, was als mehr Ballast in der Standardbibliothek betrachtet werden könnte, und führt uns immer weiter weg von den „einfachen“ Anwendungsfällen, für die Dataklassen ideal sind.
  • Das Lesen des Abschnitts „How to Teach This“ der PEP lässt uns zweifeln, dass die Fallstricke und Tücken erheblich sind, wobei erhöhte Verwirrung und Komplexität die potenziellen Vorteile überwiegen.
  • Die PEP scheint sich mehr auf die Unterstützung von Typüberprüfern als von Personen zu konzentrieren, die die Bibliothek verwenden.

Zusammenfassung

PEP 557 fügte dataclasses zur Python-Standardbibliothek hinzu. PEP 681 fügte dataclass_transform() hinzu, um Typüberprüfern das Verständnis mehrerer gängiger dataklassenähnlicher Bibliotheken zu erleichtern, wie z. B. attrs, Pydantic und Object Relational Mapper (ORM)-Pakete wie SQLAlchemy und Django.

Eine häufige Funktion, die andere Bibliotheken gegenüber der Standardimplementierung bieten, ist die Möglichkeit für die Bibliothek, an das Objekt übergebene Argumente bei der Initialisierung in die für jedes Feld erwarteten Typen mithilfe einer vom Benutzer bereitgestellten Konvertierungsfunktion zu konvertieren.

Daher fügt diese PEP einen Parameter converter zur Funktion dataclasses.field() hinzu (zusammen mit den erforderlichen Änderungen an dataclasses.Field und dataclass_transform()), um die Funktion anzugeben, die zur Konvertierung des Eingabewerts für jedes Feld in die zu speichernde Darstellung in der Dataklasse verwendet wird.

Motivation

Es gibt keine bestehende, standardmäßige Möglichkeit für dataclasses oder dataklassenähnliche Drittanbieterbibliotheken, Argumentkonvertierung auf eine typprüfbare Weise zu unterstützen. Um diese Einschränkung zu umgehen, sind Bibliotheksautoren/Benutzer gezwungen, Folgendes zu wählen:

  • Opt-in für ein benutzerdefiniertes Mypy-Plugin. Diese Plugins helfen Mypy, die Konvertierungssemantik zu verstehen, andere Werkzeuge jedoch nicht.
  • Verantwortung für die Konvertierung auf den Aufrufer des Dataklassenkonstruktors verlagern. Dies kann die Erstellung bestimmter Dataklassen unnötig wortreich und repetitiv machen.
  • Bereitstellung einer benutzerdefinierten __init__-Methode, die „breitere“ Parametertypen deklariert und sie beim Zuweisen des entsprechenden Attributs konvertiert. Dies dupliziert nicht nur die Typ-Annotationen zwischen dem Konverter und __init__, sondern schließt den Benutzer auch von vielen Funktionen aus, die dataclasses bietet.
  • Bereitstellung einer benutzerdefinierten __init__-Methode, jedoch ohne aussagekräftige Typ-Annotationen für die Parametertypen, die eine Konvertierung erfordern.

Keine dieser Optionen ist ideal.

Begründung

Das Hinzufügen von Argumentkonvertierungssemantiken ist nützlich und vorteilhaft genug, dass die meisten dataklassenähnlichen Bibliotheken Unterstützung dafür bieten. Das Hinzufügen dieser Funktion zur Standardbibliothek bedeutet, dass mehr Benutzer diese Vorteile nutzen können, ohne dass zusätzliche Bibliotheken erforderlich sind. Darüber hinaus können Drittanbieterbibliotheken Typüberprüfer auf ihre eigenen Konvertierungssemantiken hinweisen, indem sie zusätzliche Unterstützung in dataclass_transform() hinzufügen, was bedeutet, dass auch Benutzer dieser Bibliotheken davon profitieren.

Spezifikation

Neuer Parameter converter

Diese Spezifikation führt einen neuen Parameter namens converter in die Funktion dataclasses.field() ein. Wenn angegeben, stellt er einen einzelargumentigen aufrufbaren Wert dar, der zur Konvertierung aller Werte bei der Zuweisung zum zugehörigen Attribut verwendet wird.

Bei eingefrorenen Dataklassen wird der Konverter nur innerhalb einer von dataclass synthetisierten __init__-Methode beim Zuweisen des Attributs verwendet. Bei nicht eingefrorenen Dataklassen wird der Konverter für alle Attributzuweisungen verwendet (z. B. obj.attr = value), einschließlich der Zuweisung von Standardwerten.

Der Konverter wird beim Lesen von Attributen nicht verwendet, da die Attribute bereits konvertiert worden sein sollten.

Das Hinzufügen dieses Parameters impliziert auch die folgenden Änderungen:

Beispiel

def str_or_none(x: Any) -> str | None:
  return str(x) if x is not None else None

@dataclasses.dataclass
class InventoryItem:
    # `converter` as a type (including a GenericAlias).
    id: int = dataclasses.field(converter=int)
    skus: tuple[int, ...] = dataclasses.field(converter=tuple[int, ...])
    # `converter` as a callable.
    vendor: str | None = dataclasses.field(converter=str_or_none))
    names: tuple[str, ...] = dataclasses.field(
      converter=lambda names: tuple(map(str.lower, names))
    )  # Note that lambdas are supported, but discouraged as they are untyped.

    # The default value is also converted; therefore the following is not a
    # type error.
    stock_image_path: pathlib.PurePosixPath = dataclasses.field(
      converter=pathlib.PurePosixPath, default="assets/unknown.png"
    )

    # Default value conversion extends to `default_factory`;
    # therefore the following is also not a type error.
    shelves: tuple = dataclasses.field(
      converter=tuple, default_factory=list
    )

item1 = InventoryItem(
  "1",
  [234, 765],
  None,
  ["PYTHON PLUSHIE", "FLUFFY SNAKE"]
)
# item1's repr would be (with added newlines for readability):
#   InventoryItem(
#     id=1,
#     skus=(234, 765),
#     vendor=None,
#     names=('PYTHON PLUSHIE', 'FLUFFY SNAKE'),
#     stock_image_path=PurePosixPath('assets/unknown.png'),
#     shelves=()
#   )

# Attribute assignment also participates in conversion.
item1.skus = [555]
# item1's skus attribute is now (555,).

Auswirkungen auf die Typisierung

Ein converter muss ein aufrufbarer Wert sein, der ein einzelnes positionelles Argument akzeptiert, und der Parametertyp, der diesem positionellen Argument entspricht, stellt den Typ des synthetisierten __init__-Parameters dar, der mit dem Feld verknüpft ist.

Anders ausgedrückt: Das für den Parameter converter übergebene Argument muss mit Callable[[T], X] kompatibel sein, wobei T der Eingabetyp für den Konverter und X der Ausgabetyp des Konverters ist.

Typüberprüfung von default und default_factory

Da Standardwerte bedingungslos mit converter konvertiert werden, sollte, wenn ein Argument für converter zusammen mit entweder default oder default_factory angegeben wird, der Typ des Standardwerts (das default-Argument, falls vorhanden, andernfalls der Rückgabewert von default_factory) anhand des Typs des einzelnen Arguments des converter-Aufrufers überprüft werden.

Rückgabetyp des Konverters

Der Rückgabetyp des Aufrufers muss ein Typ sein, der mit dem deklarierten Typ des Feldes kompatibel ist. Dies schließt den Typ des Feldes exakt ein, kann aber auch ein Typ sein, der spezialisierter ist (z. B. ein Konverter, der eine list[int] für ein Feld zurückgibt, das als list annotiert ist, oder ein Konverter, der eine int für ein Feld zurückgibt, das als int | str annotiert ist).

Indirektion von zulässigen Argumenttypen

Ein Nachteil, der sich aus dieser PEP ergibt, ist, dass nicht sofort ersichtlich ist, welche Argumenttypen in der __init__-Methode der Dataklasse und bei der Attributzuweisung zulässig sind. Die zulässigen Typen werden durch den Konverter definiert.

Dies gilt beim Lesen von Code aus der Quelle. Hilfsmittel für die Typisierung wie typing.reveal_type und „IntelliSense“ in einer IDE sollten es jedoch leicht machen, genau zu wissen, welche Typen ohne das Lesen von Quellcode zulässig sind.

Abwärtskompatibilität

Diese Änderungen führen keine Kompatibilitätsprobleme ein, da sie nur opt-in neue Funktionen einführen.

Sicherheitsimplikationen

Es gibt keine direkten Sicherheitsbedenken bei diesen Änderungen.

Wie man das lehrt

Dokumentation und Beispiele, die den neuen Parameter und das Verhalten erklären, werden den relevanten Abschnitten der Dokumentationsseite (hauptsächlich bei dataclasses) hinzugefügt und vom Dokument *What’s New* verlinkt.

Die hinzugefügte Dokumentation/Beispiele decken auch die „häufigen Fallstricke“ ab, auf die Benutzer von Konvertern wahrscheinlich stoßen werden. Solche Fallstricke umfassen:

  • Notwendigkeit, None/Sentinel-Werte zu verarbeiten.
  • Notwendigkeit, Werte zu verarbeiten, die bereits vom richtigen Typ sind.
  • Vermeidung von Lambdas für Konverter, da der Typ des synthetisierten __init__-Parameters Any wird.
  • Vergessen, Werte in den Bodies von benutzerdefinierten __init__-Methoden in eingefrorenen Dataklassen zu konvertieren.
  • Vergessen, Werte in den Bodies von benutzerdefinierten __setattr__-Methoden in nicht eingefrorenen Dataklassen zu konvertieren.

Zusätzlich sollten potenziell verwirrende Pattern-Matching-Semantiken abgedeckt werden.

@dataclass
class Point:
    x: int = field(converter=int)
    y: int

match Point(x="0", y=0):
    case Point(x="0", y=0):  # Won't be matched
        ...
    case Point():  # Will be matched
        ...
    case _:
        ...

Es ist jedoch anzumerken, dass dieses Verhalten für jeden Typ gilt, der im Initialisierer eine Konvertierung durchführt, und Typüberprüfer sollten in der Lage sein, diesen Fallstrick zu erkennen.

match int("0"):
  case int("0"):  # Won't be matched
      ...
  case _:  # Will be matched
      ...

Referenzimplementierung

Die attrs-Bibliothek enthält bereits einen converter-Parameter mit denselben Konvertersemantiken (Konvertierung im Initialisierer und bei Attributzuweisungen), wenn der Dekorator @define verwendet wird.

CPython-Unterstützung wird in einem Branch im Fork des Autors implementiert.

Abgelehnte Ideen

Nur „converter“ zu typing.dataclass_transforms field_specifiers hinzufügen

Die Idee, diese Ergänzung auf dataclass_transform() zu beschränken, wurde kurz auf Typing-SIG diskutiert, wo vorgeschlagen wurde, dies auf dataclasses allgemeiner auszuweiten.

Darüber hinaus stellt die Aufnahme in dataclasses sicher, dass jeder die Vorteile nutzen kann, ohne zusätzliche Bibliotheken zu benötigen.

Keine Konvertierung von Standardwerten

Es gibt Vor- und Nachteile sowohl bei der Konvertierung als auch bei der Nicht-Konvertierung von Standardwerten. Wenn Standardwerte unverändert bleiben, können Typüberprüfer und Dataklassenautoren davon ausgehen, dass der Typ des Standardwerts mit dem Typ des Feldes übereinstimmt. Die Konvertierung von Standardwerten hat jedoch drei große Vorteile:

  1. Konsistenz. Die bedingungslose Konvertierung aller Werte, die dem Attribut zugewiesen werden, beinhaltet weniger „Sonderregeln“, die sich Benutzer merken müssen.
  2. Einfachere Standardwerte. Wenn der Standardwert den gleichen Typ wie benutzerseitig bereitgestellte Werte haben darf, erhalten Dataklassenautoren die gleichen Annehmlichkeiten wie ihre Aufrufer.
  3. Kompatibilität mit attrs. Attrs verwendet bedingungslos den Konverter zur Konvertierung von Standardwerten.

Automatische Konvertierung unter Verwendung des Feldes

Eine Idee könnte sein, den angegebenen Typ des Feldes (z. B. str oder int) als Konverter für jedes bereitgestellte Argument zu verwenden. Pydantics Datenkonvertierung hat Semantiken, die diesem Ansatz ähneln.

Dies funktioniert gut für recht einfache Typen, führt jedoch zu Mehrdeutigkeit beim erwarteten Verhalten für komplexe Typen wie Generics. Z. B. Für tuple[int, ...] ist unklar, ob der Konverter einfach einen Iterable in ein Tupel konvertieren soll, oder ob er zusätzlich jeden Elementtyp zu int konvertieren soll. Oder für int | None, was nicht aufrufbar ist.

Ableitung des Attributtyps aus dem Rückgabetyp des Konverters

Eine andere Idee wäre, dem Benutzer zu erlauben, die Typ-Annotation des Attributs wegzulassen, wenn ein field mit einem converter-Argument bereitgestellt wird. Obwohl dies die übliche Wiederholung, die diese PEP einführt (z. B. x: str = field(converter=str)), reduzieren würde, ist nicht klar, wie dies am besten unterstützt werden kann, während die aktuellen Dataklassen-Semantiken beibehalten werden (nämlich, dass die Reihenfolge der Attribute für Dinge wie die synthetisierte __init__ oder dataclasses.fields beibehalten wird). Dies liegt daran, dass es in Python (heute) keine einfache Möglichkeit gibt, die reinen Annotationsattribute, die mit nicht annotierten Attributen vermischt sind, in der Reihenfolge abzurufen, in der sie definiert wurden.

Eine Sentinel-Annotation könnte angewendet werden (z. B. x: FromConverter = ...), dies bricht jedoch eine grundlegende Annahme von Typ-Annotationen.

Schließlich ist dies machbar, wenn *alle* Felder (einschließlich derer ohne Konverter) dataclasses.field zugewiesen werden, wodurch der eigene Namensraum der Klasse die Reihenfolge vorgibt. Dies tauscht jedoch die Wiederholung von Typ+Konverter gegen die Wiederholung von Feldzuweisungen. Das Endergebnis ist kein Gewinn oder Verlust an Wiederholung, aber mit der zusätzlichen Komplexität der Dataklassen-Semantiken.

Diese PEP schlägt nicht vor, dass dies nicht geschehen kann oder sollte. Nur dass es nicht in dieser PEP enthalten ist.


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

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