>  기사  >  백엔드 개발  >  Python의 합계 유형

Python의 합계 유형

Mary-Kate Olsen
Mary-Kate Olsen원래의
2024-10-19 06:18:02649검색

Sum Types in Python

파이썬은 사랑스러운 언어입니다. 그러나 Python으로 작업할 때 합계 유형에 대한 내장 지원이 누락되는 경우가 많습니다. Haskell 및 Rust와 같은 언어를 사용하면 이러한 작업이 매우 쉬워집니다.

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

Python은 기본적으로 이러한 종류의 구성을 지원하지 않지만 Expr과 같은 유형은 그럼에도 불구하고 표현이 가능하고 쉽다는 것을 알게 될 것입니다. 게다가 우리는 모든 불쾌한 상용구를 처리하는 데코레이터를 만들 수 있습니다. 결과는 위의 Haskell 예제와 크게 다르지 않습니다:

# 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

합계 유형 표현

"태그 합집합"을 사용하여 합계 유형을 표현하겠습니다. 예를 들어 이해하기 쉽습니다.

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

각 변형은 동일한 클래스(이 경우 Expr)의 인스턴스입니다. 각각에는 해당 변형과 관련된 데이터를 나타내는 "태그"가 포함되어 있습니다.

Expr을 사용하는 가장 기본적인 방법은 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)

그러나 여기에는 몇 가지 단점이 있습니다.

  • Expr이 사용되는 모든 곳에서 동일한 if-else 체인이 반복됩니다.
  • 태그 값을 "lit"에서 "literal"로 변경하면 중단됩니다. 기존 코드입니다.
  • 합계 유형을 사용하려면 구현 세부 사항(예: 태그 및 각 변형이 사용하는 필드의 이름).

일치 구현 중

합계 유형을 사용하는 데 사용되는 단일 공개 일치 방법을 노출하여 이러한 모든 문제를 피할 수 있습니다.

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

하지만 먼저 다양한 변형을 좀 더 균일하게 만들어야 합니다. 데이터를 다양한 필드에 저장하는 대신, 각 변형은 이제 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

이를 통해 일치 항목을 구현할 수 있습니다.

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

위에서 언급한 모든 문제를 단번에 해결했습니다! 또 다른 예이자 상황의 변화를 위해 다음은 Rust의 Option 유형을 다음과 같은 방식으로 표기한 것입니다.

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)

작은 삶의 질 혜택으로 일치 시 밑줄(_)로 표시된 특수 와일드카드 또는 "포괄" 처리기를 지원할 수 있습니다.

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

이를 통해 다음과 같은 일치 항목을 사용할 수 있습니다.

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

열거형 구현

Option 클래스에서 볼 수 있듯이 합계 유형을 생성하는 데 필요한 많은 코드는 동일한 패턴을 따릅니다.

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

직접 작성하는 대신 데코레이터를 작성하여 변형에 대한 일부 설명을 기반으로 이러한 메서드를 생성해 보겠습니다.

def enum(**variants):
    pass

어떤 설명인가요? 가장 간단한 방법은 변형 이름 목록을 제공하는 것이지만, 우리가 기대하는 인수 유형도 제공하면 좀 더 나은 결과를 얻을 수 있습니다. 다음과 같이 Option 클래스를 자동으로 향상시키기 위해 enum을 사용합니다.

# 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

enum의 기본 구조는 다음과 같습니다.

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

이것은 또 다른 함수를 반환하는 함수로, 우리가 향상시키고 있는 클래스를 유일한 인수로 사용하여 호출됩니다. 강화 내에서 일치 항목과 함께 각 변형을 구성하는 방법을 첨부하겠습니다.

먼저, 그냥 카피 파스타이기 때문에 일치:

# 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

각 변형을 구성하는 방법을 추가하는 것은 약간 더 복잡합니다. 각 항목에 대한 메소드를 정의하면서 변형 사전을 반복합니다.

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

make_constructor는 태그(및 이름) 태그와 "유형 서명" 기호가 있는 변형에 대한 생성자 함수를 생성합니다.

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)

참조용 enum의 전체 정의는 다음과 같습니다.

보너스 기능

더 많은 던더 메소드

__repr__ 및 __eq__ 메소드를 사용하여 합계 클래스를 쉽게 향상할 수 있습니다.

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

이러한 방식으로 향상을 통해 최소한의 조작으로 Option을 정의할 수 있습니다.

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

재귀적 정의

안타깝게도 enum은 (아직) 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}")

우리는 Expr 정의되기 전 클래스를 사용하고 있습니다. 여기서 쉬운 해결 방법은 클래스를 정의한 후 데코레이터를 호출하는 것입니다.

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)

그러나 이를 지원하기 위해 간단한 변경 사항이 있습니다. "서명"을 튜플을 반환하는 함수로 허용하는 것입니다.

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

이렇게 하려면 make_constructor를 약간만 변경하면 됩니다.

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

결론

유용하기는 하지만 새로운 멋진 열거형 데코레이터에는 단점도 있습니다. 가장 분명한 점은 어떤 종류의 "중첩" 패턴 일치도 수행할 수 없다는 것입니다. Rust에서는 다음과 같은 일을 할 수 있습니다:

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

하지만 동일한 결과를 얻으려면 더블 매치를 수행해야 합니다.

def enum(**variants):
    pass

그래도 이런 경우는 비교적드물어 보입니다.

또 다른 단점은 일치를 위해서는 많은 함수를 구성하고 호출해야 한다는 것입니다. 즉, 동등한 if-else 체인보다 훨씬 느릴 가능성이 높습니다. 그러나 여기에는 일반적인 경험 법칙이 적용됩니다. 인체공학적 이점이 마음에 들면 enum을 사용하고, 너무 느리면 "생성된" 코드로 교체하세요.

위 내용은 Python의 합계 유형의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.