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

Jumlah Jenis dalam Python

Mary-Kate Olsen
Lepaskan: 2024-10-19 06:18:02
asal
746 orang telah melayarinya

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
Salin selepas log masuk
Salin selepas log masuk

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
Salin selepas log masuk
Salin selepas log masuk

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
Salin selepas log masuk
Salin selepas log masuk

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)
Salin selepas log masuk
Salin selepas log masuk

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):
        # ...
Salin selepas log masuk
Salin selepas log masuk

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
Salin selepas log masuk
Salin selepas log masuk

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}")
Salin selepas log masuk
Salin selepas log masuk

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)
Salin selepas log masuk
Salin selepas log masuk

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}")
Salin selepas log masuk
Salin selepas log masuk

Ini membolehkan kami menggunakan padanan seperti:

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

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}")
Salin selepas log masuk
Salin selepas log masuk

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

def enum(**variants):
    pass
Salin selepas log masuk
Salin selepas log masuk

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
Salin selepas log masuk

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
Salin selepas log masuk
Salin selepas log masuk

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
Salin selepas log masuk
Salin selepas log masuk

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
Salin selepas log masuk
Salin selepas log masuk

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)
Salin selepas log masuk
Salin selepas log masuk

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):
        # ...
Salin selepas log masuk
Salin selepas log masuk

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
Salin selepas log masuk
Salin selepas log masuk

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}")
Salin selepas log masuk
Salin selepas log masuk

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)
Salin selepas log masuk
Salin selepas log masuk

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}")
Salin selepas log masuk
Salin selepas log masuk

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(),
    )
Salin selepas log masuk
Salin selepas log masuk

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}")
Salin selepas log masuk
Salin selepas log masuk

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

def enum(**variants):
    pass
Salin selepas log masuk
Salin selepas log masuk

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!

sumber:dev.to
Kenyataan Laman Web ini
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
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan