PEP 661 – ค่า Sentinel ได้รับการอนุมัติหลังจาก 5 ปี
(peps.python.org)- PEP 661 เสนออ็อบเจ็กต์ callable แบบ built-in ของ Python คือ
sentinel()และ C APIPySentinel_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 ของฟังก์ชันได้ยาก>>> help(traceback.print_exception) Help on function print_exception in module traceback: print_exception(exc, /, value=<object object at 0x000002825DF09650>, tb=<object object at 0x000002825DF09650>, 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>)")ได้
- แนวทางนี้คล้ายกับวิธีที่ระบบชนิดเดิมจัดการกับ
NoneMISSING = 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 notfrom 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: """Unique sentinel values.""" __slots__ = ("__name__", "_module_name") def __init_subclass__(cls): raise TypeError("type 'sentinel' is not an acceptable base type") def __init__(self, name, /): if not isinstance(name, str): raise TypeError("sentinel name must be a string") 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 = 'NotGiven' NotGiven = NotGivenType.NotGiven - มีความซ้ำซ้อนมากเกินไป และ
reprก็ยาวเกินไป เช่น<NotGivenType.NotGiven: 'NotGiven'> - แม้จะกำหนด
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 เดิมมาใช้รูปแบบนี้ได้โดยไม่ต้องเปลี่ยน
-
อนุญาตให้ปรับแต่งการประเมินค่าแบบบูลีน
- ในการอภิปราย มีการพิจารณาแนวทางที่ทำให้ sentinel ถูกกำหนดให้เป็นค่าจริง ค่าปลอม หรือแปลงเป็น
boolไม่ได้อย่างชัดเจน - sentinel ของ third-party บางตัวเปิดให้พฤติกรรมแบบค่าปลอมเป็นส่วนหนึ่งของ public API
- ผู้เข้าร่วมหลายคนเห็นว่าการให้เกิดข้อยกเว้นในบริบทแบบบูลีนนั้นช่วยบังคับการตรวจสอบเอกลักษณ์ได้ดีกว่า
- PEP เลือกคงข้อเสนอเริ่มต้นให้เรียบง่าย โดยใช้พฤติกรรมค่า truthy ปกติของอ็อบเจ็กต์ทั่วไป และแนะนำให้ใช้การตรวจสอบเอกลักษณ์
- พฤติกรรมบูลีนแบบปรับแต่งได้อาจนำกลับมาพิจารณาในภายหลัง หากเห็นว่าคุ้มกับความซับซ้อนเพิ่มเติมทั้งใน API และการระบุชนิด
- ในการอภิปราย มีการพิจารณาแนวทางที่ทำให้ sentinel ถูกกำหนดให้เป็นค่าจริง ค่าปลอม หรือแปลงเป็น
-
ใช้
typing.Literalใน type annotation- มีหลายคนเสนอแนวทางนี้ระหว่างการอภิปราย และในตอนแรก PEP ก็รับแนวทางนี้ไว้
- อย่างไรก็ตาม
Literal["MISSING"]อาจทำให้สับสน เพราะไม่ได้เป็น forward reference ไปยังค่า sentinelMISSINGแต่หมายถึงค่าสตริง"MISSING" - การใช้ bare name ก็ถูกเสนออยู่บ่อยครั้งในการอภิปรายเช่นกัน
- วิธี bare name สอดคล้องกับแบบอย่างที่
Noneสร้างไว้และรูปแบบที่รู้จักกันดี โดยไม่ต้อง import และสั้นกว่ามาก
แนวทางการใช้งานเพิ่มเติม
- หากกำหนด sentinel ใน class scope ต้องการหลีกเลี่ยงการชนกันของชื่อ หรือกรณีที่
reprแบบระบุขอบเขตชัดเจนจะเข้าใจง่ายกว่า ควรส่งชื่อแบบมีขอบเขตที่ต้องการเข้าไปอย่างชัดเจน>>> class MyClass: ... NotGiven = sentinel('MyClass.NotGiven') >>> 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 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
รู้สึกแปลก ๆ ที่ชื่อที่เลือกดูเหมือนจะมีความหมายแคบเกินไป
ถ้าดูจากชื่ออย่างเดียว ดูเหมือนอะไรที่คล้าย สัญลักษณ์เฉพาะตัว จะเป็น primitive ที่ยืดหยุ่นกว่า จริง ๆ แล้วมันก็น่าจะทำงานคล้าย symbol อยู่มาก เลยอาจใช้แบบนั้นได้ แต่การตั้งชื่อว่า “Sentinels” ก็ยังฟังดูแปลก ๆ อาจเป็นเพราะคุ้นกับ Lisp ด้วย
SENTINEL_Aเป็น คนละ type กับSENTINEL_Bเพื่อที่จะถามได้ว่าค่าหนึ่งis_a SENTINEL_Aหรือไม่symbol ของ Ruby ไม่ได้ทำงานแบบนั้น:
:beef.is_a? :droog.class #=> trueLiteralและสตริงลิเทอรัลสำหรับกรณีใช้งานส่วนใหญ่ของ 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อาจเขียนแบบนี้ได้แน่นอนว่ายังอาจใช้หลาย sentinel เพื่อใส่ความหมายเพิ่มเติมได้ เดิมทีก็ทำได้อยู่แล้ว แต่ยังไม่มีวิธีที่ “ได้รับการแนะนำอย่างเป็นทางการ” ในเอกสาร สิ่งนี้อาจพาผู้เขียนแพ็กเกจไปอีกทิศทางหนึ่ง
แม้จะเป็นตัวอย่างที่ค่อนข้างฝืน แต่ในกรณีนี้สามารถแยกได้ระหว่างสถานการณ์ที่มี ID เดิมอยู่แต่ไม่มีค่าที่เชื่อมไว้ กับสถานการณ์ที่ล้มเหลวเพราะไม่มี ID นั้นอยู่เลย วิธีแบบ “Pythonic” น่าจะเป็นการใช้ exception มากกว่า แต่ก็ดูเป็น แนวทางเชิงฟังก์ชัน มากกว่าที่ปกติมักเขียน Python กัน
คิดว่าน่าจะดีกว่าถ้าแค่นำ
SymbolAPI ของ JavaScript มาใช้ตรง ๆ เพราะมีประโยชน์ในภาพรวมอยู่แล้ว และก็แก้ปัญหาที่ต้องการแก้ในที่นี้ได้ด้วย