선언이 짧고 간결하며 필요한 부분만 있는 것 또한 큰 장점입니다.
예를 들어,
from fieldenum import fieldenum, Variant, Unit
@fieldenum
class Message:
Quit = Unit
Move = Variant(x=int, y=int)
Write = Variant(str)
ChangeColor = Variant(int, int, int)
위에 있는 fieldenum을 dataclass로 구현하려면 다음과 같이 짜야 합니다.
from dataclasses import dataclass
from typing import Self
class Message:
Quit = Self
Move = Self
Write = Self
ChangeColor = Self
class QuitMessageClass(Message, metaclass=ParamlessSingletonMeta):
pass
QuitMessage = QuitMessageClass()
@dataclass(frozen=True, kw_only=True)
class MoveMessage(Message):
x: int
y: int
@dataclass(frozen=True)
class WriteMessage(Message):
_0: str
@dataclass(frozen=True)
class ChangeColorMessage(Message):
_0: int
_1: int
_2: int
Message.Quit = QuitMessage
Message.Move = MoveMessage
Message.Write = WriteMessage
Message.ChangeColor = ChangeColorMessage
코드가 길어지고 보기도 어려워졌고, 실수할 가능성도 높으며, 코드가 깔끔하다고 느껴지지는 않죠?
물론 이렇게 짜더라도 fieldenum에서 제공하는 더 많은 다른 기능들(제너릭, repr, __fields__
, ...)은 제공받을 수 없습니다.
따라서 이 모든 것들을 구현하고 모아놓은 fieldenum이 있으면 훨씬 편리합니다.
그 외에도 예시
파트에 있는 내용을 참고해 보시면 좋을 듯 합니다.
from dataclasses import dataclass
@dataclass(frozen=True) # repr True by default
class QuitMessage:
pass
@dataclass(frozen=True, kw_only=True) # repr True by default
class MoveMessage:
x: int
y: int
@dataclass(frozen=True) # repr True by default
class WriteMessage:
_0: str
@dataclass(frozen=True) # repr True by default
class ChangeColorMessage:
_0: int
_1: int
_2: int
Message = QuitMessage | MoveMessage | WriteMessage | ChangeColorMessage
- dataclass는 기본적으로 repr 구현을 지원합니다
- dataclasses.fields는 필드 정의에 대한 실행시간 정보를 제공합니다
- 지네릭은 typing 모듈에 의해 3.5부터, syntactic sugar는 3.12부터 지원합니다
- Messages 이름 공간의 경우 모듈로 구현 가능합니다
그럼에도 불구하고 class 정의에 필요한 보일러플레이트 코드가 없다는 점, enum과 class를 한가지 인터페이스로 사용할 수 있는 점이 장점이 될 수 있겠네요. 상세한 설명 감사합니다
https://stackoverflow.com/a/47784683
이런 식으로 구조체를 표현하고자 하는 시도들이 여러가지가 있어왔는데, 결국에는 파이썬의 한계이자 단점으로 볼 수 있을 것 같습니다. ADT(algebraic data type)를 학교 수업때 ocaml로 처음 접했었는데 일할때는 이런 식으로 흉내만 내야 한다는 게 좀 안타깝기도 하네요
ilotoki님께서 만드신 라이브러리가 가장 ADT에 근접한 사례로 볼 수 있을 것 같습니다. 언젠가 표준 라이브러리에 포함되고 널리 쓰이게 된다면 좋을 것 같습니다
Message
의 구현은 Union으로 하게 된다면 메서드 상속을 이용할 수 없습니다. 예를 들어
from fieldenum import fieldenum, Variant, Unit
@fieldenum
class Message:
Quit = Unit
Move = Variant(x=int, y=int)
Write = Variant(str)
ChangeColor = Variant(int, int, int)
def process(self):
...
위와 같이 .process
메서드를 추가하면 모든 배리언트들에 대해 .process()
메서드를 사용할 수 있습니다.
# Message.process() 메서드를 각 배리언트에서 사용 가능
Message.Quit.process()
Message.Move(x=123, y=456).process()
Message.Write("hello, world").process()
Message.ChangeColor(123, 000, 89).process()
또한 제가 설명드린 repr는 '해당 enum의 배리언트로서의 repr'를 의미한 것입니다.
예를 들어 fieldenum을 repr를 감싸 호출하면 다음과 같이 실행됩니다.
print(repr(Message.Move(x=123, y=456))) # Message.Move(x=123, y=456)
커스텀 __repr__
가 없으면 Message
enum의 하위 배리언트라는 사실이 표현되지 않습니다.
Quit
은 유닛 배리언트로 호출 없이 사용합니다.
Message.Quit # 별도의 호출 (예: `Message.Quit()`) 없이 사용 가능
또한 호출을 사용해야 하는 배리언트 종류인 fieldless 배리언트의 경우에는 싱글톤으로서 is
연산자로 확인할 수 있습니다.
from fieldenum import fieldenum, Variant, Unit
class WithFieldless:
Fieldless = Variant()
assert WithFieldless.Fieldless() is WithFieldless.Fieldless()
fieldenum을 사용하면 이렇게 놓치기 쉬운 다양한 구현 디테일을 자동으로 챙기는 데에 도움이 됩니다.
dataclass의 union 타입이 더 낫지 않을까 하는데 선언문이 짧은거 빼고는 장점을 잘 모르겠네요. fieldenum이 특별히 나은점이 있을까요?