【问题标题】:Why does some_dict['key'] = somevalue work if the key is not in some_dict?如果 key 不在 some_dict 中,为什么 some_dict['key'] = somevalue 起作用?
【发布时间】:2017-08-18 11:44:52
【问题描述】:

我知道我可以通过这样做在 python dict 中添加一个新的键/值

some_dict['absent_key'] = somevalue

但我不太了解内部工作原理。

我曾经认为字典的行为类似于 C++ 地图。 [] 运算符将为给定键创建元素(如果它尚不存在),然后返回对它的引用,以便可以在与运算符 = 相同的行中为其分配一个值。

但是 C++ 中的这种行为会导致如果我们从映射中查询不存在的键的值,则为该键创建元素,并且将返回值类型的默认值而不是一个错误。在 python 中,这会抛出一个KeyError。

所以我不明白的是:如何,因为 [] 运算符也必须在 python 中的 = 之前进行评估(我认为?),它的行为是否会有所不同,具体取决于结果是否会被读取或分配一个值(在表达式评估的那一点它不应该知道)?

python 计算表达式的顺序有区别吗?或者解释器是否更聪明,因为字典具有硬编码类型,因此它更准确地知道它的行为,而 std::map 位于“库”中,因此编译器可以假设更少?还是其他原因?

【问题讨论】:

  • 在 lhs 上,[] 映射到 __setitem__(key, rhs),在 rhs 上 [] 映射到 __getitem__(key) - 它们实现了不同的逻辑。在 C++ 中,[] 是它自己的运算符,它返回一个可以更新的引用。在 python 中,= 是一个语句,lhs 不是 C++ 中的表达式。
  • 旁注:访问运算符[] 不是必需的:您可以使用dictionary.get(key, some_optional_default_value_if_key_not_found)。

标签: python dictionary operators


【解决方案1】:

操作:

some_dict[key]

和

some_dict[key] = value

和

del some_dict[key]

使用对象的不同特殊方法:__getitem__、__setitem__ 和__delitem__。因此,实现它们的不仅仅是一个运算符 ([])。

也许一个例子可以说明:

class Something(dict):  # subclassing dict
    def __getitem__(self, key):
        print('trying to get', key)
        return super().__getitem__(key)
    def __setitem__(self, key, value):
        print('trying to set', key, 'to', value)
        return super().__setitem__(key, value)
    def __delitem__(self, key):
        print('trying to delete', key)
        return super().__delitem__(key)

测试:

>>> s = Something({'a': 1, 'b': 2})
>>> s['a']
trying to get a
1

>>> s['c'] = 10
trying to set c to 10

>>> del s['b']
trying to delete b

所以这取决于它们是如何实现的。在普通的 Python 中 dicts __getitem__ 只返回键的值,如果它不存在则抛出。

但子类也可以实现 __missing__ 方法 - 以防万一他们想要在字典中不存在键的情况下自定义行为(在查找期间)。

【讨论】:

  • 酷,所以这也意味着我可以继承 dict,并重载 __getitem__() 以使其表现得像 std::map 如果我想要:)
  • @bartoli 是的。但我认为您不需要为此创建自己的子类:collections.defaultdict 在 Python 标准库中也可用。不确定它是否与std::map 相同,但它“更接近”。 :)
  • @MSeifert 我想说这种行为是 very close ;-) 见这里:stackoverflow.com/questions/10124679/…。
  • @ChristianDean 在比较不同语言的数据结构时,我总是很小心。如果您使用绝对值(来自语言 A 的 a 等同于来自语言 B 的 b)有人会戳它并提出 100 个(一些有效的一些......不是)它不等价的原因。只是我个人的经验。
  • @MSeifert 我完全同意。这就是为什么我在我的答案中添加警告。当我在评论中说“非常接近”时,我的意思纯粹是指丢失密钥时发生的行为。在这两种数据结构中,都不会引发错误。当然还有一些区别,比如缺少键时调用的“工厂函数”,所以我永远不会声称它们是相同的。
【解决方案2】:

幕后发生了什么?

在 Python 中,当您为键赋值时:

dictionary[key] = value

Python 将上述语法糖翻译成:

dictionary.__setitem__(key, value)

如您所见,Python 在后台调用了__setitem__ 方法。 __setitem__ 方法直接对应于索引数据结构并将新值分配给所述索引的操作。它可以被重载以自定义它的行为。

__setitem__ 用于 Python 字典的默认行为是更改键的值(如果存在),如果不存在则引发 KeyError。为了证明这一点,您可以继承 dict 类并重载 __setitem__ 以显示它的参数:

>>> class Dict(dict):
...     def __setitem__(self, key, value):
...         print('Putting "%s" in dict with value of "%s"' % (key, value))
...         super().__setitem__(key, value)
...
>>>
>>> d = Dict()
>>> d['name'] = 'Hammy'
Putting "name" in dict with value of "Hammy"
>>> d['age'] = 25
Putting "age" in dict with value of "25"
>>> d
{'name': 'Hammy', 'age': 25}

Python 有等效的 std::map 吗?

就像@MSeifert 所说,您可以通过重载__missing__ 方法来自定义不存在密钥时发生的情况。

这就是collections.defaultdict 类在标准库中的作用。它重载__missing__ 以创建缺少的key 并将您选择的默认值映射到它。 Here's the relevant snippet 来自 CPython 源码:

static PyObject *
defdict_missing(defdictobject *dd, PyObject *key)
{
    PyObject *factory = dd->default_factory;
    PyObject *value;
    /* ... */
    value = PyEval_CallObject(factory, NULL);
    if (value == NULL)
        return value;
    if (PyObject_SetItem((PyObject *)dd, key, value) < 0) {
        Py_DECREF(value);
        return NULL;
    }
    return value;
}

注意defaultdict是用C实现的。下面是一个用法示例:

>>> from collections import defaultdict
>>> map = defaultdict(int)
>>> map['a'] = 1
>>> map['b'] = 2
>>> map['c'] # default factory function `int` called
0
>>> map
defaultdict(<class 'int'>, {'a': 1, 'b': 2, 'c': 0})

defaultdict 非常符合 std::map::operator[] 的行为。如果在使用 std::map::operator[] 时某个键不存在,则运算符调用一个与键的值的预期类型匹配的“工厂函数”,并将其分配给缺失的键。

因此,如果您想要行为类似于 std::map 的东西,请使用 defaultdict。请注意,我说的是“喜欢”。这是因为 C++ 和 Python 是两种完全不同的语言。说一种语言的数据结构在另一种语言中具有完全等价物,这种说法很少是正确的。

【讨论】:

  • @MSeifert 感谢 C 代码高亮显示。我试图记住如何做到这一点。
【解决方案3】:

my_dict['key'] = 'value' 符号只是为了:

my_dict.__setitem__('key', 'value')

该函数完成了存储数据的所有工作。但是,您可以根据需要执行它。 python 解释器和库使用的底层机制通常来自更快的编译语言,如 C。

还有更多像__len__()、__getitem__(x)和__delitem__(x)这样的函数来处理所有the other dict之类的操作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-06
    • 1970-01-01
    • 1970-01-01
    • 2012-06-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多