PEP 3106 – 重新设计 dict.keys()、.values() 和 .items()
- 作者:
- Guido van Rossum
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2006年12月19日
- Python 版本:
- 3.0
- 发布历史:
摘要
本PEP提议更改内置 dict 类型的 .keys()、.values() 和 .items() 方法,使其返回一个类似集合或无序的容器对象,其内容从底层字典派生,而不是返回一个作为键等副本的列表;并删除 .iterkeys()、.iterval() 和 .iteritems() 方法。
这种方法灵感来源于 Java 集合框架 [1] 中采用的方法。
引言
长期以来,计划是更改内置 dict 类型的 .keys()、.values() 和 .items() 方法,使其返回比列表更轻量的对象,并废弃 .iterkeys()、.itervalues() 和 .iteritems()。其思想是,当前(在 2.x 中)的代码:
for k, v in d.iteritems(): ...
应该重写为:
for k, v in d.items(): ...
(对于 .itervalues() 和 .iterkeys() 类似,但后者是多余的,因为我们可以将该循环写成 for k in d。)
当前代码:
a = d.keys() # assume we really want a list here
(等等)应该重写为:
a = list(d.keys())
实现这一点至少有两种方法。最初的计划是简单地让 .keys()、.values() 和 .items() 返回一个迭代器,即在 Python 2.x 中 iterkeys()、itervalues() 和 iteritems() 返回的精确值。然而,Java 集合框架 [1] 建议有一种更好的解决方案:这些方法返回具有集合行为(对于 .keys() 和 .items())或多重集合(== 包)行为(对于 .values())的对象,这些对象不包含键、值或项的副本,而是引用底层字典并根据需要从字典中提取它们的值。
这种方法的优点是仍然可以这样编写代码:
a = d.items()
for k, v in a: ...
# And later, again:
for k, v in a: ...
实际上,Python 3.0 中的 iter(d.keys())(等等)将执行 Python 2.x 中 d.iterkeys()(等等)的操作;但在大多数情况下,我们不必编写 iter() 调用,因为它隐含在 for 循环中。
.keys() 和 .items() 方法返回的对象行为类似于集合。values() 方法返回的对象行为类似于一个更简单的无序集合——它不能是一个集合,因为可能存在重复的值。
由于集合行为,可以通过简单地测试来检查两个字典是否具有相同的键:
if a.keys() == b.keys(): ...
对于 .items() 也是如此。
这些操作的线程安全性仅限于以非线程安全方式使用它们可能会导致异常,但不会导致内部表示的损坏。
与 Python 2.x 中一样,在使用迭代器遍历字典时修改字典具有未定义的效果,并且在大多数情况下会引发 RuntimeError 异常。(这类似于 Java 集合框架所做的保证。)
.keys() 和 .items() 返回的对象与内置 set 和 frozenset 类型的实例完全可互操作;例如:
set(d.keys()) == d.keys()
保证为 True(除非 d 同时被另一个线程修改)。
规范
我正在使用伪代码来指定语义:
class dict:
# Omitting all other dict methods for brevity.
# The .iterkeys(), .itervalues() and .iteritems() methods
# will be removed.
def keys(self):
return d_keys(self)
def items(self):
return d_items(self)
def values(self):
return d_values(self)
class d_keys:
def __init__(self, d):
self.__d = d
def __len__(self):
return len(self.__d)
def __contains__(self, key):
return key in self.__d
def __iter__(self):
for key in self.__d:
yield key
# The following operations should be implemented to be
# compatible with sets; this can be done by exploiting
# the above primitive operations:
#
# <, <=, ==, !=, >=, > (returning a bool)
# &, |, ^, - (returning a new, real set object)
#
# as well as their method counterparts (.union(), etc.).
#
# To specify the semantics, we can specify x == y as:
#
# set(x) == set(y) if both x and y are d_keys instances
# set(x) == y if x is a d_keys instance
# x == set(y) if y is a d_keys instance
#
# and so on for all other operations.
class d_items:
def __init__(self, d):
self.__d = d
def __len__(self):
return len(self.__d)
def __contains__(self, (key, value)):
return key in self.__d and self.__d[key] == value
def __iter__(self):
for key in self.__d:
yield key, self.__d[key]
# As well as the set operations mentioned for d_keys above.
# However the specifications suggested there will not work if
# the values aren't hashable. Fortunately, the operations can
# still be implemented efficiently. For example, this is how
# intersection can be specified:
def __and__(self, other):
if isinstance(other, (set, frozenset, d_keys)):
result = set()
for item in other:
if item in self:
result.add(item)
return result
if not isinstance(other, d_items):
return NotImplemented
d = {}
if len(other) < len(self):
self, other = other, self
for item in self:
if item in other:
key, value = item
d[key] = value
return d.items()
# And here is equality:
def __eq__(self, other):
if isinstance(other, (set, frozenset, d_keys)):
if len(self) != len(other):
return False
for item in other:
if item not in self:
return False
return True
if not isinstance(other, d_items):
return NotImplemented
# XXX We could also just compare the underlying dicts...
if len(self) != len(other):
return False
for item in self:
if item not in other:
return False
return True
def __ne__(self, other):
# XXX Perhaps object.__ne__() should be defined this way.
result = self.__eq__(other)
if result is not NotImplemented:
result = not result
return result
class d_values:
def __init__(self, d):
self.__d = d
def __len__(self):
return len(self.__d)
def __contains__(self, value):
# This is slow, and it's what "x in y" uses as a fallback
# if __contains__ is not defined; but I'd rather make it
# explicit that it is supported.
for v in self:
if v == value:
return True
return False
def __iter__(self):
for key in self.__d:
yield self.__d[key]
def __eq__(self, other):
if not isinstance(other, d_values):
return NotImplemented
if len(self) != len(other):
return False
# XXX Sometimes this could be optimized, but these are the
# semantics: we can't depend on the values to be hashable
# or comparable.
olist = list(other)
for x in self:
try:
olist.remove(x)
except ValueError:
return False
assert olist == []
return True
def __ne__(self, other):
result = self.__eq__(other)
if result is not NotImplemented:
result = not result
return result
备注
视图对象不能直接修改,但它们不实现 __hash__();如果底层字典被修改,它们的值可能会改变。
对底层字典的唯一要求是它实现 __getitem__()、__contains__()、__iter__() 和 __len__()。
我们不实现 .copy() – .copy() 方法的存在表明副本与原始对象具有相同的类型,但如果没有复制底层字典,这是不可行的。如果您想要特定类型(如 list 或 set)的副本,您可以将上述对象之一传递给 list() 或 set() 构造函数。
该规范意味着 .keys()、.values() 和 .items() 返回项的顺序是相同的(就像在 Python 2.x 中一样),因为该顺序都源自字典迭代器(该迭代器可能具有任意性,但在字典未修改的情况下是稳定的)。这可以用以下不变式表示:
list(d.items()) == list(zip(d.keys(), d.values()))
未解决的问题
我们是否需要更多动力?我认为能够在不复制键和项的情况下对它们进行集合操作本身就足够说明问题了。
我省略了各种集合操作的实现。这些仍然可能带来小惊喜。
如果多次调用 d.keys()(等等)返回相同的对象,那也没关系,因为对象的唯一状态是它引用的字典。这是否值得在字典对象中增加额外的槽位?这应该是弱引用还是 d_keys(等等)对象一旦创建就永远存在?草案:可能不值得在每个字典中增加额外的槽位。
d_keys、d_values 和 d_items 是否应该有一个公共实例变量或方法,通过它我们可以检索底层字典?草案:是的(但应该叫什么?)。
我正在征集比 d_keys、d_values 和 d_items 更好的名称。这些类可以是公共的,以便它们的实现可以被其他映射的 .keys()、.values() 和 .items() 方法重用。或者应该吗?
d_keys、d_values 和 d_items 类是否应该可重用?草案:是的。
它们是否应该可子类化?草案:是的(但请参见下文)。
一个特别棘手的问题是,以其他操作(例如 .discard())定义的那些操作是否必须真的以这些其他操作来实现;这可能看起来无关紧要,但如果这些类被子类化,它就会变得相关。从历史上看,Python 在这种情况下清晰地指定高度优化内置类型的语义方面记录不佳;我的草案是继续这种趋势。子类化可能仍然有助于*添加*新方法,例如。
我将把决定权(特别是关于命名)留给提交工作实现的人。
参考资料
来源: https://github.com/python/peps/blob/main/peps/pep-3106.rst
最后修改时间: 2025-02-01 08:59:27 GMT