1 điểm bởi GN⁺ 1 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • PEP 661 đề xuất đối tượng gọi được dựng sẵn sentinel() của Python và C API PySentinel_New() để tạo giá trị sentinel có thể phân biệt riêng trong những trường hợp None là một giá trị hợp lệ
  • Cách làm quen thuộc _sentinel = object() hiện nay có repr dài và không rõ ràng trong chữ ký hàm, đồng thời có thể gây ra vấn đề với chữ ký kiểu rõ ràng, sao chép và pickle
  • Lời gọi sentinel('MISSING') tạo ra một đối tượng duy nhất mới với repr ngắn; nếu muốn dùng chung cùng một sentinel thì phải gán vào biến và tái sử dụng một cách tường minh như MISSING = sentinel('MISSING')
  • Khuyến nghị so sánh sentinel bằng is, sentinel được đánh giá là giá trị đúng, copy.copy()copy.deepcopy() trả về cùng một đối tượng, và nếu có thể import theo tên từ module thì danh tính của nó vẫn được giữ nguyên sau khi pickle
  • Hệ thống kiểu cho phép dùng chính sentinel trong biểu thức kiểu như int | MISSING, và tài liệu chính thức mới nhất hiện có trong tài liệu [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)") của Python 3.15

Bối cảnh ra đời

  • Giá trị sentinel (sentinel value), tức giá trị giữ chỗ duy nhất, được dùng làm giá trị mặc định khi đối số hàm không được truyền vào, giá trị trả về để biểu thị việc tìm kiếm thất bại, hoặc giá trị biểu thị dữ liệu bị thiếu
  • Python thường có giá trị đặc biệt None dùng cho mục đích này, nhưng trong những ngữ cảnh mà bản thân None cũng là một giá trị hợp lệ thì cần một giá trị sentinel riêng biệt để phân biệt với None
  • Tháng 5/2021, danh sách thư python-dev đã thảo luận về cách triển khai tốt hơn giá trị sentinel dùng trong traceback.print_exception
  • Cách triển khai hiện có dùng thành ngữ phổ biến _sentinel = object(), nhưng repr của nó quá dài và thiếu thông tin, khiến chữ ký hàm khó đọc
    &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)  
    
  • Trong quá trình thảo luận, người ta cũng xác nhận thêm các vấn đề khác của những cách triển khai sentinel hiện có
    • Một số sentinel không có kiểu riêng, nên khó định nghĩa chữ ký kiểu rõ ràng cho các hàm dùng sentinel làm giá trị mặc định
    • Sau khi sao chép có thể sinh ra một thực thể khác, khiến so sánh bằng is thất bại và hành vi khác với kỳ vọng
    • Một số thành ngữ phổ biến cũng gặp vấn đề tương tự sau khi unpickle từ dữ liệu đã pickle
  • Victor Stinner đã cung cấp danh sách các giá trị sentinel được dùng trong thư viện chuẩn Python, và từ đó xác nhận rằng ngay trong thư viện chuẩn cũng có nhiều cách triển khai khác nhau, với nhiều cách mắc một hoặc nhiều vấn đề nêu trên
  • Cuộc bỏ phiếu trên discuss.python.org không đưa ra được kết luận rõ ràng với tổng cộng 39 phiếu
    • 40% chọn “hiện trạng là ổn và không cần tính nhất quán”
    • Đa số chọn một hoặc nhiều giải pháp chuẩn hóa
    • 37% chọn phương án “dùng nhất quán một factory/lớp/metaclass sentinel chuyên dụng mới và công khai cung cấp trong thư viện chuẩn”
  • Chính vì kết quả trái chiều đó mà PEP đã được viết, dẫn tới kết luận rằng một cách triển khai đơn giản và tốt trong thư viện chuẩn sẽ hữu ích cả bên trong lẫn bên ngoài thư viện chuẩn
  • Việc chuyển toàn bộ sentinel hiện có của thư viện chuẩn sang cách này không phải yêu cầu bắt buộc, mà được để cho người bảo trì tương ứng tự quyết định
  • Tài liệu PEP là tài liệu mang tính lịch sử; tài liệu chính thức mới nhất hiện có trong tài liệu [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)") của Python 3.15

