Heim > Backend-Entwicklung > Python-Tutorial > Summentypen in Python

Summentypen in Python

Mary-Kate Olsen
Freigeben: 2024-10-19 06:18:02
Original
770 Leute haben es durchsucht

Sum Types in Python

Python ist eine schöne Sprache. Wenn ich jedoch in Python arbeite, vermisse ich häufig die integrierte Unterstützung für Summentypen. Sprachen wie Haskell und Rust machen so etwas so einfach:

data Op = Add | Sub | Mul
  deriving (Show)

data Expr
  = Lit Integer
  | BinOp Op Expr Expr
  deriving (Show)

val :: Expr -> Integer
val (Lit val) = val
val (BinOp op lhs rhs) =
  let x = val lhs
      y = val rhs
   in apply op x y

apply :: Op -> Integer -> Integer -> Integer
apply Add x y = x + y
apply Sub x y = x - y
apply Mul x y = x * y

val (BinOp Add (BinOp Mul (Lit 2) (Lit 3)) (Lit 4))
-- => 10
Nach dem Login kopieren
Nach dem Login kopieren

Obwohl Python diese Art der Konstruktion nicht standardmäßig unterstützt, werden wir sehen, dass Typen wie Expr dennoch möglich (und einfach) auszudrücken sind. Darüber hinaus können wir einen Dekorateur erstellen, der die ganze fiese Standardarbeit für uns erledigt. Das Ergebnis unterscheidet sich nicht allzu sehr vom Haskell-Beispiel oben:

# The `enum` decorator adds methods for constructing and matching on the
# different variants:
@enum(add=(), sub=(), mul=())
class Op:
    def apply(self, x, y):
        return self.match(
            add=lambda: x + y,
            sub=lambda: x - y,
            mul=lambda: x * y,
        )


# Recursive sum types are also supported:
@enum(lit=(int,), bin_op=lambda: (Op, Expr, Expr))
class Expr:
    def val(self):
        return self.match(
            lit=lambda value: value,
            bin_op=lambda op, lhs, rhs: op.apply(lhs.val(), rhs.val()),
        )


Expr.bin_op(
    Op.add(),
    Expr.bin_op(Op.mul(), Expr.lit(2), Expr.lit(3)),
    Expr.lit(4)
).val()
# => 10
Nach dem Login kopieren
Nach dem Login kopieren

Darstellung von Summentypen

Wir stellen Summentypen mithilfe einer „getaggten Union“ dar. Das lässt sich leicht anhand eines Beispiels erkennen:

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.value = value
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.op = op
        e.lhs = lhs
        e.rhs = rhs
        return e
Nach dem Login kopieren
Nach dem Login kopieren

Jede Variante ist eine Instanz derselben Klasse (in diesem Fall Expr). Jede enthält ein „Tag“, das angibt, um welche Variante es sich handelt, zusammen mit den spezifischen Daten dafür.

Die einfachste Art, einen Ausdruck zu verwenden, ist mit einer if-else-Kette:

class Expr:
    # ...
    def val(self):
        if self.tag == "lit":
            return self.value
        elif self.tag == "bin_op":
            x = self.lhs.val()
            y = self.rhs.val()
            return self.op.apply(x, y)
Nach dem Login kopieren
Nach dem Login kopieren

Dies hat jedoch einige Nachteile:

  • Die gleiche if-else-Kette wird überall dort wiederholt, wo ein Ausdruck verwendet wird.
  • Das Ändern des Tag-Werts – beispielsweise von „lit“ in „literal“ – führt zu Problemen Vorhandener Code.
  • Um Summentypen zu konsumieren, müssen Sie die Implementierungsdetails kennen (d. h. das Tag und die Namen der Felder, die von jeder Variante verwendet werden).

Übereinstimmung umsetzen

Wir können all diese Probleme vermeiden, indem wir eine einzige öffentliche Match-Methode bereitstellen, die zum Konsumieren von Summentypen verwendet wird:

class Expr:
    # ...
    def match(self, handlers):
        # ...
Nach dem Login kopieren
Nach dem Login kopieren

Aber zunächst müssen wir die verschiedenen Varianten noch etwas einheitlicher gestalten. Anstatt ihre Daten in verschiedenen Feldern zu speichern, speichert jede Variante sie jetzt in einem Tupel mit dem Namen data:

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.data = (value,)
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.data = (op, lhs, rhs)
        return e
Nach dem Login kopieren
Nach dem Login kopieren

Dadurch können wir match:
implementieren

class Expr:
    # ...
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
Nach dem Login kopieren
Nach dem Login kopieren

Auf einen Schlag haben wir alle oben genannten Probleme gelöst! Als weiteres Beispiel und zur Abwechslung ist hier der Option-Typ von Rust auf diese Weise transkribiert:

