Python 中的 "sentinel object" 模式有什么意义

What is the point of "sentinel object" pattern in Python

我最近了解到 python 中的 "sentinel object" 模式。我被它吸引了,并开始在任何可能的地方使用它。但是,在不需要它的地方使用它之后,一位同事问我这件事。现在,鉴于 "x in dict" 存在,我看不到它的用途。这是一个(截断的)规范示例,来自 functools LRU 缓存库:

def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
    # Constants shared by all lru cache instances:
    sentinel = object()          # unique object used to signal cache misses
    make_key = _make_key         # build a key from the function arguments
    PREV, NEXT, KEY, RESULT = 0, 1, 2, 3   # names for the link fields

    cache = {}
    hits = misses = 0
    full = False
    cache_get = cache.get    # bound method to lookup a key or return None
    cache_len = cache.__len__  # get cache size without calling len()
    lock = RLock()           # because linkedlist updates aren't threadsafe
    root = []                # root of the circular doubly linked list
    root[:] = [root, root, None, None]     # initialize by pointing to self

    if maxsize == 0:

        def wrapper(*args, **kwds):
            # No caching -- just a statistics update after a successful call
            nonlocal misses
            result = user_function(*args, **kwds)
            misses += 1
            return result

    elif maxsize is None:

        def wrapper(*args, **kwds):
            # Simple caching without ordering or size limit
            nonlocal hits, misses
            key = make_key(args, kwds, typed)
            result = cache_get(key, sentinel)
            if result is not sentinel:
                hits += 1
                return result
            result = user_function(*args, **kwds)
            cache[key] = result
            misses += 1
            return result

现在,只关注使用模式的部分:

            result = cache_get(key, sentinel)                    
            if result is not sentinel:                           
                hits += 1                                        
                return result                                    
            result = user_function(*args, **kwds)                
            cache[key] = result                                  
            misses += 1                                          
            return result 

据我所知,这可以重写为以下方式:

            if key not in cache:
                result = user_function(*args, **kwds)
                cache[key] = result
                misses += 1
            else:
                result = cache_get(key)
                hits += 1
            return result

我想知道:这个哨兵方法有什么好处?我认为这可能是效率。 python wiki 说 "x in s" 是 O(n) 平均情况,而获取项目是 O(1) 平均情况。但这真的会产生实际的时差吗?

我 运行 在我的笔记本电脑上进行了一些快速测试,运行时间很接近,无论是在大多数按键命中还是大多数按键未命中的情况下。

作为对@martineau 的回复,我认为我们不会从这个模式中获得任何额外的功能,正如这个交互式会话所展示的那样:

>>> d={1:None}
>>> if 1 in d:
...     print('one is there')
... 
one is there
>>> if 2 in d:
...     print('two is not')
... 
>>> d={1:None,None:3}
>>> if None in d:
...     print('we can find a none key as well')
... 
we can find a none key as well

所以,问题仍然存在:这种模式的意义何在?

在您显示的代码中,使用带有标记值的 dict.get 是针对字典中存在键 的情况的小优化。在这种情况下,您只需要在 get 调用中执行一次散列和密钥查找过程,而不是在 if key in dict: value = dict[key] 等效项中需要的两次。

这不会改变计算复杂性,因为字典索引和成员资格测试都是 O(1),但如果它们在 "hot" 代码中,即使是小的性能改进也很重要 "hot" =27=] 经常。这正是您显示的代码所提供的记忆最有用的地方!

您可能会在标准库的某些 Python 代码中看到其他一些非常常见的微优化。您的示例包含另一个,将绑定方法 (cache.get) 保存到局部变量 (cache_get)。这让代码避免在每次需要时重新绑定方法,这涉及索引到实例和 class 字典并创建绑定方法对象。