1 คะแนน โดย GN⁺ 1 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • PEP 661 เสนออ็อบเจ็กต์ callable แบบ built-in ของ Python คือ sentinel() และ C API PySentinel_New() เพื่อสร้างค่า sentinel ที่แยกแยะได้ต่างหากในกรณีที่ None เป็นค่าที่ใช้ได้
  • สำนวนเดิมอย่าง _sentinel = object() มีปัญหาที่ repr ยาวและไม่ชัดเจนใน signature ของฟังก์ชัน และอาจมีปัญหากับ type signature ที่ชัดเจน การคัดลอก และการ pickle
  • การเรียก sentinel('MISSING') จะสร้างอ็อบเจ็กต์ใหม่ที่ไม่ซ้ำกันและมี repr แบบสั้น โดยหากต้องการใช้ sentinel ตัวเดิมร่วมกัน ต้องกำหนดให้ตัวแปรแล้วนำกลับมาใช้ซ้ำอย่างชัดเจน เช่น MISSING = sentinel('MISSING')
  • แนะนำให้เปรียบเทียบ sentinel ด้วย is และมันจะถูกประเมินเป็นค่าจริง ขณะที่ copy.copy() และ copy.deepcopy() จะคืนอ็อบเจ็กต์เดิม และหากสามารถ import จากโมดูลด้วยชื่อได้ ก็จะคงเอกลักษณ์เดิมไว้แม้ผ่านการ pickle แล้ว
  • ระบบ type รองรับให้ใช้ sentinel เองในนิพจน์ type ได้ เช่น int | MISSING และเอกสารทางการล่าสุดอยู่ที่เอกสาร [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)") ของ Python 3.15

ที่มาของการนำมาใช้

  • ค่า sentinel (sentinel value) ซึ่งเป็นค่าตัวแทนเฉพาะ ใช้เป็นค่าเริ่มต้นเมื่อไม่ได้ส่งอาร์กิวเมนต์ให้ฟังก์ชัน ใช้เป็นค่าที่ส่งกลับเพื่อบอกว่าค้นหาไม่พบ หรือใช้แทนข้อมูลที่ขาดหาย
  • ใน Python ปกติมีค่าพิเศษ None ที่มักใช้เพื่อจุดประสงค์นี้ แต่ในบริบทที่ None เองก็เป็นค่าที่ใช้ได้ จำเป็นต้องมีค่า sentinel แยกต่างหากที่แยกออกจาก None ได้
  • เมื่อเดือนพฤษภาคม 2021 มีการพูดคุยกันใน mailing list ของ python-dev เกี่ยวกับวิธีทำให้ค่า sentinel ที่ใช้ใน traceback.print_exception มีการติดตั้งใช้งานที่ดีกว่าเดิม
  • การติดตั้งใช้งานเดิมใช้สำนวนที่พบได้บ่อยคือ _sentinel = object() แต่ repr ยาวเกินไปและให้ข้อมูลน้อย ทำให้อ่าน signature ของฟังก์ชันได้ยาก
    &gt;&gt;&gt; help(traceback.print_exception)  
    Help on function print_exception in module traceback:  
    
    print_exception(exc, /, value=&lt;object object at  
    0x000002825DF09650&gt;, tb=&lt;object object at 0x000002825DF09650&gt;,  
    limit=None, file=None, chain=True)  
    
  • ระหว่างการอภิปราย ยังพบปัญหาอื่นของการติดตั้งใช้งาน sentinel แบบเดิมด้วย
    • sentinel บางตัวไม่มี type เฉพาะ ทำให้ยากต่อการกำหนด type signature ที่ชัดเจนให้ฟังก์ชันที่ใช้ sentinel เป็นค่าเริ่มต้น
    • หลังคัดลอกแล้วเกิดอินสแตนซ์แยก ทำให้การเปรียบเทียบด้วย is ล้มเหลวและมีพฤติกรรมต่างจากที่คาด
    • สำนวนที่ใช้กันทั่วไปบางแบบก็มีปัญหาคล้ายกันหลัง pickle แล้ว unpickle
  • Victor Stinner ได้จัดทำรายการค่า sentinel ที่ใช้อยู่ใน Python standard library และยืนยันว่าแม้ภายใน standard library เองก็ใช้หลายแนวทาง และหลาย implementation ก็มีปัญหาอย่างน้อยหนึ่งข้อจากที่กล่าวมา
  • การโหวตบน discuss.python.org ไม่ได้ข้อสรุปที่ชัดเจนจากคะแนนทั้งหมด 39 เสียง
    • 40% เลือก “สภาพปัจจุบันก็ใช้ได้ดีอยู่แล้ว และไม่จำเป็นต้องทำให้สอดคล้องกัน”
    • ผู้โหวตส่วนใหญ่เลือกแนวทางมาตรฐานอย่างน้อยหนึ่งแบบ
    • 37% เลือกตัวเลือก “ใช้ factory/class/metaclass สำหรับ sentinel แบบเฉพาะทางตัวใหม่อย่างสม่ำเสมอ และเปิดเผยให้ใช้ใน standard library”
  • ด้วยผลลัพธ์ที่แตกออกเป็นหลายทาง จึงมีการเขียน PEP ขึ้น และนำไปสู่ข้อสรุปว่า implementation ใน standard library ที่เรียบง่ายและดีนั้นมีประโยชน์ทั้งภายในและภายนอก standard library
  • ไม่ได้บังคับว่าต้องเปลี่ยน sentinel เดิมทั้งหมดใน standard library ให้มาใช้แนวทางนี้ โดยปล่อยให้เป็นดุลยพินิจของผู้ดูแลแต่ละส่วน
  • เอกสาร PEP เป็นเอกสารเชิงประวัติศาสตร์ และเอกสารทางการล่าสุดอยู่ที่เอกสาร [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)") ของ Python 3.15

