Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Jumlah Jenis dalam Python

Jumlah Jenis dalam Python

Mary-Kate Olsen
Mary-Kate Olsenasal
2024-10-19 06:18:02652semak imbas

Sum Types in Python

Python ialah bahasa yang menarik. Walau bagaimanapun, apabila bekerja dalam Python saya sering mendapati diri saya kehilangan sokongan terbina dalam untuk jenis jumlah. Bahasa seperti Haskell dan Rust menjadikan perkara seperti ini begitu mudah:

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

Walaupun Python tidak menyokong pembinaan seperti ini di luar kotak, kita akan melihat bahawa jenis seperti Expr masih boleh (dan mudah) untuk dinyatakan. Tambahan pula, kita boleh mencipta penghias yang mengendalikan semua boilerplate yang jahat untuk kita. Hasilnya tidak terlalu berbeza daripada contoh Haskell di atas:

# 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

Mewakili Jenis Jumlah

Kami akan mewakili jenis jumlah menggunakan "kesatuan berteg". Ini mudah untuk grok dengan contoh:

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

Setiap varian ialah contoh kelas yang sama (dalam kes ini Expr). Setiap satu mengandungi "teg" yang menunjukkan varian itu, bersama-sama dengan data khusus untuknya.

Cara paling asas untuk menggunakan Expr ialah dengan rantai if-else:

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)

Walau bagaimanapun, ini mempunyai beberapa kelemahan:

  • Rantai if-else yang sama diulangi di mana-mana Expr digunakan.
  • Menukar nilai teg—katakan daripada "menyala" kepada "harfiah"—berhenti kod sedia ada.
  • Penggunaan jenis jumlah memerlukan mengetahui butiran pelaksanaan (iaitu teg dan nama medan yang digunakan oleh setiap varian).

Melaksanakan perlawanan

Kita boleh mengelakkan semua isu ini dengan mendedahkan satu kaedah padanan awam yang digunakan untuk menggunakan jenis jumlah:

class Expr:
    # ...
    def match(self, handlers):
        # ...

Tetapi pertama sekali kita perlu membuat varian yang berbeza sedikit lebih seragam. Daripada menyimpan datanya dalam pelbagai medan, setiap varian kini akan menyimpannya dalam tuple bernama 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

Ini membolehkan kami melaksanakan padanan:

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}")

Dalam satu masa, kami telah menyelesaikan semua masalah yang dinyatakan di atas! Sebagai contoh lain, dan untuk perubahan pemandangan, berikut ialah jenis Rust's Option yang ditranskripsikan dalam fesyen ini:

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)

Sebagai faedah kualiti hidup yang kecil, kami boleh menyokong kad bebas khas atau pengendali "catchall" dalam perlawanan, ditunjukkan dengan garis bawah (_):

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}")

Ini membolehkan kami menggunakan padanan seperti:

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

Melaksanakan enum

Seperti yang ditunjukkan oleh kelas Option, banyak kod yang diperlukan untuk mencipta jenis jumlah mengikut corak yang sama:

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}")

Daripada menulis ini sendiri, mari kita tulis penghias untuk menjana kaedah ini berdasarkan beberapa huraian varian.

def enum(**variants):
    pass

Perihalan macam mana? Perkara yang paling mudah ialah membekalkan senarai nama varian, tetapi kita boleh melakukan lebih baik sedikit dengan turut menyediakan jenis hujah yang kita jangkakan. Kami akan menggunakan enum untuk meningkatkan kelas Opsyen kami secara automatik seperti ini:

# 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

Struktur asas enum kelihatan seperti ini:

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

Ia adalah fungsi yang mengembalikan fungsi satu lagi, yang akan dipanggil dengan kelas yang kami pertingkatkan sebagai satu-satunya hujahnya. Dalam peningkatan kami akan melampirkan kaedah untuk membina setiap varian, bersama-sama dengan padanan.

Pertama, padankan, kerana ia hanya salinan 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

Menambah kaedah untuk membina setiap varian hanya terlibat sedikit sahaja. Kami mengulangi kamus varian, mentakrifkan kaedah untuk setiap entri:

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

di mana make_constructor mencipta fungsi pembina untuk varian dengan teg (dan nama) dan tanda "tandatangan jenis":

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)

Berikut ialah takrifan penuh enum untuk rujukan.

Ciri-ciri Bonus

Lebih Banyak Kaedah Dunder

Kami boleh meningkatkan kelas jumlah kami dengan mudah dengan kaedah __repr__ dan __eq__:

class Expr:
    # ...
    def match(self, handlers):
        # ...

Dengan peningkatan yang dipertingkatkan dalam fesyen ini, kami boleh menentukan Pilihan dengan cruft minimum:

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

Definisi Rekursif

Malangnya, enum belum (belum) mencapai tugas untuk mentakrifkan Expr:

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}")

Kami menggunakan Expr kelas sebelum ia ditakrifkan. Penyelesaian mudah di sini ialah dengan hanya memanggil penghias selepas menentukan kelas:

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)

Tetapi ada perubahan mudah yang boleh kita lakukan untuk menyokong ini: benarkan "tandatangan" menjadi fungsi yang mengembalikan tuple:

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}")

Semua ini memerlukan perubahan kecil dalam make_constructor:

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

Kesimpulan

Walaupun ia mungkin berguna, penghias enum baharu kami yang mewah bukan tanpa kekurangannya. Yang paling ketara ialah ketidakupayaan untuk melakukan apa-apa jenis padanan corak "bersarang". Dalam Rust, kita boleh melakukan perkara seperti ini:

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}")

Tetapi kami terpaksa melakukan perlawanan berganda untuk mencapai keputusan yang sama:

def enum(**variants):
    pass

Maksudnya, kes seperti ini kelihatan agak jarang berlaku.

Satu lagi kelemahan ialah padanan memerlukan membina dan memanggil banyak fungsi. Ini bermakna ia berkemungkinan jauh lebih perlahan daripada rantai if-else yang setara. Walau bagaimanapun, peraturan biasa digunakan di sini: gunakan enum jika anda menyukai faedah ergonomiknya dan gantikannya dengan kod "dijana" jika ia terlalu perlahan.

Atas ialah kandungan terperinci Jumlah Jenis dalam Python. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn