首页 > 解决方案 > Python中的“哨兵对象”模式有什么意义

问题描述

我最近了解了 python 中的“哨兵对象”模式。我被它所吸引,并开始尽可能地使用它。但是,在不需要它的地方使用它之后,一位同事问我这个问题。现在,鉴于“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)平均情况,而get item是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

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

标签: pythonsentinel

解决方案


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

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

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


推荐阅读