10 คะแนน โดย ilotoki0804 2024-06-01 | 14 ความคิดเห็น | แชร์ทาง WhatsApp
  • fieldenum คือ enum ที่มีค่าอยู่ภายใน (และสามารถสร้างอินสแตนซ์ได้)
  • รองรับ enum แบบมีฟิลด์ของ Rust ได้อย่างเรียบร้อย
  • พยายามสร้างสมดุลระหว่างความบริสุทธิ์ของการเขียนโปรแกรมเชิงฟังก์ชันกับความใช้งานได้จริงใน Python
  • รองรับ Option ซึ่งเป็นทางเลือกแทน None และ BoundResult ซึ่งเป็นทางเลือกแทนข้อยกเว้นโดยค่าเริ่มต้น
  • มีการทดสอบอย่างครบถ้วน
  • ตอนนี้เอกสารภาษาอังกฤษยังค่อนข้างน้อย แต่มีแผนจะค่อย ๆ เติมให้ครบ
  • ยินดีต้อนรับการสนับสนุนทุกรูปแบบ ไม่ว่าจะเป็น issue, PR, star เป็นต้น

14 ความคิดเห็น

 
savvykang 2024-06-02

ผมคิดว่า union type ของ dataclass อาจจะดีกว่า แต่ยกเว้นแค่ตรงที่ประกาศสั้นกว่าแล้ว ก็ยังไม่ค่อยเห็นข้อดีนักครับ มีจุดไหนที่ fieldenum ดีกว่าอย่างชัดเจนเป็นพิเศษไหม?

 
ilotoki0804 2024-06-03

จุดเด่นอย่างหนึ่งคือการประกาศที่สั้น กระชับ และมีเฉพาะส่วนที่จำเป็น
ตัวอย่างเช่น

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 มีให้ (generics, repr, __fields__, ...)

ดังนั้นหากมี fieldenum ที่รวบรวมและจัดการสิ่งเหล่านี้ไว้ทั้งหมด ก็จะสะดวกกว่ามาก

นอกจากนี้ยังแนะนำให้ลองดูเนื้อหาในส่วน ตัวอย่าง เพิ่มเติมด้วย

 
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 รองรับการทำ repr โดยค่าเริ่มต้น
  2. dataclasses.fields ให้ข้อมูลรันไทม์เกี่ยวกับการนิยามฟิลด์
  3. generic รองรับผ่านโมดูล typing ตั้งแต่ 3.5 และ syntactic sugar รองรับตั้งแต่ 3.12
  4. ในกรณีของเนมสเปซ Messages สามารถทำเป็นโมดูลได้

ถึงอย่างนั้น การไม่มีโค้ด boilerplate ที่จำเป็นสำหรับการนิยาม class และการสามารถใช้ enum กับ class ผ่านอินเทอร์เฟซเดียวกันได้ ก็อาจเป็นข้อดีอยู่เหมือนกันนะครับ ขอบคุณสำหรับคำอธิบายละเอียด ๆ

 
savvykang 2024-06-03

https://stackoverflow.com/a/47784683

ก่อนหน้านี้ก็มีความพยายามหลายแบบในการแสดงโครงสร้างข้อมูลในลักษณะนี้ แต่ท้ายที่สุดก็ดูเหมือนจะเป็นทั้งข้อจำกัดและจุดอ่อนของ Python เอง ผมเคยรู้จัก ADT (algebraic data type) ครั้งแรกตอนเรียนในมหาวิทยาลัยผ่าน OCaml แต่พอเวลาทำงานจริงกลับต้องทำได้แค่เลียนแบบในลักษณะนี้ ก็แอบน่าเสียดายอยู่เหมือนกัน

ไลบรารีที่ ilotoki สร้างขึ้นน่าจะถือเป็นกรณีที่เข้าใกล้ ADT มากที่สุด หวังว่าสักวันหนึ่งมันจะได้ถูกรวมเข้าไปใน standard library และถูกใช้อย่างแพร่หลาย

 
ilotoki0804 2024-06-03

หากใช้ 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() ได้กับทุก variant

