13

在寻找使用嵌套字典的方法时,我发现了nosklo发布的以下代码,我想解释一下。

class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

测试:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

输出:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

我是一个相当新手的程序员。我在业余时间学到了大部分我所知道的东西,我唯一的正式培训是在高中时的 Turbo Pascal。我理解并且能够以简单的方式使用类,例如使用__init__、类方法,以及使用foo.man = 'choo'.

我不知道方括号系列是如何通过类正确定向的(我认为它们是以__getitem__某种方式调用的),并且不明白它们是如何被如此简洁地处理而不必单独调用该方法三次的。

我的印象是(dict)类声明中的__init__.

我以前用过try: except:,不过,还是用非常简单的方式。在我看来try,当它运行时,它正在调用一系列函数__getitem__。我收集到,如果当前级别的字典存在,则尝试将通过并转到下一个字典。我认为except,当有 a 时运行,KeyError但我以前从未见过self这样使用过。 Self被当作​​字典对待,而我认为selfclass AutoVivification……的一个实例,两者兼而有之吗?我从来没有像这样连续分配两次,foo = man = choo但怀疑它value指向self[item]whileself[item]指向的结果type(self)。但type(self)会返回如下内容:<class '__main__.AutoVivification'>不会吗?我不知道末尾多余的圆括号是做什么用的。因为我不知道函数是如何被调用的,所以我不明白在哪里value返回。

抱歉有这些问题!这里面有很多我不明白的地方,而且我不知道在哪里可以找到它,除非我花了几个小时阅读文档,而在这些文档中我保留的很少。这段代码看起来可以满足我的目的,但我想在使用它之前理解它。

如果您想知道我在使用嵌套字典的程序中要做什么:我正在尝试以天文比例保存地图数据。虽然我无法创建嵌套 4 次的 10^6 项的字典/列表(即 10^24 项!),但空间大部分是空的,因此我可以将空值完全排除在外,只有在有东西时才分配。困扰我的是处理字典的有效方法。

4

2 回答 2

19

逐行:

class AutoVivification(dict):

我们制作了 的子类dictAutoVivification一种 的dict,带有一些局部变化。

def __getitem__(self, item):

每当有人尝试通过索引查找访问实例上的项目时,都会调用该__getitem()__钩子。[...]因此,每当有人这样做时object[somekey]type(object).__getitem__(object, somekey)都会被调用。

我们将跳过try一会儿,下一行是:

 return dict.__getitem__(self, item)

这将调用未绑定的方法__getitem__(),并将我们自己的实例与键一起传递给它。换句话说,我们将原始 __getitem__类称为父类定义的dict

现在,我们都知道如果item字典中没有键会发生什么,KeyError会引发 a。这就是try:,except KeyError组合的用武之地:

    try:
        return dict.__getitem__(self, item)
    except KeyError:
        value = self[item] = type(self)()
        return value

因此,如果当前实例(它是 的子类型dict)没有给定键,它将捕获KeyError原始dict.__getitem__()方法抛出的异常,相反我们创建一个值,将其存储self[item]并返回该值。

现在,记住它self是 的(子类)dict,所以它是字典。因此,它可以分配新值(顺便说一下,它会为此使用__setitem__hook),并且在这种情况下,它会创建一个self. 那是另一个dict子类。

那么当我们调用时会发生什么细节a[1][2][3] = 4呢?Python 一步一步地经历这一步:

  1. a[1]导致type(a).__getitem__(a, 1). 的自定义__getitem__方法AutoVivification捕获KeyError,创建 的实例AutoVivification,将其存储在键下1并返回它。

  2. a[1]返回一个空AutoVivification实例。在该对象上调用下一个项目访问[2],我们重复步骤 1 中发生的事情;有一个KeyError,一个新的实例AutoVivification被创建,存储在2键下,并且这个新实例被返回给调用者。

  3. a[1][2]返回一个空AutoVivification实例。在该对象上调用下一个项目访问[3],我们重复步骤 1(和步骤 2)中发生的事情。有一个KeyError,一个新的实例AutoVivification被创建,存储在3键下,并且这个新实例被返回给调用者。

  4. a[1][2][3]返回一个空AutoVivification实例。现在我们在该实例中存储一个新值,4.

转到下一行代码后a[1][3][3] = 5,顶级AutoVivification实例已经有一个1键,并且该return dict.__getitem__(self, item)行将返回相应的值,该值恰好是AutoVivification在上面的步骤中创建的实例。

从那里,[3]项目访问调用将再次创建一个新AutoVivification实例(因为对象a[1]只有一个2键),我们再次执行所有相同的步骤。

于 2012-11-07T19:07:02.400 回答
2

请参阅object.__getitem__文档作为开始。

class AutoVivification(dict)声明创建AutoVivification了 的子类dict,因此它的行为与原样相同,dict除非它显式覆盖某些行为 - 就像这个类在覆盖 时所做的那样__getitem__

调用dict.__getitem__(self, item)通常会改为:

super(AutoVivification, self).__getitem__(item)

(至少在 Python 2.x 中;Python 3 具有更好的语法。)无论哪种方式,它所做的都是尝试让默认dict行为运行,但在不起作用的情况下实现回退。

type(self)()先查找self实例对应的类对象,然后调用类对象——在这种情况下和写一样AutoVivification(),看起来应该熟悉得多。

希望能为您解决问题!

于 2012-11-07T19:05:31.503 回答