เกณฑ์การออกแบบ

  • อ็อบเจ็กต์ sentinel ต้องเมื่อเปรียบเทียบด้วยตัวดำเนินการ is แล้ว จะเหมือนกับตัวเองเสมอ และไม่เหมือนกับอ็อบเจ็กต์อื่นใด
  • การสร้างอ็อบเจ็กต์ sentinel ต้องเป็นโค้ด บรรทัดเดียว ที่เรียบง่ายและเข้าใจได้ทันที
  • ต้องสามารถนิยามค่า sentinel ที่แตกต่างกันได้หลายตัวตามต้องการอย่างง่ายดาย
  • อ็อบเจ็กต์ sentinel ต้องมี repr ที่สั้นและชัดเจน
  • ต้องสามารถใช้ type signature ที่ชัดเจนกับ sentinel ได้
  • ต้องทำงานได้ถูกต้องหลังการคัดลอก และต้องมีพฤติกรรมที่คาดเดาได้เมื่อนำไป pickle และ unpickle
  • ต้องทำงานได้บน CPython 3.x และ PyPy3 และถ้าเป็นไปได้ก็ควรทำงานได้บน implementation อื่นของ Python ด้วย
  • ทั้งการติดตั้งใช้งานและการใช้งานต้องเรียบง่ายและตรงไปตรงมาที่สุด และไม่ควรกลายเป็นอีกหนึ่งแนวคิดพิเศษที่เพิ่มภาระในการเรียนรู้ Python
  • เนื่องจาก standard library ไม่สามารถพึ่งพา implementation จากแพ็กเกจบน PyPI อย่าง sentinels หรือ sentinel ได้ จึงจำเป็นต้องมี implementation ที่ใช้งานได้ภายใน standard library เอง

สเปกของ sentinel()

  • มีการเพิ่มอ็อบเจ็กต์ที่เรียกใช้ได้แบบ built-in ตัวใหม่ชื่อ sentinel
    >>> MISSING = sentinel('MISSING')  
    >>> MISSING  
    MISSING  
    
  • sentinel() รับอาร์กิวเมนต์แบบ positional-only หนึ่งตัวคือ name และ name ต้องเป็น str เท่านั้น
  • หากส่งค่าที่ไม่ใช่สตริงเข้ามา จะเกิด TypeError
  • name ใช้เป็นชื่อของ sentinel และใช้ใน repr
  • อ็อบเจ็กต์ sentinel มีแอตทริบิวต์สาธารณะสองตัว
    • __name__: ชื่อของ sentinel
    • __module__: ชื่อโมดูลที่มีการเรียก sentinel()
  • ไม่สามารถทำ subclass ของ sentinel ได้
  • ทุกครั้งที่เรียก sentinel(name) จะได้อ็อบเจ็กต์ sentinel ใหม่กลับมา
  • หากต้องใช้ sentinel เดียวกันในหลายที่ ควรเก็บไว้ในตัวแปรแล้วนำอ็อบเจ็กต์เดิมกลับมาใช้ซ้ำอย่างชัดเจน เหมือนกับสำนวน MISSING = object() ที่ใช้กันอยู่เดิม
    MISSING = sentinel('MISSING')  
    
    def read_value(default=MISSING):  
        ...  
    
  • เมื่อต้องตรวจสอบว่าค่าหนึ่งเป็น sentinel หรือไม่ แนะนำให้ใช้ ตัวดำเนินการ is เช่นเดียวกับ None
  • การเปรียบเทียบด้วย == ก็ทำงานตามที่คาดไว้ โดยจะคืนค่า True เฉพาะเมื่อเปรียบเทียบกับตัวเองเท่านั้น
  • การตรวจสอบเอกลักษณ์อย่าง if value is MISSING: โดยทั่วไปเหมาะสมกว่าการตรวจสอบแบบบูลีน เช่น if value: หรือ if not value:
  • อ็อบเจ็กต์ sentinel เป็นค่าจริง (truthy) และเมื่อประเมินเป็นบูลีนจะได้ True
    • ซึ่งเหมือนกับพฤติกรรมปริยายของคลาสทั่วไปและค่าบูลีนของ Ellipsis
    • และต่างจาก None ซึ่งเป็นค่าเท็จ (falsy)
  • หากคัดลอกอ็อบเจ็กต์ sentinel ด้วย copy.copy() หรือ copy.deepcopy() จะได้อ็อบเจ็กต์เดิมกลับมา
  • sentinel ที่สามารถ import ตามชื่อได้จากโมดูลที่ประกาศไว้ จะคงเอกลักษณ์เดิมไว้ได้หลัง pickle และ unpickle ตามกลไก pickle มาตรฐาน
    MISSING = sentinel('MISSING')  
    assert pickle.loads(pickle.dumps(MISSING)) is MISSING  
    
  • sentinel() จะบันทึกโมดูลที่เรียกใช้ไว้ในแอตทริบิวต์ __module__ ตอนสร้าง sentinel
  • การ pickle จะบันทึก sentinel ด้วยโมดูลและชื่อ ส่วนการ unpickle จะ import โมดูลแล้วดึง sentinel ตามชื่อนั้น
  • sentinel ที่ไม่สามารถ import ได้ด้วยโมดูลและชื่อ เช่น sentinel ที่สร้างใน local scope และไม่ได้ถูกกำหนดให้กับชื่อที่ตรงกันใน global ของโมดูลหรือแอตทริบิวต์ของคลาส จะไม่สามารถ pickle ได้
  • repr ของอ็อบเจ็กต์ sentinel คือ name ที่ส่งให้ sentinel() โดยจะไม่มีตัวระบุโมดูลต่อท้ายแบบอัตโนมัติ
  • หากต้องการ repr แบบมีชื่อกำกับ ต้องใส่ไว้ในชื่อนั้นอย่างชัดเจน
    >>> MyClass_NotGiven = sentinel('MyClass.NotGiven')  
    >>> MyClass_NotGiven  
    MyClass.NotGiven  
    
  • ไม่ได้กำหนดการเปรียบเทียบลำดับของอ็อบเจ็กต์ sentinel
  • sentinel ไม่รองรับ weakref

