PEP 3106 – 重构 dict.keys()、.values() 和 .items()
- 作者:
- Guido van Rossum
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2006年12月19日
- Python 版本:
- 3.0
- 历史记录:
摘要
本 PEP 提案修改内置 dict 类型中的 .keys()、.values() 和 .items() 方法,使其返回一个类似集合或无序的容器对象,该对象的内容源自底层字典,而不是一个键、值等的副本列表;并移除 .iterkeys()、.itervalues() 和 .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() 构造函数即可。
规范隐含着 .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
上次修改: 2023-09-09 17:39:29 GMT