为什么 UserDict.popitem() 不按 LIFO 顺序弹出元素?
在题述版本中,UserDict.popitem() 返回最先插入的元素。这是因为它继承了 MutableMapping.popitem(),该方法会删除迭代时得到的第一个键。底层 data 字典保留了插入顺序,而 UserDict.popitem() 并未调用 data.popitem()。
为什么与 dict.popitem() 不同?
D 直接继承 dict,使用的是 dict.popitem()。从 Python 3.7 起,这个方法明确保证 LIFO,即删除并返回最后插入的键值对。Python 官方文档
UserDict 的继承关系不同。在 CPython 3.13.9 源码和 3.14.0 源码中,UserDict 都继承自 MutableMapping,没有自行定义 popitem(),它的 __iter__() 返回 iter(self.data)。
MutableMapping.popitem() 的关键操作是:
key = next(iter(self))
value = self[key]
del self[key]
return key, value
它先取得迭代得到的第一个键,再读取和删除对应元素;如果映射为空,则抛出 KeyError。CPython 3.14.0 的实现
对于题目中的 ud,调用过程如下:
iter(ud)转到iter(ud.data)。- 按插入顺序取得第一个键
"foo"。 - 删除该键,返回
('foo', 'bar')。
这个输出与底层字典保序一致:插入顺序决定了迭代顺序,popitem() 的实现决定了取出哪一端的元素。
这是 bug,还是既有行为?
上述源码表明,这是题述版本中的既有实现行为。MutableMapping.popitem() 的通用实现只要求移除并返回一个键值对,没有提供 dict.popitem() 那样的 LIFO 保证。
因此,不能把 dict.popitem() 的顺序约定直接套到所有可变映射上。至于 UserDict 是否应该与 dict 保持一致,不能仅凭这段代码和源码认定官方已将其视为缺陷,也不能认定 FIFO 是专门设计的稳定承诺。
需要 LIFO 时如何处理?
如果你的 UserDict 子类没有自定义读取、删除行为,可以覆盖 popitem(),直接调用底层字典:
from collections import UserDict
class UD(UserDict):
def popitem(self):
return self.data.popitem()
ud = UD(foo="bar", baz=42)
print(ud.popitem()) # ('baz', 42)
这样会采用 dict.popitem() 的 LIFO 行为,字典为空时也会抛出 KeyError。
如果子类的 __getitem__() 或 __delitem__() 包含校验、日志等逻辑,直接操作 self.data 会绕过这些方法。可以只用底层字典选择最后一个键,再通过实例完成读取和删除:
from collections import UserDict
class UD(UserDict):
def popitem(self):
try:
key = next(reversed(self.data))
except StopIteration:
raise KeyError("popitem(): dictionary is empty") from None
value = self[key]
del self[key]
return key, value
这里假设 self.data 仍是普通 dict,适用于题述版本。选择哪种写法,取决于读取和删除操作是否需要经过子类的方法。
使用时注意什么?
LIFO 按键的插入顺序判断。更新已有键的值不会把该键移到末尾;删除后重新插入才会改变它的位置。字典顺序说明
如果程序依赖特定的弹出顺序,应在自己的类中明确实现这一约定,避免把题述版本中 UserDict.popitem() 的表现当成所有映射通用的规则。
备注:内容仅供参考。