class Option:
    def some(x):
        o = Option()
        o.tag = "some"
        o.data = (x,)
        return o

    def none():
        o = Option()
        o.tag = "none"
        o.data = ()
        return o

    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")

    def __repr__(self):
        return self.match(
            some=lambda x: f"Option.some({repr(x)})",
            none=lambda: "Option.none()",
        )

    def __eq__(self, other):
        if not isinstance(other, Option):
            return NotImplemented
        return self.tag == other.tag and self.data == other.data

    def map(self, fn):
        return self.match(
            some=lambda x: Option.some(fn(x)),
            none=lambda: Option.none()
        )

Option.some(2).map(lambda x: x**2)
# => Option.some(4)
Nach dem Login kopieren
Nach dem Login kopieren

Als kleinen Vorteil für die Lebensqualität können wir einen speziellen Wildcard- oder „Catchall“-Handler im Match unterstützen, der durch einen Unterstrich (_) gekennzeichnet ist:

def match(self, **handlers):
    if self.tag in handlers:
        return handlers[self.tag](*self.data)
    elif "_" in handlers:
        return handlers["_"]()
    else:
        raise RuntimeError(f"missing handler for {self.tag}")
Nach dem Login kopieren
Nach dem Login kopieren

Dies ermöglicht uns die Verwendung von Übereinstimmungen wie:

def map(self, fn):
    return self.match(
        some=lambda x: Option.some(fn(x)),
        _=lambda: Option.none(),
    )
Nach dem Login kopieren
Nach dem Login kopieren

Enumeration implementieren

Wie die Option-Klasse zeigt, folgt ein Großteil des Codes, der zum Erstellen von Summentypen benötigt wird, demselben Muster:

class Foo:
    # For each variant:
    def my_variant(bar, quux):
        # Construct an instance of the class:
        f = Foo()
        # Give the instance a distinct tag:
        f.tag = "my_variant"
        # Save the values we received:
        f.data = (bar, quux)
        return f

    # This is always the same:
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        elif "_" in handlers:
            return handlers["_"]()
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
Nach dem Login kopieren
Nach dem Login kopieren

Anstatt dies selbst zu schreiben, schreiben wir einen Dekorateur, der diese Methoden basierend auf einer Beschreibung der Varianten generiert.

def enum(**variants):
    pass
Nach dem Login kopieren
Nach dem Login kopieren

Was für eine Beschreibung? Am einfachsten wäre es, eine Liste mit Variantennamen bereitzustellen, aber wir können es noch etwas besser machen, indem wir auch die Arten von Argumenten angeben, die wir erwarten. Wir würden enum verwenden, um unsere Option-Klasse automatisch wie folgt zu erweitern:

# Add two variants:
# - One named `some` that expects a single argument of any type.
# - One named `none` that expects no arguments.
@enum(some=(object,), none=())
class Option:
    pass
Nach dem Login kopieren

Die Grundstruktur von enum sieht folgendermaßen aus:

data Op = Add | Sub | Mul
  deriving (Show)

data Expr
  = Lit Integer
  | BinOp Op Expr Expr
  deriving (Show)

val :: Expr -> Integer
val (Lit val) = val
val (BinOp op lhs rhs) =
  let x = val lhs
      y = val rhs
   in apply op x y

apply :: Op -> Integer -> Integer -> Integer
apply Add x y = x + y
apply Sub x y = x - y
apply Mul x y = x * y

val (BinOp Add (BinOp Mul (Lit 2) (Lit 3)) (Lit 4))
-- => 10
Nach dem Login kopieren
Nach dem Login kopieren

Es ist eine Funktion, die eine andere Funktion zurückgibt, die mit der Klasse, die wir erweitern, als einziges Argument aufgerufen wird. Innerhalb von „Enhance“ fügen wir Methoden zum Erstellen jeder Variante sowie Match hinzu.

Erstmals zusammenpassen, denn es handelt sich nur um eine Kopie der Pasta:

# The `enum` decorator adds methods for constructing and matching on the
# different variants:
@enum(add=(), sub=(), mul=())
class Op:
    def apply(self, x, y):
        return self.match(
            add=lambda: x + y,
            sub=lambda: x - y,
            mul=lambda: x * y,
        )


# Recursive sum types are also supported:
@enum(lit=(int,), bin_op=lambda: (Op, Expr, Expr))
class Expr:
    def val(self):
        return self.match(
            lit=lambda value: value,
            bin_op=lambda op, lhs, rhs: op.apply(lhs.val(), rhs.val()),
        )


Expr.bin_op(
    Op.add(),
    Expr.bin_op(Op.mul(), Expr.lit(2), Expr.lit(3)),
    Expr.lit(4)
).val()
# => 10
Nach dem Login kopieren
Nach dem Login kopieren