Tiêu chí thiết kế

  • Đối tượng sentinel khi so sánh bằng toán tử is phải luôn đồng nhất với chính nó và không đồng nhất với bất kỳ đối tượng nào khác
  • Việc tạo đối tượng sentinel phải là một dòng mã đơn giản và trực quan
  • Phải có thể dễ dàng định nghĩa nhiều giá trị sentinel khác nhau tùy nhu cầu
  • Đối tượng sentinel phải có repr ngắn gọn và rõ ràng
  • Phải có thể dùng chữ ký kiểu rõ ràng cho sentinel
  • Phải hoạt động đúng cả sau khi sao chép, và có hành vi có thể dự đoán được khi pickle và unpickle
  • Phải chạy được trên CPython 3.x và PyPy3, và nếu có thể thì cả trên các bản triển khai Python khác
  • Cả việc triển khai lẫn sử dụng đều phải đơn giản và trực quan nhất có thể, để không trở thành thêm một khái niệm đặc biệt gây gánh nặng khi học Python
  • Thư viện chuẩn không thể phụ thuộc vào các gói PyPI như sentinels hay sentinel, nên cần một cách triển khai có thể dùng ngay trong thư viện chuẩn

Đặc tả sentinel()

  • Đã bổ sung một đối tượng callable built-in mới là sentinel
    >>> MISSING = sentinel('MISSING')  
    >>> MISSING  
    MISSING  
    
  • sentinel() nhận một đối số chỉ-vị-trí là name, và name bắt buộc phải là str
  • Nếu truyền vào giá trị không phải chuỗi thì sẽ phát sinh TypeError
  • name được dùng làm tên sentinel và làm repr của nó
  • Đối tượng sentinel có hai thuộc tính công khai
    • __name__: tên sentinel
    • __module__: tên mô-đun nơi sentinel() được gọi
  • sentinel không thể bị subclass
  • Mỗi lần gọi sentinel(name) sẽ trả về một đối tượng sentinel mới
  • Nếu cần dùng cùng một sentinel ở nhiều nơi, phải gán nó vào biến rồi tái sử dụng rõ ràng cùng một đối tượng, tương tự idiom MISSING = object() hiện có
    MISSING = sentinel('MISSING')  
    
    def read_value(default=MISSING):  
        ...  
    
  • Khi kiểm tra một giá trị có phải là sentinel hay không, cách dùng toán tử is được khuyến nghị, giống như với None
  • So sánh == cũng hoạt động đúng như mong đợi, chỉ trả về True khi so sánh với chính nó
  • Kiểm tra đồng nhất như if value is MISSING: thường phù hợp hơn kiểm tra boolean như if value: hoặc if not value:
  • Đối tượng sentinel là truthy, và khi đánh giá boolean sẽ cho kết quả True
    • Điều này giống hành vi mặc định của các lớp tùy ý và giá trị boolean của Ellipsis
    • Khác với None, vốn là falsy
  • Nếu sao chép đối tượng sentinel bằng copy.copy() hoặc copy.deepcopy(), cùng chính đối tượng đó sẽ được trả về
  • Sentinel có thể được import theo tên từ mô-đun nơi nó được định nghĩa sẽ giữ nguyên tính đồng nhất sau khi pickle và unpickle theo cơ chế pickle tiêu chuẩn
    MISSING = sentinel('MISSING')  
    assert pickle.loads(pickle.dumps(MISSING)) is MISSING  
    
  • sentinel() ghi lại mô-đun gọi tại thời điểm tạo sentinel vào thuộc tính __module__
  • Quá trình pickle ghi lại sentinel theo mô-đun và tên, còn unpickle sẽ import mô-đun rồi lấy sentinel theo tên
  • Những sentinel không thể import bằng mô-đun và tên, chẳng hạn sentinel được tạo trong scope cục bộ và không được gán vào tên tương ứng ở global của mô-đun hoặc thuộc tính lớp, thì không thể pickle
  • repr của đối tượng sentinel chính là name được truyền vào sentinel(), không tự động kèm thêm định danh mô-đun
  • Nếu cần repr có định danh đầy đủ thì phải đưa nó vào tên một cách tường minh
    >>> MyClass_NotGiven = sentinel('MyClass.NotGiven')  
    >>> MyClass_NotGiven  
    MyClass.NotGiven  
    
  • So sánh thứ tự của đối tượng sentinel không được định nghĩa
  • Sentinel không hỗ trợ weakref

