10 pontos por ilotoki0804 2024-06-01 | 14 comentários | Compartilhar no WhatsApp
  • fieldenum é um enum que possui valores (e pode ser instanciado).
  • Oferece suporte elegante a enums com campos no estilo Rust.
  • Busca equilibrar a pureza da programação funcional com a praticidade em Python.
  • Por padrão, oferece suporte a Option como alternativa a None e a BoundResult como alternativa a exceções.
  • Totalmente testado.
  • A documentação em inglês ainda é limitada, mas há planos de melhorá-la gradualmente.
  • Todo tipo de apoio é bem-vindo, como issues, PRs, stars e outras formas de contribuição.

14 comentários

 
savvykang 2024-06-02

Eu também fico pensando se não seria melhor usar um tipo union com dataclass; tirando o fato de a declaração ser mais curta, não vejo muito bem quais seriam as vantagens. O fieldenum teria algum ponto em que ele é especialmente melhor?

 
ilotoki0804 2024-06-03

A declaração ser curta e concisa, contendo apenas o que é necessário, também é uma grande vantagem.
Por exemplo,

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)  

Para implementar o fieldenum acima com dataclass, seria preciso escrever algo assim.

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  

O código fica mais longo, mais difícil de ler, a chance de cometer erros aumenta, e não dá exatamente a sensação de um código limpo, não é?

Claro, mesmo escrevendo dessa forma, você ainda não terá acesso a vários outros recursos oferecidos pelo fieldenum (genéricos, repr, __fields__, ...).

Por isso, ter um fieldenum que já implementa e reúne tudo isso é muito mais conveniente.

Além disso, também vale a pena dar uma olhada no conteúdo da seção de exemplos.

 
savvykang 2024-06-03
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  
  1. dataclass oferece implementação de repr por padrão
  2. dataclasses.fields fornece informações em tempo de execução sobre a definição dos campos
  3. Genéricos são suportados desde a 3.5 pelo módulo typing, e o açúcar sintático desde a 3.12
  4. No caso do namespace Messages, isso pode ser implementado com um módulo

Mesmo assim, a ausência de código boilerplate necessário para definir classes e a possibilidade de usar enum e class com uma única interface certamente podem ser vantagens. Obrigado pela explicação detalhada.

 
savvykang 2024-06-03

https://stackoverflow.com/a/47784683

Já houve várias tentativas de representar structs desse jeito, mas no fim acho que isso pode ser visto como um limite e uma desvantagem do Python. Tive meu primeiro contato com ADT (algebraic data type) nas aulas da faculdade, com OCaml, e é meio triste que no trabalho a gente tenha que apenas imitar isso dessa forma.

A biblioteca criada pelo ilotoki provavelmente é o caso que mais se aproxima de um ADT. Seria ótimo se algum dia ela fosse incluída na biblioteca padrão e passasse a ser amplamente usada.

 
ilotoki0804 2024-06-03

Se a implementação de Message fosse feita com Union, não seria possível aproveitar herança de métodos. Por exemplo,

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):  
        ...  

Ao adicionar o método .process como acima, é possível usar o método .process() em todas as variantes.

# O método Message.process() pode ser usado em cada variante  
Message.Quit.process()  
Message.Move(x=123, y=456).process()  
Message.Write("hello, world").process()  
Message.ChangeColor(123, 000, 89).process()  

Além disso, o repr que mencionei significa o "repr como variante desse enum".
Por exemplo, ao chamar repr com fieldenum envolvendo a chamada, ele é executado da seguinte forma.

print(repr(Message.Move(x=123, y=456)))  # Message.Move(x=123, y=456)  

Sem um __repr__ personalizado, o fato de ele ser uma subvariante do enum Message não é representado.

Quit é uma variante unitária e é usada sem chamada.

Message.Quit  # Pode ser usado sem chamada separada (por exemplo, `Message.Quit()`)  

Além disso, no caso das variantes sem campos que exigem chamada, elas podem ser verificadas como singleton com o operador is.

from fieldenum import fieldenum, Variant, Unit  
  
class WithFieldless:  
    Fieldless = Variant()  
  
assert WithFieldless.Fieldless() is WithFieldless.Fieldless()  

Usar fieldenum ajuda a cuidar automaticamente desses vários detalhes de implementação que são fáceis de deixar passar.

 
wyatt216 2024-06-02

Queria sugerir, por acaso, que você apresentasse isso na PyCon Korea. Achei muito divertido de ver e gostaria de ouvir diretamente de você as histórias e explicações do processo de criação!

 
ilotoki0804 2024-06-02

Seria realmente uma honra poder apresentar isso na PyCon. Não sei se só por eu querer isso significa que vou conseguir (^^;), mas vou pensar a respeito.

 
kayws426 2024-06-01

E também seria bom se o README em inglês explicasse um exemplo de Option.
Option é fácil de entender e permite uma abordagem mais familiar. Talvez também fosse melhor explicar Option primeiro na ordem das explicações da documentação.

 
ilotoki0804 2024-06-01

A documentação em inglês ainda não está pronta, então está um pouco incompleta... Quando a documentação em coreano estiver suficientemente madura, pretendo traduzi-la para o inglês. Ou PRs relacionados também são bem-vindos!
Também acho que apresentar Option primeiro parece melhor. Vou corrigir isso.

 
kayws426 2024-06-01

Ooh. Que interessante!!
Há uma correção no código de exemplo da documentação em coreano do link que você enviou.

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()) #!!!!! aqui parece que deveria ser option, não value !!!!!#  
  
value = hello()  
print_hello(value)  
 
ilotoki0804 2024-06-01

Obrigado por avisar. Corrigi!

 
ilotoki0804 2024-06-01

Era para eu ter postado no Show GN, mas por engano publiquei como post normal;;

 
moderator 2024-06-01

Corrigi isso.

 
ilotoki0804 2024-06-01

Obrigado~