字典视图对象是什么?

185

在Python 2.7中,我们可以使用字典视图方法(dictionary view methods)

现在,我知道以下两种方法的优缺点:

  • dict.items() (以及values, keys):返回一个列表,因此你可以将结果存储下来。
  • dict.iteritems() (以及类似的方法):返回一个生成器(generator),因此你可以逐个迭代生成的每个值。

dict.viewitems() (以及类似的方法)是什么?它们有什么好处?它们如何工作?究竟什么是视图?

我读到视图总是反映字典的更改。但从性能和内存的角度来看,它是如何表现的?有哪些优缺点?

5个回答

191
字典视图本质上就是它们的名字所说的那样:视图就像是字典键和值(或项目)的窗口。以下是 Python 3 的官方文档摘录:
>>> dishes = {'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500}
>>> keys = dishes.keys()
>>> values = dishes.values()

>>> # view objects are dynamic and reflect dict changes
>>> del dishes['eggs']
>>> keys  # No eggs anymore!
dict_keys(['sausage', 'bacon', 'spam'])

>>> values  # No eggs value (2) anymore!
dict_values([1, 1, 500])

(Python 2的等价方法使用dishes.viewkeys()dishes.viewvalues()。)

这个例子展示了视图的动态特性:键视图并不是在某一时刻键的副本,而是一个简单的窗口,显示你的键;如果它们被更改,那么你通过窗口看到的也会随之改变。这个特性在某些情况下非常有用(例如,一个程序可以在多个部分上使用键的视图,而不是每次需要时重新计算当前键列表)——请注意,如果在遍历视图时修改字典键,则迭代器应该如何行为并不明确,这可能会导致错误

一种优势是,比如说查看键只使用了少量且固定的内存少量且固定的处理器时间,因为没有创建键列表(另一方面,Python 2 经常会不必要地创建新列表,正如 Rajendran T 所引用的那样,这需要与列表长度成比例的时间和空间)。继续窗户的比喻,如果你想看到墙后面的景色,你只需在上面开个口子(建造一个窗户);将键复制到列表中则相当于在你的墙上画一份景色副本——这需要时间、空间,并且不会自动更新。
总之,视图只是对字典的视图(窗户),即使字典发生变化,它们也能显示其内容。它们提供与列表不同的功能:键列表包含给定时间点上字典键的副本,而视图是动态的,并且获取速度更快,因为它不需要复制任何数据(键或值)才能创建。

7
好的,那么这与直接访问内部键列表有什么不同?速度更快还是更慢?内存效率更高吗?是否受限制?如果您可以读取和编辑它,那感觉就像拥有对此列表的引用一样。 - Bite code
5
谢谢。事实上,视图 就是 您访问“内部键列表”的方式(请注意,这个“键列表”不是Python列表,而是确切地一个视图)。与Python 2的键(或值或项)列表相比,视图更加内存高效,因为它们不复制任何内容;它们确实像是“对键列表的引用”(还要注意,“对列表的引用”在Python中实际上只是简单地称为列表,因为列表是可变对象)。另外请注意,您不能直接编辑视图:相反,您仍然编辑字典,并且视图会立即反映出您的更改。 - Eric O. Lebigot
4
好的,我对实现还不是很清楚,但这是迄今为止最好的答案。 - Bite code
2
谢谢。的确,这个回答主要涉及视图的语义。我对它们在CPython中的实现没有详细信息,但我猜测视图基本上是指向正确结构(键和/或值)的指针,并且这些结构是字典对象本身的一部分。 - Eric O. Lebigot
5
值得一提的是,这篇文章中的示例代码来自Python 3,与我在Python 2.7中获得的不同。 - snth
显示剩余7条评论

23

仅从文档中我得出以下印象:

  1. 视图类似于伪集合,不支持索引,因此您可以使用它们进行成员测试和迭代(因为键是可哈希且唯一的,键和值视图更像“集合”,因为它们不包含重复项)。
  2. 您可以存储并多次使用它们,就像列表版本一样。
  3. 因为它们反映了底层字典,所以字典中的任何更改都会更改视图,并且几乎一定会更改迭代的顺序。因此,与列表版本不同,它们不是“稳定的”。
  4. 因为它们反映了底层字典,所以它们几乎肯定是小型代理对象;复制键/值/项需要以某种方式监视原始字典,并在发生更改时多次复制它,这将是一种荒谬的实现。因此,我预计内存开销很小,但访问速度比直接访问字典稍慢。

因此,我认为关键用例是如果您正在保留一个字典并在其键/项/值之间重复迭代。您可以使用视图,将for k,v in mydict.iteritems():转换为for k,v in myview:。但是,如果您只是迭代一次字典,我认为iter-版本仍然更可取。


2
从我们获得的一些信息中分析利弊,给予 +1。 - Bite code
如果我在视图上创建一个迭代器,每当字典发生更改时,它仍然会失效。这与对字典本身(例如iteritems())进行迭代的迭代器相同。那么这些视图有什么意义呢?什么情况下我会很高兴拥有它们? - Alfe
@Alfe 你说得对,这是字典迭代的一个问题,视图并不能解决这个问题。比如说你需要将一个字典的值传递给一个函数,你可以使用 .values(), 但这会复制整个列表,可能会很耗费资源。虽然有 .itervalues(),但你无法多次消耗它们,所以它不能与每个函数一起使用。视图不需要昂贵的复制,但它们作为独立的值仍然更有用,而不是迭代器。但它们仍然不打算帮助同时进行迭代和修改(在这种情况下你确实需要一个副本)。 - Ben

22

正如你所提到的,dict.items() 返回字典的(键,值)对列表的副本,这是浪费的,而 dict.iteritems() 返回字典的(键,值)对的迭代器。

现在以以下示例来看一下字典的迭代器和视图之间的区别:

>>> d = {"x":5, "y":3}
>>> iter = d.iteritems()
>>> del d["x"]
>>> for i in iter: print i
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

然而,视图仅显示字典中的内容。它不关心它是否发生了更改:

>>> d = {"x":5, "y":3}
>>> v = d.viewitems()
>>> v
dict_items([('y', 3), ('x', 5)])
>>> del d["x"]
>>> v
dict_items([('y', 3)])

视图仅是字典现在的样子。删除一个条目后,.items() 将过时,.iteritems() 将会抛出错误。


很好的例子,谢谢。不过应该是 v = d.items() 而不是 v - d.viewitems()。 - rix
1
这个问题是关于Python 2.7的,所以viewitems()是正确的(在Python 3中,items()正确地提供了视图)。 - Eric O. Lebigot
然而,视图不能用于迭代字典时修改它。 - 0 _

15

视图方法返回一个列表(与.keys().items().values()相比,不是该列表的副本),因此更加轻量级,但反映字典的当前内容。

来源:Python 3.0 - dict methods return views - why?

主要原因是对于许多用例,返回一个完全分离的列表是不必要且浪费的。这将需要复制整个内容(可能很多或很少)。

如果您只想遍历键,则不需要创建新列表。如果您确实需要它作为单独的列表(作为副本),则可以轻松从视图创建该列表。


9
视图方法返回视图对象,这些对象不符合列表接口。 - Matthew Trevor

8

视图可以让您访问底层数据结构,而无需复制它。除了动态创建列表之外,它们最有用的用途之一是in测试。比如,您想要检查一个值是否在字典中(无论是键还是值)。

第一种选择是使用dict.keys()创建一个包含所有键的列表,这样做可以起到作用,但显然会消耗更多的内存。如果字典非常大呢?那将是浪费。

使用views可以迭代实际数据结构,而不需要中间列表。

让我们举个例子。我有一个包含1000个随机字符串和数字的键值对字典,k是我想查找的键。

large_d = { .. 'NBBDC': '0RMLH', 'E01AS': 'UAZIQ', 'G0SSL': '6117Y', 'LYBZ7': 'VC8JQ' .. }

>>> len(large_d)
1000

# this is one option; It creates the keys() list every time, it's here just for the example
timeit.timeit('k in large_d.keys()', setup='from __main__ import large_d, k', number=1000000)
13.748743600954867


# now let's create the list first; only then check for containment
>>> list_keys = large_d.keys()
>>> timeit.timeit('k in list_keys', setup='from __main__ import large_d, k, list_keys', number=1000000)
8.874809793833492


# this saves us ~5 seconds. Great!
# let's try the views now
>>> timeit.timeit('k in large_d.viewkeys()', setup='from __main__ import large_d, k', number=1000000)
0.08828549011070663

# How about saving another 8.5 seconds?

如您所见,迭代view对象可以大幅提升性能,同时减少内存开销。当需要执行类似于Set的操作时应使用它们。

注意:本文基于 Python 2.7 运行。


在Python >=3中,我相信.keys()默认返回一个视图。不过最好再确认一下。 - Yolo Voe
1
你是对的。Python 3+ 更多地使用视图对象而不是列表,这样更节省内存。 - Chen A.
1
这些计时结果非常有意义,但是检查k是否为字典large_d的键应该使用Python中的k in large_d来完成,这可能基本上与使用视图一样快(换句话说,k in large_d.keys()不符合Python风格,应该避免使用,就像k in large_d.viewkeys()一样)。 - Eric O. Lebigot
谢谢您提供一个坚实、有用的例子。实际上,k in large_dk in large_d.viewkeys() 快得多,所以应该避免使用后者,但对于 k in large_d.viewvalues() 这是有意义的。 - naught101

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接