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 字典并创建绑定方法对象。
我最近了解到 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 字典并创建绑定方法对象。