การกำหนดชนิด

  • เพื่อให้การใช้ sentinel ในโค้ด Python ที่มีการระบุชนิดชัดเจนและเรียบง่ายขึ้น จึงมีการเพิ่มการจัดการพิเศษสำหรับอ็อบเจ็กต์ sentinel ในระบบชนิด
  • อ็อบเจ็กต์ sentinel สามารถใช้เป็นค่าที่แทนตัวมันเองภายใน[นิพจน์ชนิด](<https://typing.python.org/en/latest/… "(in typing>)")ได้
  • แนวทางนี้คล้ายกับวิธีที่ระบบชนิดเดิมจัดการกับ None
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING = MISSING) -> int:  
        ...  
    
  • type checker ควรมองการสร้าง sentinel ในรูปแบบ NAME = sentinel('NAME') ว่าเป็นการสร้างอ็อบเจ็กต์ sentinel ใหม่
  • หากชื่อที่ส่งให้ sentinel() ไม่ตรงกับชื่อเป้าหมายของการกำหนดค่า type checker ควรรายงานข้อผิดพลาด
  • sentinel ที่นิยามด้วยไวยากรณ์นี้สามารถใช้ใน[นิพจน์ชนิด](<https://typing.python.org/en/latest/… "(in typing>)")ได้
  • ชนิดของ sentinel นั้นแสดงถึง[ชนิดแบบสถิตสมบูรณ์](<https://typing.python.org/en/latest/… "(in typing>)")ที่มีสมาชิกเพียงตัวเดียว คืออ็อบเจ็กต์ sentinel นั้นเอง
  • type checker ควรรองรับการทำให้ชนิดยูเนียนที่มี sentinel แคบลงด้วยตัวดำเนินการ is และ is not
    from typing import assert_type  
    
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING) -> None:  
        if value is MISSING:  
            assert_type(value, MISSING)  
        else:  
            assert_type(value, int)  
    
  • ฝั่ง runtime implementation ต้องมีเมธอด __or__ และ __ror__ เพื่อรองรับการใช้งานในนิพจน์ชนิด และเมธอดเหล่านี้จะคืนค่าอ็อบเจ็กต์ [typing.Union](<https://docs.python.org/3/library/typing.html#typing.Union "(in Python v3.14>)")
  • Typing Council สนับสนุน ส่วนที่เกี่ยวกับชนิดของข้อเสนอนี้

C API

  • เนื่องจาก sentinel อาจมีประโยชน์ใน C extension ด้วย จึงมีการเสนอฟังก์ชัน C API ใหม่สองตัว
  • PyObject *PySentinel_New(const char *name, const char *module_name) ใช้สร้างอ็อบเจ็กต์ sentinel ใหม่
  • bool PySentinel_Check(PyObject *obj) ใช้ตรวจสอบว่าอ็อบเจ็กต์นั้นเป็น sentinel หรือไม่
  • โค้ด C สามารถใช้ตัวดำเนินการ == เพื่อตรวจสอบว่าเป็น sentinel ตัวที่ต้องการหรือไม่

ความเข้ากันได้และความปลอดภัย

  • เมื่อมีการเพิ่มชื่อ built-in ใหม่ โค้ดที่ปัจจุบันสมมติว่าชื่อ bare name sentinel จะทำให้เกิด NameError จะไม่เห็นผลลัพธ์แบบเดิมอีกต่อไป
  • นี่เป็นประเด็นด้านความเข้ากันได้ที่เกิดขึ้นเป็นปกติเมื่อมีการเพิ่มชื่อ built-in ใหม่
  • ชื่อ sentinel ที่มีอยู่แล้วใน local, global หรือ import จะไม่ได้รับผลกระทบ
  • โค้ดที่ใช้ชื่อ sentinel อยู่แล้วอาจต้องปรับให้ใช้ built-in อ็อบเจ็กต์ตัวใหม่ และอาจได้รับคำเตือนใหม่จาก linter ที่เตือนเรื่องชื่อชนกับ built-in
  • เห็นว่าการสื่อสารผ่านวิธีเอกสารประกอบตามปกติสำหรับฟีเจอร์ built-in ใหม่ เช่น docstring, เอกสารไลบรารี และส่วน “What’s New” ก็เพียงพอแล้ว
  • ข้อเสนอนี้ถือว่าไม่มีผลกระทบด้านความปลอดภัย

การอ้างอิงอิมพลีเมนเทชันและแบ็กพอร์ต

  • การอ้างอิงอิมพลีเมนเทชันมีให้ในรูปแบบ CPython pull request [10]
  • การอ้างอิงอิมพลีเมนเทชันก่อนหน้านี้อยู่ใน GitHub repository แยกต่างหาก [7]
  • โครงร่างของพฤติกรรมที่ตั้งใจไว้มีดังนี้
    class sentinel:  
        &quot;&quot;&quot;Unique sentinel values.&quot;&quot;&quot;  
    
        __slots__ = (&quot;__name__&quot;, &quot;_module_name&quot;)  
    
        def __init_subclass__(cls):  
            raise TypeError(&quot;type &#039;sentinel&#039; is not an acceptable base type&quot;)  
    
        def __init__(self, name, /):  
            if not isinstance(name, str):  
                raise TypeError(&quot;sentinel name must be a string&quot;)  
            self.__name__ = name  
            self._module_name = sys._getframemodulename(1)  
    
        @property  
        def __module__(self):  
            return self._module_name  
    
        def __repr__(self):  
            return self.__name__  
    
        def __reduce__(self):  
            return self.__name__  
    
        def __copy__(self):  
            return self  
    
        def __deepcopy__(self, memo):  
            return self  
    
        def __or__(self, other):  
            return typing.Union[self, other]  
    
        def __ror__(self, other):  
            return typing.Union[other, self]  
    
    • ในโมดูล typing-extensions มี แบ็กพอร์ต อยู่ แต่ในปัจจุบันยังไม่ตรงกับพฤติกรรมของ PEP ฉบับปรับปรุงล่าสุดอย่างแม่นยำ

ทางเลือกที่ถูกปฏิเสธ

  • ใช้ NotGiven = object()

    • วิธีนี้มีข้อเสียทั้งหมดที่กล่าวถึงในเกณฑ์การออกแบบของ PEP
    • repr ยาวและไม่ชัดเจน ทำให้ระบุ type signature ให้ชัดเจนได้ยาก และอาจเกิดปัญหาเกี่ยวกับการคัดลอกหรือการ pickle
  • เพิ่มค่า sentinel ใหม่เพียงค่าเดียว เช่น MISSING หรือ Sentinel

    • หากใช้ค่าเดียวในหลายที่และหลายวัตถุประสงค์ ก็ยากที่จะมั่นใจได้เสมอว่าในบางกรณีการใช้งาน ค่านั้นจะไม่ใช่ค่าที่ถูกต้องในตัวมันเอง
    • ค่า sentinel เฉพาะที่แยกจากกันสามารถใช้งานได้อย่างมั่นใจกว่าโดยไม่ต้องกังวลกับ edge case ที่อาจเกิดขึ้น
    • ค่า sentinel ควรสามารถมีชื่อและ repr ที่มีความหมายและเหมาะกับบริบทการใช้งานได้
    • ตัวเลือกนี้มีผู้เลือกเพียง 12% ในการโหวต จึงได้รับความนิยมน้อยมาก
  • ใช้ค่า sentinel Ellipsis ที่มีอยู่แล้ว

    • เดิมที Ellipsis ไม่ได้ถูกออกแบบมาเพื่อวัตถุประสงค์นี้
    • แม้จะถูกนำไปใช้เพิ่มขึ้นในการกำหนดบล็อก class หรือ function ว่างแทน pass แต่ก็ยังไม่สามารถใช้งานได้อย่างมั่นใจในทุกกรณีเท่ากับค่า sentinel เฉพาะที่แยกจากกัน
  • ใช้ Enum ที่มีค่าเดียว

    • รูปแบบที่เสนอมีดังนี้
    class NotGivenType(Enum):  
      NotGiven = &#039;NotGiven&#039;  
      NotGiven = NotGivenType.NotGiven  
    
  • มีความซ้ำซ้อนมากเกินไป และ repr ก็ยาวเกินไป เช่น &lt;NotGivenType.NotGiven: &#039;NotGiven&#039;&gt;
  • แม้จะกำหนด repr ให้สั้นลงได้ แต่ก็ทำให้โค้ดและความซ้ำซ้อนเพิ่มขึ้นอีก
  • เป็นตัวเลือกเดียวจาก 9 ตัวเลือกในแบบโหวตที่ไม่ได้รับคะแนนเลย จึงได้รับความนิยมน้อยที่สุด
  • ตัวตกแต่งคลาสสำหรับ sentinel

    • รูปแบบที่เสนอมีดังนี้
      @sentinel  
      class NotGivenType: pass  
      NotGiven = NotGivenType()  
      
    • แม้การ implement ตัวตกแต่งจะเรียบง่ายและชัดเจนได้ แต่รูปแบบการใช้งานยาวเกินไป ซ้ำซ้อน และจำได้ยาก
  • ใช้วัตถุคลาส

    • คลาสเป็น singleton โดยพื้นฐานอยู่แล้ว จึงพอจะคิดใช้เป็นค่า sentinel ได้
    • รูปแบบที่ง่ายที่สุดมีดังนี้
      class NotGiven: pass  
      
      • หากต้องการ repr ที่ชัดเจน จะต้องใช้ metaclass หรือตัวตกแต่งคลาส
      class NotGiven(metaclass=SentinelMeta): pass  
      
      @Sentinel  
      class NotGiven: pass  
      
    • การใช้คลาสในลักษณะนี้เป็นเรื่องไม่ปกติ จึงอาจทำให้สับสนได้
    • หากไม่มีคำอธิบายประกอบ ก็ยากจะเข้าใจเจตนาของโค้ด และยังเกิดพฤติกรรมที่ไม่คาดคิดและไม่พึงประสงค์ เช่น sentinel กลายเป็นสิ่งที่เรียกใช้ได้
  • กำหนดเพียงรูปแบบมาตรฐานที่แนะนำโดยไม่มี implementation

    • รูปแบบที่มีอยู่เดิมซึ่งใช้กันทั่วไปส่วนใหญ่มีข้อเสียสำคัญ
    • จนถึงตอนนี้ยังไม่พบรูปแบบที่ชัดเจนและกระชับ โดยหลีกเลี่ยงข้อเสียเหล่านี้ได้
    • ในการโหวตที่เกี่ยวข้อง ตัวเลือกแบบแนะนำรูปแบบได้รับความนิยมน้อย และแม้แต่ตัวเลือกที่ได้คะแนนมากที่สุดก็ยังมีเพียง 25%
  • ใช้โมดูลใหม่ใน standard library

    • ร่างแรกเสนอแนวทางเพิ่มคลาส Sentinel ในโมดูลใหม่ชื่อ sentinels หรือ sentinellib
    • การเพิ่มโมดูลใหม่เพียงเพื่อ callable สาธารณะตัวเดียวถือว่าไม่จำเป็น
    • การใช้โมดูลทำให้การใช้งานไม่สะดวกไปกว่ารูปแบบ object() ที่มีอยู่เดิม
    • Steering Council ก็แนะนำอย่างชัดเจนเช่นกันว่าควรทำให้เป็นความสามารถแบบ built-in ที่ใช้ง่ายพอ ๆ กับ object()
    • ชื่อ sentinels ยังชนกับแพ็กเกจบน PyPI ที่มีการใช้งานอย่างต่อเนื่องอยู่แล้ว และการทำเป็น built-in จะช่วยหลีกเลี่ยงปัญหาเรื่องชื่อได้
  • ใช้รีจิสทรีชื่อ sentinel รายโมดูล

    • ร่างแรกเสนอให้ทำให้ชื่อ sentinel มีเอกลักษณ์ภายในโมดูล
    • ในดีไซน์นี้ หากเรียก sentinel("MISSING") ซ้ำในโมดูลเดียวกัน ก็จะคืนอ็อบเจ็กต์เดิมผ่านรีจิสทรีระดับโปรเซสแบบ global ที่ใช้ชื่อโมดูลและชื่อ sentinel เป็นคีย์
    • พฤติกรรมนี้มีความเป็นนัยมากเกินไป จึงถูกปฏิเสธ
    • หากต้องการ sentinel ที่ใช้ร่วมกัน ก็สามารถกำหนดไว้ตัวเดียวอย่างชัดเจนเหมือน MISSING = object() และนำกลับมาใช้ซ้ำตามชื่อได้
    • ใน local scope อาจต้องการ sentinel ใหม่ทุกครั้งที่เรียกหรือทำซ้ำ ดังนั้นการเรียก sentinel(name) ซ้ำควรสร้างอ็อบเจ็กต์ที่ต่างกันเหมือนกับการเรียก object() ซ้ำ
    • เมื่อตัดรีจิสทรีออก implementation และโมเดลความคิดก็เรียบง่ายขึ้น เหลือเพียงกฎว่า sentinel(name) จะสร้างอ็อบเจ็กต์ใหม่ที่ไม่ซ้ำกัน โดยมี repr เป็น name
  • ค้นพบหรือส่งต่อชื่อโมดูลโดยอัตโนมัติ

    • ร่างแรกเสนออาร์กิวเมนต์ module_name แบบเลือกได้ เพื่อรองรับดีไซน์ที่อิงรีจิสทรี
    • เมื่อรีจิสทรีถูกถอดออก อาร์กิวเมนต์ module_name แบบสาธารณะก็ไม่จำเป็นต่อข้อเสนอหลักอีกต่อไป
    • implementation จะบันทึกโมดูลที่เรียกไว้ภายใน เพื่อให้การ pickle สามารถ serialize sentinel ที่ import ได้ด้วยโมดูลและชื่อ คล้ายกับ TypeVar
    • ชื่อโมดูลภายในนี้ไม่มีผลต่อ repr ของ sentinel
    • หากต้องการ repr ที่มีชื่อโมดูลหรือชื่อคลาสรวมอยู่ด้วย ก็สามารถระบุไว้ในอาร์กิวเมนต์ name เดียวได้โดยตรง เช่น sentinel("mymodule.MISSING")
  • อนุญาตให้ปรับแต่ง repr

    • แนวทางนี้มีข้อดีตรงที่ช่วยย้ายค่า sentinel เดิมมาใช้รูปแบบนี้ได้โดยไม่ต้องเปลี่ยน repr
    • แต่สุดท้ายถูกตัดออก เพราะเห็นว่าไม่คุ้มกับความซับซ้อนที่เพิ่มขึ้น
  • อนุญาตให้ปรับแต่งการประเมินค่าแบบบูลีน

    • ในการอภิปราย มีการพิจารณาแนวทางที่ทำให้ sentinel ถูกกำหนดให้เป็นค่าจริง ค่าปลอม หรือแปลงเป็น bool ไม่ได้อย่างชัดเจน
    • sentinel ของ third-party บางตัวเปิดให้พฤติกรรมแบบค่าปลอมเป็นส่วนหนึ่งของ public API
    • ผู้เข้าร่วมหลายคนเห็นว่าการให้เกิดข้อยกเว้นในบริบทแบบบูลีนนั้นช่วยบังคับการตรวจสอบเอกลักษณ์ได้ดีกว่า
    • PEP เลือกคงข้อเสนอเริ่มต้นให้เรียบง่าย โดยใช้พฤติกรรมค่า truthy ปกติของอ็อบเจ็กต์ทั่วไป และแนะนำให้ใช้การตรวจสอบเอกลักษณ์
    • พฤติกรรมบูลีนแบบปรับแต่งได้อาจนำกลับมาพิจารณาในภายหลัง หากเห็นว่าคุ้มกับความซับซ้อนเพิ่มเติมทั้งใน API และการระบุชนิด
  • ใช้ typing.Literal ใน type annotation

    • มีหลายคนเสนอแนวทางนี้ระหว่างการอภิปราย และในตอนแรก PEP ก็รับแนวทางนี้ไว้
    • อย่างไรก็ตาม Literal["MISSING"] อาจทำให้สับสน เพราะไม่ได้เป็น forward reference ไปยังค่า sentinel MISSING แต่หมายถึงค่าสตริง "MISSING"
    • การใช้ bare name ก็ถูกเสนออยู่บ่อยครั้งในการอภิปรายเช่นกัน
    • วิธี bare name สอดคล้องกับแบบอย่างที่ None สร้างไว้และรูปแบบที่รู้จักกันดี โดยไม่ต้อง import และสั้นกว่ามาก

แนวทางการใช้งานเพิ่มเติม

  • หากกำหนด sentinel ใน class scope ต้องการหลีกเลี่ยงการชนกันของชื่อ หรือกรณีที่ repr แบบระบุขอบเขตชัดเจนจะเข้าใจง่ายกว่า ควรส่งชื่อแบบมีขอบเขตที่ต้องการเข้าไปอย่างชัดเจน
    &gt;&gt;&gt; class MyClass:  
    ...    NotGiven = sentinel(&#039;MyClass.NotGiven&#039;)  
    &gt;&gt;&gt; MyClass.NotGiven  
    MyClass.NotGiven  
    
  • อนุญาตให้สร้าง sentinel ภายในฟังก์ชันหรือเมธอดได้
  • แต่ละการเรียก sentinel() จะสร้างอ็อบเจ็กต์ที่แตกต่างกัน ดังนั้น sentinel ที่สร้างใน local scope จะทำงานเหมือนค่าที่สร้างจากการเรียก object() ภายใน scope นั้น
  • ค่า boolean ของ NotImplemented คือ True แต่การใช้งานในลักษณะนี้ถูกกำหนดให้เลิกใช้ตั้งแต่ Python 3.9 และจะทำให้เกิด deprecation warning
  • การเลิกใช้นี้เกิดจากปัญหาเฉพาะของ NotImplemented ที่อธิบายไว้ใน bpo-35712 [8]
  • หากต้องกำหนดค่า sentinel ที่เกี่ยวข้องกันหลายค่า หรือกำหนดลำดับระหว่างค่าพวกนี้ ควรใช้ Enum หรือแนวทางที่คล้ายกัน
  • เรื่องการระบุชนิดของ sentinel เหล่านี้มีการอภิปรายหลายทางเลือกใน mailing list ของ typing-sig [9]

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

 
GN⁺ 1 시간 전
ความคิดเห็นจาก Lobste.rs
  • รู้สึกแปลก ๆ ที่ชื่อที่เลือกดูเหมือนจะมีความหมายแคบเกินไป
    ถ้าดูจากชื่ออย่างเดียว ดูเหมือนอะไรที่คล้าย สัญลักษณ์เฉพาะตัว จะเป็น primitive ที่ยืดหยุ่นกว่า จริง ๆ แล้วมันก็น่าจะทำงานคล้าย symbol อยู่มาก เลยอาจใช้แบบนั้นได้ แต่การตั้งชื่อว่า “Sentinels” ก็ยังฟังดูแปลก ๆ อาจเป็นเพราะคุ้นกับ Lisp ด้วย

    • ดูเหมือนเป้าหมายคือทำให้ SENTINEL_A เป็น คนละ type กับ SENTINEL_B เพื่อที่จะถามได้ว่าค่าหนึ่ง is_a SENTINEL_A หรือไม่
      symbol ของ Ruby ไม่ได้ทำงานแบบนั้น: :beef.is_a? :droog.class #=> true
    • แนวคิดแบบ Lisp นั้นถูกต้อง กำลังตั้งต้นจากสมมติฐานว่าการใช้งานแบบกว้าง ๆ เป็นสิ่งพึงประสงค์และเป็นปัญหาที่ต้องแก้ แต่ใน Python ก็มี Literal และสตริงลิเทอรัลสำหรับกรณีใช้งานส่วนใหญ่ของ symbol แบบ Lisp อยู่แล้ว
      ที่เรียกสิ่งเหล่านี้ว่า sentinel แบบมีชื่อ ก็เพราะ sentinel values เป็นแนวคิดและแพตเทิร์นที่พบได้บ่อยใน Python และ sentinel มีไว้เพื่อแก้ปัญหาบางส่วนที่เกิดจากการใช้แพตเทิร์นนั้นแบบเฉพาะทาง ตามที่อธิบายไว้ตรงส่วน “Motivation” และ “Rationale”
      นอกจากนี้ sentinel ไม่มี value semantics ดังนั้น sentinel สองตัวที่ชื่อเดียวกันก็ยังเป็นคนละค่าและไม่เท่ากัน จึงไม่ได้ทำงานเหมือน symbol และไม่ควรใช้แบบนั้น
  • สำหรับปัญหาค่าเริ่มต้นของ named argument ใน Typst แค่เพิ่ม ค่า auto ควบคู่กับ none ก็แทบจะพอแสดงอินเทอร์เฟซ named argument ที่ต้องการได้ทั้งหมดแล้ว
    none เพียงอย่างเดียวมักให้ความหมายไม่เหมาะกับค่าเริ่มต้นของ named argument ส่วนใหญ่ none เหมาะเป็นค่า return เริ่มต้น แต่เมื่อเป็น argument ของฟังก์ชัน มันมักสื่อความหมายในฐานะคำนามได้ไม่ถูกต้อง เช่น matrix(axes=None) จะหมายถึงเอาแกนออก หรือให้คงไว้ตามปกติ ก็ยังไม่ชัดเจน และก็ไม่แน่ชัดด้วยว่าการส่ง none กับการไม่ส่งอะไรเลยต่างกันหรือไม่ ถ้าจะไปใช้ multiple dispatch เพื่อแยกว่ามีพารามิเตอร์นี้หรือไม่ ก็จะเสียจุดศูนย์กลางในการอธิบายพฤติกรรมของพารามิเตอร์นั้นในเอกสารไป
    auto เป็นค่าเริ่มต้นที่ยอดเยี่ยม เพราะสื่อความหมายตรงตัวว่า “จัดการให้เหมาะสมตามข้อมูลที่มี” ซิกเนเจอร์ auto | none ใช้ได้เหมือนบูลีนที่ชัดเจนกว่า และ T | auto | none ก็ให้ข้อมูลได้มากพอสมควรว่าฟังก์ชันจะใช้ค่านั้นอย่างไร เช่น ถ้า T คือ color ก็มีแนวโน้มว่า auto จะเลือกค่าเริ่มต้นอย่างสีขาว/ดำหรือสืบทอดจาก parent, T คือการตั้งค่าสีแบบชัดเจน, และ none อาจหมายถึงไม่ตั้งค่าสีเลยหรือทำให้โปร่งใส ขึ้นอยู่กับบริบท

  • น่าสนใจดี และก็สงสัยว่า semantics ของบางแพ็กเกจจะเปลี่ยนไปอย่างไร ตัวอย่างเช่น แทนที่จะคืนค่า Item | None อาจเขียนแบบนี้ได้

    NOT_FOUND = sentinel("NOT_FOUND")  
    def get_item(iid: str) -> Item | NOT_FOUND: ...  
    

    แน่นอนว่ายังอาจใช้หลาย sentinel เพื่อใส่ความหมายเพิ่มเติมได้ เดิมทีก็ทำได้อยู่แล้ว แต่ยังไม่มีวิธีที่ “ได้รับการแนะนำอย่างเป็นทางการ” ในเอกสาร สิ่งนี้อาจพาผู้เขียนแพ็กเกจไปอีกทิศทางหนึ่ง

    MISSING_ID = sentinel("MISSING_ID")  
    MISSING_VALUE = sentinel("MISSING_VALUE")
    
    def get_item(iid: str) -> Item | MISSING_ID | MISSING_VALUE: ...  
    

    แม้จะเป็นตัวอย่างที่ค่อนข้างฝืน แต่ในกรณีนี้สามารถแยกได้ระหว่างสถานการณ์ที่มี ID เดิมอยู่แต่ไม่มีค่าที่เชื่อมไว้ กับสถานการณ์ที่ล้มเหลวเพราะไม่มี ID นั้นอยู่เลย วิธีแบบ “Pythonic” น่าจะเป็นการใช้ exception มากกว่า แต่ก็ดูเป็น แนวทางเชิงฟังก์ชัน มากกว่าที่ปกติมักเขียน Python กัน

    • ดูเหมือนเป็นวิธีที่สะอาดกว่าในการใช้ singleton ซึ่งเมื่อก่อนมักสร้างคลาสหลอกขึ้นมาแล้ว instantiate แยกตามโมดูล
      class _MissingId: ...
      
      MISSING_ID = _MissingId()
      
      # elsewhere  
      from ... import MISSING_ID  
      
      ทำให้นึกถึง Symbols
    • ใน PEP บอกว่าถ้าจะนิยาม sentinel หลายค่าที่เกี่ยวข้องกัน หรือถึงขั้นกำหนดลำดับการเรียงระหว่างพวกมัน ก็ควรใช้ Enum หรือสิ่งที่คล้ายกันแทน
  • คิดว่าน่าจะดีกว่าถ้าแค่นำ Symbol API ของ JavaScript มาใช้ตรง ๆ เพราะมีประโยชน์ในภาพรวมอยู่แล้ว และก็แก้ปัญหาที่ต้องการแก้ในที่นี้ได้ด้วย