Show GN: fieldenum: 파이썬에서의 러스트식 필드형 enum 지원
(github.com/ilotoki0804)- fieldenum은 값을 가지는 (인스턴스화할 수 있는) enum입니다.
- 러스트의 필드 있는 enum을 깔끔하게 지원합니다.
- 함수형 프로그래밍의 순수성과 파이썬에서의 실용성 사이의 균형을 잡기 위해 노력했습니다.
- 기본으로
None
의 대안인Option
이나 예외의 대안인BoundResult
을 지원합니다. - 완전히 테스트되어 있습니다.
- 아직 영문 문서가 빈약하지만 점차 보강해 나갈 계획입니다.
- 이슈, PR, star 등의 다양한 형태의 지원들을 모두 환영합니다.
dataclass의 union 타입이 더 낫지 않을까 하는데 선언문이 짧은거 빼고는 장점을 잘 모르겠네요. fieldenum이 특별히 나은점이 있을까요?
선언이 짧고 간결하며 필요한 부분만 있는 것 또한 큰 장점입니다.
예를 들어,
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을 사용하면 이렇게 놓치기 쉬운 다양한 구현 디테일을 자동으로 챙기는 데에 도움이 됩니다.
혹시 파이콘 한국에서 발표해주시면 어떨까 제안드려봅니다. 너무 재미있게 봐서, 만드시는 과정에 있으셨던 이야기와 설명을 직접 듣고 싶습니다!
그리고 영문 README 에 Option 예시도 설명되었으면 좋겠네요.
Option 은 쉽게 이해되고 친숙하게 접근할수 있을테니까요. 문서상 설명 순서에서 Option을 먼저 설명하면 더 좋을것 같기도 합니다.
영어 문서는 아직 준비 중이라 조금 빈약합니다... 한국어 문서가 충분히 무르익으면 영어로 번역하려고 합니다. 혹은 관련 PR도 환영합니다!
저도 Option
을 먼저 소개하는 것이 더 나아 보이네요. 수정하겠습니다.
오오. 신기하네요!!
링크주신 한글 문서 예시 코드에 수정사항이 있습니다.
from fieldenum import fieldenum, Variant, Unit, unreachable
from fieldenum.enums import Option
def hello() -> Option: # GOOD
return Option.Some("hello")
def print_hello(option: Option): # GOOD
print(value.unwrap()) #!!!!! 여기에 value가 아니라 option 이어야 할것 같아 보이네요 !!!!!#
value = hello()
print_hello(value)