我应该如何使用Optional类型提示?

 收藏

I'm trying to understand how to use the Optional type hint. From PEP 434, I know I can use Optional for def test(a: int = None) either as def test(a: Union[int, None]) or def test(a: Optional[int]).

但是下面的例子呢?

def test(a : dict = None):
    #print(a) ==> {'a': 1234}
    #or
    #print(a) ==> None

def test(a : list = None):
    #print(a) ==> [1,2,3,4, 'a', 'b']
    #or
    #print(a) ==> None

Optional[type] seems to mean the same thing as Union[type, None], why should I use Optional[] at all?

回复
  • yut 回复

    Optional[...] is a shorthand notation for Union[..., None], telling the type checker that either an object of the specific type is required, or None is required. ... stands for any valid type hint, including complex compound types or a Union[] of more types. Whenever you have a keyword argument with default value None, you should use Optional.

    So for your two examples, you have dict and list container types, but the default value for the a keyword argument shows that None is permitted too so use Optional[...]:

    from typing import Optional
    
    def test(a: Optional[dict] = None) -> None:
        #print(a) ==> {'a': 1234}
        #or
        #print(a) ==> None
    
    def test(a: Optional[list] = None) -> None:
        #print(a) ==> [1, 2, 3, 4, 'a', 'b']
        #or
        #print(a) ==> None
    

    Note that there is technically no difference between using Optional[] on a Union[], or just adding None to the Union[]. So Optional[Union[str, int]] and Union[str, int, None] are exactly the same thing.

    Personally, I'd stick with always using Optional[] when setting the type for a keyword argument that uses = None to set a default value, this documents the reason why None is allowed better. Moreover, it makes it easier to move the Union[...] part into a separate type alias, or to later remove the Optional[...] part if an argument becomes mandatory.

    例如,说你有

    from typing import Optional, Union
    
    def api_function(optional_argument: Optional[Union[str, int]] = None) -> None:
        """Frob the fooznar.
    
        If optional_argument is given, it must be an id of the fooznar subwidget
        to filter on. The id should be a string, or for backwards compatibility,
        an integer is also accepted.
    
        """
    

    then documentation is improved by pulling out the Union[str, int] into a type alias:

    from typing import Optional, Union
    
    # subwidget ids used to be integers, now they are strings. Support both.
    SubWidgetId = Union[str, int]
    
    
    def api_function(optional_argument: Optional[SubWidgetId] = None) -> None:
        """Frob the fooznar.
    
        If optional_argument is given, it must be an id of the fooznar subwidget
        to filter on. The id should be a string, or for backwards compatibility,
        an integer is also accepted.
    
        """
    

    The refactor to move the Union[] into an alias was made all the much easier because Optional[...] was used instead of Union[str, int, None]. The None value is not a 'subwidget id' after all, it's not part of the value, None is meant to flag the absence of a value.

    Side note: You want to avoid using the standard library container types in type hinting however, as you can't say anything about what types they must contain; so instead of dict and list, use typing.List and typing.Dict. And when only reading from a container type, you may just as well accept any immutable abstract container type; lists and tuples are Sequence objects, while dict is a Mapping type:

    from typing import Mapping, Optional, Sequence, Union
    
    def test(a: Optional[Mapping[str, int]] = None) -> None:
        """accepts an optional map with string keys and integer values"""
        # print(a) ==> {'a': 1234}
        # or
        # print(a) ==> None
    
    def test(a: Optional[Sequence[Union[int, str]]] = None) -> None:
        """accepts an optional sequence of integers and strings
        # print(a) ==> [1, 2, 3, 4, 'a', 'b']
        # or
        # print(a) ==> None