Gán kiểu

  • Để việc dùng sentinel trong mã Python có gán kiểu trở nên rõ ràng và đơn giản, hệ thống kiểu được bổ sung xử lý đặc biệt cho đối tượng sentinel
  • Đối tượng sentinel có thể được dùng trong biểu thức kiểu") như một giá trị đại diện cho chính nó
  • Điều này tương tự cách hệ thống kiểu hiện có xử lý None
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING = MISSING) -> int:  
        ...  
    
  • Trình kiểm tra kiểu phải nhận diện việc tạo sentinel dạng NAME = sentinel('NAME') là tạo ra một đối tượng sentinel mới
  • Nếu tên truyền vào sentinel() không khớp với tên đích của phép gán, trình kiểm tra kiểu phải báo lỗi
  • Sentinel được định nghĩa bằng cú pháp này có thể dùng trong biểu thức kiểu")
  • Kiểu của sentinel tương ứng biểu thị một kiểu tĩnh hoàn toàn") chỉ có đúng một thành viên là chính đối tượng sentinel đó
  • Trình kiểm tra kiểu phải hỗ trợ thu hẹp union type có chứa sentinel bằng các toán tử isis 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)  
    
  • Triển khai runtime phải có các phương thức __or____ror__ để hỗ trợ dùng trong biểu thức kiểu, và các phương thức này trả về đối tượng typing.Union
  • Typing Council đã ủng hộ phần liên quan đến typing của đề xuất này

C API

  • Sentinel cũng có thể hữu ích trong extension C, nên hai hàm C API mới được đề xuất
  • PyObject *PySentinel_New(const char *name, const char *module_name) tạo một đối tượng sentinel mới
  • bool PySentinel_Check(PyObject *obj) kiểm tra xem một đối tượng có phải sentinel hay không
  • Mã C có thể dùng toán tử == để kiểm tra có phải một sentinel cụ thể hay không

Tương thích và bảo mật

  • Khi bổ sung một tên built-in mới, mã hiện đang giả định bare name sentinel sẽ gây ra NameError sẽ không còn thấy cùng kết quả như trước
  • Đây là một cân nhắc tương thích thường gặp khi thêm tên built-in mới
  • Các tên cục bộ, toàn cục hoặc đã import sẵn có tên sentinel sẽ không bị ảnh hưởng
  • Mã đã dùng tên sentinel có thể cần được điều chỉnh để dùng đối tượng built-in mới, và có thể nhận cảnh báo mới từ các linter vốn cảnh báo xung đột với tên built-in
  • Người ta cho rằng cách tài liệu hóa thông thường cho tính năng built-in mới như docstring, tài liệu thư viện và mục “What’s New” là đủ
  • Đề xuất này được xem là không có tác động bảo mật

Bản triển khai tham chiếu và backport

  • Bản triển khai tham chiếu được cung cấp dưới dạng pull request của CPython [10]
  • Bản triển khai tham chiếu trước đó nằm trong một kho GitHub riêng [7]
  • Phác thảo hành vi dự kiến như sau
    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]  
    