Das Hinzufügen von Methoden zum Erstellen jeder Variante ist nur geringfügig aufwändiger. Wir durchlaufen das Variantenwörterbuch und definieren eine Methode für jeden Eintrag:

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.value = value
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.op = op
        e.lhs = lhs
        e.rhs = rhs
        return e
Nach dem Login kopieren
Nach dem Login kopieren

wobei make_constructor eine Konstruktorfunktion für eine Variante mit Tag (und Name)-Tag und „Typsignatur“-Sig erstellt:

class Expr:
    # ...
    def val(self):
        if self.tag == "lit":
            return self.value
        elif self.tag == "bin_op":
            x = self.lhs.val()
            y = self.rhs.val()
            return self.op.apply(x, y)
Nach dem Login kopieren
Nach dem Login kopieren

Hier ist die vollständige Definition von enum als Referenz.

Bonusfunktionen

Weitere Dunder-Methoden

Wir können unsere Summenklassen ganz einfach mit den Methoden __repr__ und __eq__ erweitern:

class Expr:
    # ...
    def match(self, handlers):
        # ...
Nach dem Login kopieren
Nach dem Login kopieren

Mit der auf diese Weise verbesserten Verbesserung können wir Option mit minimalem Aufwand definieren:

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.data = (value,)
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.data = (op, lhs, rhs)
        return e
Nach dem Login kopieren
Nach dem Login kopieren

Rekursive Definitionen

Leider ist enum der Aufgabe, Expr: zu definieren, (noch) nicht gewachsen

class Expr:
    # ...
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
Nach dem Login kopieren
Nach dem Login kopieren

Wir verwenden die Klasse Expr bevor sie definiert wurde. Eine einfache Lösung besteht darin, nach der Definition der Klasse einfach den Dekorator aufzurufen:

class Option:
    def some(x):
        o = Option()
        o.tag = "some"
        o.data = (x,)
        return o

    def none():
        o = Option()
        o.tag = "none"
        o.data = ()
        return o

    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")

    def __repr__(self):
        return self.match(
            some=lambda x: f"Option.some({repr(x)})",
            none=lambda: "Option.none()",
        )

    def __eq__(self, other):
        if not isinstance(other, Option):
            return NotImplemented
        return self.tag == other.tag and self.data == other.data

    def map(self, fn):
        return self.match(
            some=lambda x: Option.some(fn(x)),
            none=lambda: Option.none()
        )

Option.some(2).map(lambda x: x**2)
# => Option.some(4)
Nach dem Login kopieren
Nach dem Login kopieren

Aber wir können eine einfache Änderung vornehmen, um dies zu unterstützen: Erlauben Sie, dass eine „Signatur“ eine Funktion ist, die ein Tupel zurückgibt:

def match(self, **handlers):
    if self.tag in handlers:
        return handlers[self.tag](*self.data)
    elif "_" in handlers:
        return handlers["_"]()
    else:
        raise RuntimeError(f"missing handler for {self.tag}")
Nach dem Login kopieren
Nach dem Login kopieren

Alles was dazu erforderlich ist, ist eine kleine Änderung in make_constructor:

def map(self, fn):
    return self.match(
        some=lambda x: Option.some(fn(x)),
        _=lambda: Option.none(),
    )
Nach dem Login kopieren
Nach dem Login kopieren

Abschluss

So nützlich er auch sein mag, unser schicker neuer Enum-Dekorator ist nicht ohne Mängel. Am offensichtlichsten ist die Unfähigkeit, irgendeine Art von „verschachteltem“ Mustervergleich durchzuführen. In Rust können wir Dinge wie diese tun:

class Foo:
    # For each variant:
    def my_variant(bar, quux):
        # Construct an instance of the class:
        f = Foo()
        # Give the instance a distinct tag:
        f.tag = "my_variant"
        # Save the values we received:
        f.data = (bar, quux)
        return f

    # This is always the same:
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        elif "_" in handlers:
            return handlers["_"]()
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
Nach dem Login kopieren
Nach dem Login kopieren

Aber wir sind gezwungen, ein Doppelspiel durchzuführen, um das gleiche Ergebnis zu erzielen:

def enum(**variants):
    pass
Nach dem Login kopieren
Nach dem Login kopieren

Allerdings scheinen solche Fälle relativ selten zu sein.

Ein weiterer Nachteil besteht darin, dass für die Übereinstimmung viele Funktionen erstellt und aufgerufen werden müssen. Das bedeutet, dass sie wahrscheinlich viel langsamer ist als die entsprechende if-else-Kette. Hier gilt jedoch die übliche Faustregel: Verwenden Sie Enum, wenn Ihnen die ergonomischen Vorteile gefallen, und ersetzen Sie es durch den „generierten“ Code, wenn es zu langsam ist.

Das obige ist der detaillierte Inhalt vonSummentypen in Python. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage