satgo1546’s ocean

Any sufficiently primitive magic is indistinguishable from technology.

慢报:Python 3.15新增内置类sentinel

PEP 661给Python 3.15增加了一个名为sentinel的内置类,其主要特性类似TypeScript的Symbol(不是Symbol.for 😾):通过x = sentinel('x')语法定义的变量具有unique symbol类型,该类型仅包含当次生成的那一个对象x

JavaScript引入Symbol的目的是在保持兼容性的前提下允许对象属性名不为字符串。Python没有JavaScript的历史包袱,引入sentinel的目的不是解锁新的可能性,而是统一函数不便用None作为参数默认值时选择的其他默认值。

比如说,假设需要用Python实现dict.pop(self, key, default=?)方法。这是个常见的需求:第三方Python容器库要想兼容标准库接口,就必须这么做。

class MyDict[K, V]:
    def pop[R](self, key: K, default: R = ?) -> V | R:
        ...

键不存在的场合,如果调用时提供了default参数,则dict.pop方法返回该值,否则抛出KeyError错误。因为pop(key)pop(key, None)的语义并不相同,所以方法签名的?处不能填None。正确方法是创建一个无法被其他模块拿到的对象missing作为参数默认值,不管是什么对象都可以,然后通过if default is missing:检查参数是否已给出。

在sentinel出现之前,人们有各种办法创建无法被其他模块拿到的对象。collections.abc.MutableMapping用的是最简单的object()

class MutableMapping(Mapping):
    __marker = object()

    def pop(self, key, default=__marker):
        try:
            value = self[key]
        except KeyError:
            if default is self.__marker:
                raise
            return default
        else:
            del self[key]
            return value

sortedcontainers为了输出得更好看,专门定义了个类

class SortedDict(dict):
    class _NotGiven:
        def __repr__(self):
            return '<not-given>'

    __not_given = _NotGiven()

    def pop(self, key, default=__not_given):
        if key in self:
            self._list_remove(key)
            return dict.pop(self, key)
        else:
            if default is self.__not_given:
                raise KeyError(key)
            return default

bidict更进一步,定义了个无意义单选项枚举,以辅助类型标注:

class MissingT(Enum):
    MISSING = 'MISSING'

MISSING: t.Final[t.Literal[MissingT.MISSING]] = MissingT.MISSING
from ._typing import MISSING

class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
    @t.overload
    def pop(self, key: KT, /) -> VT: ...
    @t.overload
    def pop(self, key: KT, default: DT = ..., /) -> VT | DT: ...

    def pop(self, key: KT, default: ODT[DT] = MISSING, /) -> VT | DT:
        try:
            return self._pop(key)
        except KeyError:
            if default is MISSING:
                raise
            return default

为了统一这些用法,并方便后两者(美观输出、类型检查)实现,嫌builtins里的东西还不够多的Python往全局加了个sentinel类。

xkcd #927……

在GitHub上查看和发表评论