Các phương án thay thế bị bác bỏ

  • Dùng NotGiven = object()

    • Cách này có tất cả các nhược điểm đã được nêu trong tiêu chí thiết kế của PEP
    • repr dài và không rõ ràng, khó làm cho type signature rõ ràng, và có thể phát sinh vấn đề liên quan đến sao chép hoặc pickling
  • Thêm một giá trị sentinel mới duy nhất như MISSING hoặc Sentinel

    • Nếu một giá trị được dùng ở nhiều nơi cho nhiều mục đích, không phải lúc nào cũng có thể chắc chắn rằng trong một số trường hợp sử dụng, chính giá trị đó sẽ không phải là giá trị hợp lệ
    • Các giá trị sentinel chuyên dụng, tách biệt có thể được dùng tự tin hơn mà không cần cân nhắc các edge case tiềm ẩn
    • Giá trị sentinel cần có khả năng cung cấp tên và repr có ý nghĩa, phù hợp với ngữ cảnh sử dụng
    • Phương án này rất kém phổ biến, chỉ được 12% phiếu bầu chọn
  • Dùng giá trị sentinel Ellipsis hiện có

    • Ellipsis ban đầu không phải là giá trị được thiết kế cho mục đích này
    • Dù ngày càng thường được dùng để định nghĩa thân class hoặc function rỗng thay cho pass, nó vẫn không thể được dùng tự tin trong mọi trường hợp như một giá trị sentinel chuyên dụng, tách biệt
  • Dùng Enum một giá trị duy nhất

    • Thành ngữ được đề xuất như sau
    class NotGivenType(Enum):  
      NotGiven = &#039;NotGiven&#039;  
      NotGiven = NotGivenType.NotGiven  
    
  • Cách này lặp lại quá mức, và repr quá dài, như &lt;NotGivenType.NotGiven: &#039;NotGiven&#039;&gt;
  • Dù có thể định nghĩa repr ngắn hơn, điều đó lại làm tăng thêm mã và phần lặp
  • Đây là lựa chọn kém phổ biến nhất, là lựa chọn duy nhất trong 9 phương án bỏ phiếu không nhận được lá phiếu nào
  • Decorator cho lớp sentinel

    • Thành ngữ được đề xuất như sau
      @sentinel  
      class NotGivenType: pass  
      NotGiven = NotGivenType()  
      
    • Bản thân việc triển khai decorator có thể đơn giản và rõ ràng, nhưng thành ngữ này quá dài dòng, lặp lại và khó nhớ
  • Dùng đối tượng lớp

    • Về bản chất, class là singleton nên ý tưởng dùng nó làm giá trị sentinel là khả thi
    • Dạng đơn giản nhất như sau
      class NotGiven: pass  
      
      • Để có repr rõ ràng thì cần metaclass hoặc class decorator
      class NotGiven(metaclass=SentinelMeta): pass  
      
      @Sentinel  
      class NotGiven: pass  
      
    • Dùng class theo cách này là điều bất thường nên có thể gây nhầm lẫn
    • Nếu không có chú thích thì khó hiểu được ý đồ của mã, và còn phát sinh các hành vi không mong muốn ngoài dự kiến như sentinel trở nên callable
  • Chỉ định nghĩa thành ngữ chuẩn được khuyến nghị mà không có triển khai

    • Phần lớn các thành ngữ hiện có phổ biến đều có những nhược điểm quan trọng
    • Cho đến nay vẫn chưa tìm ra được một thành ngữ rõ ràng và ngắn gọn mà tránh được các nhược điểm đó
    • Trong cuộc bỏ phiếu liên quan, phương án khuyến nghị thành ngữ có mức ủng hộ thấp, và ngay cả lựa chọn nhận nhiều phiếu nhất cũng chỉ đạt 25%
  • Dùng một module mới trong standard library

    • Bản nháp ban đầu đề xuất thêm lớp Sentinel vào module mới sentinels hoặc sentinellib
    • Việc thêm một module mới chỉ để có một đối tượng callable công khai là không cần thiết
    • Dùng module khiến việc sử dụng tính năng này bất tiện hơn so với thành ngữ object() hiện có
    • Steering Council cũng đã khuyến nghị cụ thể rằng nên biến nó thành tính năng built-in để có thể dùng dễ dàng như object()
    • Tên sentinels đã xung đột với một gói PyPI đang được dùng tích cực, và việc biến nó thành built-in sẽ tránh được vấn đề tên gọi
  • Dùng registry tên sentinel theo từng module

    • Bản nháp ban đầu đề xuất làm cho tên sentinel là duy nhất trong module
    • Trong thiết kế này, nếu lặp lại lời gọi sentinel("MISSING") trong cùng một module, nó sẽ trả về cùng một đối tượng thông qua registry toàn cục cấp tiến trình dùng tên module và tên sentinel làm khóa
    • Hành vi này bị bác bỏ vì quá ngầm định
    • Nếu cần sentinel dùng chung, có thể định nghĩa rõ ràng một giá trị rồi tái sử dụng theo tên, như MISSING = object() hiện nay
    • Trong local scope, đôi khi lại muốn một sentinel mới ở mỗi lần gọi hay mỗi lần lặp, nên việc gọi lặp sentinel(name) phải tạo ra các đối tượng khác nhau, giống như gọi lặp object()
    • Khi bỏ registry đi, cả triển khai lẫn mô hình tư duy đều đơn giản hơn, và chỉ còn quy tắc rằng sentinel(name) tạo ra một đối tượng mới, duy nhất, với reprname
  • Tự động phát hiện hoặc truyền tên module

    • Bản nháp ban đầu đề xuất đối số module_name tùy chọn để hỗ trợ thiết kế dựa trên registry
    • Khi registry bị loại bỏ, đối số công khai module_name không còn cần thiết cho đề xuất cốt lõi nữa
    • Phần triển khai sẽ ghi lại module gọi ở nội bộ, tương tự TypeVar, để pickle có thể tuần tự hóa sentinel có thể import theo module và tên
    • Tên module nội bộ không ảnh hưởng đến repr của sentinel
    • Nếu muốn repr chứa tên module hoặc tên class, có thể đưa chúng vào đối số name duy nhất một cách tường minh, như sentinel("mymodule.MISSING")
  • Cho phép tùy biến repr

    • Điều này có ưu điểm là có thể chuyển các giá trị sentinel hiện có sang cách làm này mà không cần thay đổi repr
    • Tuy nhiên, phương án này bị loại vì không đáng để đánh đổi bằng độ phức tạp bổ sung
  • Cho phép tùy biến phép đánh giá boolean

    • Trong thảo luận, đã xem xét phương án cho phép làm cho sentinel trở thành giá trị đúng, giá trị sai, hoặc không thể chuyển sang bool
    • Một số sentinel của bên thứ ba cung cấp hành vi falsy như một phần trong API công khai
    • Nhiều người tham gia cho rằng ném ngoại lệ trong ngữ cảnh boolean sẽ ép buộc kiểm tra identity tốt hơn
    • PEP giữ đề xuất ban đầu đơn giản bằng cách duy trì hành vi truthy mặc định của đối tượng thông thường và khuyến nghị dùng kiểm tra identity
    • Hành vi boolean tùy biến có thể được xem xét sau nếu được đánh giá là đáng để chấp nhận thêm độ phức tạp cho API và typing
  • Dùng typing.Literal trong type annotation

    • Nhiều người đã đề xuất cách này trong thảo luận, và ban đầu PEP cũng áp dụng nó
    • Tuy nhiên, Literal["MISSING"] có thể gây nhầm lẫn vì nó không phải là forward reference đến giá trị sentinel MISSING, mà lại chỉ đến giá trị chuỗi "MISSING"
    • Cách dùng bare name cũng thường xuyên được đề xuất trong thảo luận
    • Cách bare name đi theo tiền lệ do None tạo ra và mẫu quen thuộc, không cần import và ngắn hơn nhiều