# สามารถใช้เมธอด Message.process() ได้ในแต่ละ variant  
Message.Quit.process()  
Message.Move(x=123, y=456).process()  
Message.Write("hello, world").process()  
Message.ChangeColor(123, 000, 89).process()  

นอกจากนี้ repr ที่ผมอธิบายหมายถึง 'repr ในฐานะที่เป็น variant ของ enum นั้น'
ตัวอย่างเช่น หากครอบการเรียก repr ด้วย fieldenum จะได้ผลลัพธ์ดังนี้

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

หากไม่มี __repr__ แบบกำหนดเอง ก็จะไม่สามารถแสดงให้เห็นว่าเป็น sub-variant ของ enum Message

Quit เป็น unit variant จึงใช้งานได้โดยไม่ต้องเรียก

Message.Quit  # ใช้งานได้โดยไม่ต้องเรียกแยกต่างหาก (เช่น `Message.Quit()`)  

อีกทั้งในกรณีของ fieldless variant ซึ่งเป็นชนิด variant ที่ต้องใช้การเรียก ก็สามารถตรวจสอบด้วยตัวดำเนินการ is ในฐานะ singleton ได้

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

การใช้ fieldenum จะช่วยดูแลรายละเอียดการใช้งานที่หลากหลายและพลาดได้ง่ายเหล่านี้ให้โดยอัตโนมัติ

 
wyatt216 2024-06-02

ขออนุญาตเสนอว่า ถ้ามีโอกาสช่วยไปบรรยายที่ PyCon Korea จะเป็นอย่างไรบ้างครับ/คะ ผม/ฉันดูอย่างสนุกมากจนอยากฟังเรื่องราวและคำอธิบายเกี่ยวกับกระบวนการสร้างจากเจ้าตัวโดยตรงเลยครับ/ค่ะ!

 
ilotoki0804 2024-06-02

คงจะเป็นเกียรติมากจริง ๆ ถ้าได้ไปพูดที่ PyCon นะครับ/ค่ะ ผม/ฉันก็ไม่แน่ใจเหมือนกันว่าพอแค่ผม/ฉันอยากทำแล้วจะทำได้ไหม(^^;) แต่จะลองคิดดูครับ/ค่ะ

 
kayws426 2024-06-01

และก็น่าจะดีถ้าใน README ภาษาอังกฤษมีการอธิบายตัวอย่างของ Option ด้วยนะครับ
เพราะ Option เข้าใจได้ง่ายและเข้าถึงได้คุ้นเคยกว่าอยู่แล้วด้วย ผมคิดว่าถ้าในลำดับการอธิบายในเอกสารอธิบาย Option ก่อนก็น่าจะดีกว่าเช่นกัน

 
ilotoki0804 2024-06-01

เอกสารภาษาอังกฤษยังอยู่ระหว่างเตรียม จึงค่อนข้างสั้นไปเล็กน้อย... หากเอกสารภาษาเกาหลีสมบูรณ์พอแล้ว ผมตั้งใจจะแปลเป็นภาษาอังกฤษ หรือหากมี PR ที่เกี่ยวข้องก็ยินดีต้อนรับ!
ผมเองก็มองว่าการแนะนำ Option ก่อนน่าจะดีกว่า จะปรับแก้ครับ

 
kayws426 2024-06-01

โอ้โห น่าสนใจมากเลยครับ!!
มีจุดที่ต้องแก้ในโค้ดตัวอย่างของเอกสารภาษาเกาหลีตามลิงก์ที่ให้มาครับ

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()) #!!!!! ตรงนี้ดูเหมือนว่าควรเป็น option ไม่ใช่ value !!!!!#  
  
value = hello()  
print_hello(value)  
 
ilotoki0804 2024-06-01

ขอบคุณที่แจ้งให้ทราบครับ แก้ไขแล้ว!

 
ilotoki0804 2024-06-01

ควรโพสต์ไปที่ Show GN แต่เผลอโพสต์เป็นหมวดทั่วไปไปซะแล้ว;;

 
moderator 2024-06-01

ได้แก้ไขไว้แล้ว

 
ilotoki0804 2024-06-01

ขอบคุณครับ~