Hướng dẫn sử dụng bổ sung

  • Khi định nghĩa sentinel trong class scope, khi muốn tránh xung đột tên, hoặc khi repr có qualifier sẽ rõ ràng hơn, thì nên truyền tường minh tên đầy đủ mong muốn
    &gt;&gt;&gt; class MyClass:  
    ...    NotGiven = sentinel(&#039;MyClass.NotGiven&#039;)  
    &gt;&gt;&gt; MyClass.NotGiven  
    MyClass.NotGiven  
    
  • Có thể tạo sentinel bên trong function hoặc method
  • Vì mỗi lời gọi sentinel() đều tạo ra một đối tượng khác nhau, sentinel tạo trong local scope sẽ hoạt động giống như giá trị được tạo bằng cách gọi object() trong scope đó
  • Giá trị boolean của NotImplementedTrue, nhưng từ Python 3.9, việc sử dụng theo cách này đã bị đưa vào diện ngừng hỗ trợ và sẽ phát ra deprecation warning
  • Việc ngừng hỗ trợ này là do các vấn đề riêng của NotImplemented, được mô tả trong bpo-35712 [8]
  • Nếu cần định nghĩa nhiều giá trị sentinel có liên quan, hoặc cần xác định thứ tự giữa chúng, thì nên dùng Enum hoặc cách tương tự
  • Về việc định kiểu cho các sentinel này, nhiều phương án đã được thảo luận trên mailing list typing-sig [9]

1 bình luận

 
Ý kiến trên Lobste.rs
  • Cái tên được chọn nghe hơi lạ vì có vẻ quá hẹp về nghĩa
    Chỉ nhìn tên thôi thì có lẽ một thành phần cơ bản linh hoạt hơn như symbol duy nhất sẽ hợp lý hơn. Trên thực tế nó gần như sẽ hoạt động giống symbol nên vẫn có thể dùng như vậy, nhưng việc đặt tên là “Sentinels” vẫn khá gượng. Có thể là do tôi quen với Lisp nên mới thấy thế

    • Có vẻ mục tiêu là để SENTINEL_A trở thành kiểu khác với SENTINEL_B, để có thể hỏi một giá trị nào đó có is_a SENTINEL_A hay không
      Symbol của Ruby không hoạt động như vậy: :beef.is_a? :droog.class #=> true
    • Cách nghĩ kiểu Lisp là hợp lý. Nhưng dù giả định rằng việc dùng rộng rãi là đáng mong muốn và là vấn đề cần giải quyết, Python đã có Literal và chuỗi literal cho hầu hết các trường hợp sử dụng symbol kiểu Lisp
      Việc chúng là các sentinel có tên là vì sentinel values vốn đã là một khái niệm và mẫu quen thuộc trong Python, và sentinel ở đây nhằm giải quyết hẹp một số vấn đề nảy sinh khi dùng mẫu đó. Đúng như phần “Motivation” và “Rationale” đã giải thích
      Ngoài ra, sentinel không có ngữ nghĩa giá trị, nên ngay cả hai sentinel cùng tên cũng là hai giá trị khác nhau và không bằng nhau. Vì vậy chúng cũng không hoạt động như symbol, và không nên được dùng theo cách đó
  • Với vấn đề giá trị mặc định cho đối số có tên, trong Typst chỉ cần thêm giá trị auto bên cạnh none là đã có thể biểu đạt gần như mọi giao diện đối số có tên mong muốn
    Chỉ none thôi thường không mang nghĩa phù hợp với giá trị mặc định của đa số đối số có tên. none ổn làm giá trị trả về mặc định, nhưng khi đi vào tham số hàm thì nhiều khi không truyền tải đúng ý nghĩa như một danh từ. matrix(axes=None) không rõ là có nghĩa bỏ trục đi, hay giữ nguyên như bình thường. Cũng không rõ việc truyền none có khác với không truyền gì cả hay không. Nếu chuyển sang multiple dispatch để phân biệt việc tham số có được truyền vào hay không, thì lại mất đi vị trí trung tâm để tài liệu hóa hành vi của tham số đó
    auto là một giá trị mặc định rất tốt vì nó diễn đạt đúng ý “hãy xử lý thích hợp dựa trên thông tin đang có”. Chữ ký auto | none có thể dùng như một kiểu boolean tường minh hơn, còn T | auto | none truyền tải khá nhiều thông tin về cách hàm sẽ dùng giá trị. Ví dụ nếu Tcolor, thì auto nhiều khả năng sẽ chọn giá trị mặc định như trắng/đen hoặc kế thừa từ cha, T là đặt màu một cách tường minh, còn none tùy ngữ cảnh có thể là hoàn toàn không đặt màu hoặc xử lý như trong suốt

  • Khá thú vị, và tôi tò mò xem ngữ nghĩa của một số package sẽ thay đổi ra sao. Ví dụ, thay vì trả về Item | None thì có thể viết như sau

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

    Tất nhiên cũng có thể dùng nhiều sentinel để mang thêm ý nghĩa. Trước đây cũng làm được rồi, nhưng chưa có cách nào được tài liệu hóa là “chính thức được khuyến nghị”. Điều này có thể khiến tác giả package đi theo hướng khác

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

    Dù là ví dụ hơi gượng, trong trường hợp này có thể phân biệt giữa việc ID đó tồn tại nhưng không có giá trị được liên kết, và việc thất bại vì bản thân ID đó không tồn tại. Cách làm “đúng chất Python” có lẽ vẫn là dùng exception, nhưng nó trông giống một cách tiếp cận hàm hơn so với kiểu thường thấy khi viết Python

    • Trước đây nó có vẻ giống một cách viết gọn gàng hơn cho singleton khi người ta tạo class giả rồi khởi tạo theo từng module
      class _MissingId: ...
      
      MISSING_ID = _MissingId()
      
      # elsewhere  
      from ... import MISSING_ID  
      
      Nó làm tôi nghĩ đến Symbols
    • PEP nói rằng nếu muốn định nghĩa nhiều giá trị sentinel có liên quan với nhau, hoặc thậm chí có cả thứ tự sắp xếp giữa chúng, thì thay vào đó nên dùng Enum hoặc thứ gì tương tự
  • Có lẽ sẽ tốt hơn nếu просто đưa luôn API Symbol của JavaScript vào. Nó hữu ích theo nghĩa tổng quát, và cũng giải quyết luôn vấn đề đang nhắm tới ở đây.