【问题标题】:overloaded __iter__ is bypassed when deriving from dict从 dict 派生时绕过重载的 __iter__
【发布时间】:2013-08-19 15:47:22
【问题描述】:

试图创建一个自定义的不区分大小写的字典,我遇到了以下不方便和(从我的角度来看)意外的行为。如果从dict 派生类,则在转换回dict 时将忽略重载的__iter__keysvalues 函数。我将其浓缩为以下测试用例:

import collections

class Dict(dict):
    def __init__(self):
        super(Dict, self).__init__(x = 1)

    def __getitem__(self, key):
        return 2

    def values(self):
        return 3

    def __iter__(self):
        yield 'y'

    def keys(self):
        return 'z'

    if hasattr(collections.MutableMapping, 'items'):
        items = collections.MutableMapping.items
    if hasattr(collections.MutableMapping, 'iteritems'):
        iteritems = collections.MutableMapping.iteritems

d = Dict()
print(dict(d))              # {'x': 1}
print(dict(d.items()))      # {'y': 2}

keys,values__iter__,__getitem__ 的值不一致,仅用于演示实际调用了哪些方法。

documentation for dict.__init__ 说:

如果给出了一个位置参数并且它是一个映射对象,一个 字典是使用与映射相同的键值对创建的 目的。否则,位置参数必须是迭代器对象。

我猜这与第一句话有关,也可能与内置词典的优化有关。

为什么对dict(d) 的调用不使用keys__iter__ 中的任何一个? 是否可以以某种方式重载“映射”以强制 dict 构造函数使用我的键值对表示?

我为什么用这个?对于不区分大小写但保留字典的字典,我想:

  • 在内部存储 (lowercase => (original_case, value)),同时显示为 (any_case => value)。
  • dict 派生,以便使用一些使用isinstance 检查的外部库代码
  • 不使用 2 个字典查找:lower_case=>original_case,后跟 original_case=>value(这是我现在正在做的解决方案)

如果您对申请案例感兴趣:here is corresponding branch

【问题讨论】:

  • 为什么要制作继承字典,下次将其转换回简单的 dict 对象时?我的意思是,一旦你使用dict(d),它就会变回普通字典。你应该使用:d.items()
  • 作为一种固有类型,重新定义dict 的语义肯定会在其他地方引起一些惊讶(如果不是彻底破坏)。
  • 我不太明白为什么这个问题被标记为主要基于意见。第二个答案和第一条评论是(无论如何它甚至得到了赞成)。接受的答案虽然陈述了“事实”,但由于实施细节,这是不可能的。此外,这个问题的答案远非显而易见,因为在面向对象的语言中,这是出乎意料的行为,不可能覆盖字典 __iter__

标签: python dictionary overloading subclassing


【解决方案1】:

the file dictobject.c 中,您可以在第 1795ff 行看到。相关代码:

static int
dict_update_common(PyObject *self, PyObject *args, PyObject *kwds, char *methname)
{
    PyObject *arg = NULL;
    int result = 0;

    if (!PyArg_UnpackTuple(args, methname, 0, 1, &arg))
        result = -1;

    else if (arg != NULL) {
        _Py_IDENTIFIER(keys);
        if (_PyObject_HasAttrId(arg, &PyId_keys))
            result = PyDict_Merge(self, arg, 1);
        else
            result = PyDict_MergeFromSeq2(self, arg, 1);
    }
    if (result == 0 && kwds != NULL) {
        if (PyArg_ValidateKeywordArguments(kwds))
            result = PyDict_Merge(self, kwds, 1);
        else
            result = -1;
    }
    return result;
}

这告诉我们,如果对象有属性keys,则调用的代码只是一个合并。那里调用的代码(l. 1915 ff.)区分了真正的字典和其他对象。在真正的dicts的情况下,使用PyDict_GetItem()读出项目,这是对象的“最内部接口”,不需要使用任何用户定义的方法。

因此,您应该使用UserDict module,而不是从dict 继承。

【讨论】:

  • 我明白了,问题出在Pydict_Check()。我觉得实现不仅应该检查继承,还应该检查被覆盖的方法,以便产生更一致的行为。从 UserDict 继承(正确)在 isinstance(d, dict) 检查中给出 False,这就是我想从 dict 派生的原因。
  • 奇怪,我在你的答案中使用链接看到的源代码和你显示的不完全一样,行号也不一样...
  • 在这种情况下,似乎可以通过使用返回正确值的__getitem__() 方法创建派生类来使其工作。
  • @martineau 确实很奇怪;我写的时候似乎对文件有不同的修订。
  • 我刚刚将链接更改为 concreate 修订版,因此我们不应再出现任何差异。但由于调用链是 dict_init() -> dict_update_common() -> PyDict_Merge() -> PyDict_Merge(),覆盖 __getitem__() 也无济于事。
【解决方案2】:

是否有可能以某种方式重载“映射”以强制 dict 构造函数使用我的键值对表示?

没有。

作为一种固有类型,重新定义 dict 的语义肯定会导致其他地方彻底崩溃。

你有一个库,你不能覆盖 dict 的行为,这很难,但重新定义语言原语不是答案。如果有人在你背后搞了整数加法的交换性质,你可能会觉得很烦人;这就是为什么他们不能。

关于您的评论“UserDict(正确地)在isinstance(d, dict) 检查中给出False”,当然是因为它不是dictdict 具有非常具体的不变量@ 987654328@不能保证。

【讨论】:

  • 这就是type(d) is dict 的用途。准确获得dict 的保证。对于子类 (isinstance),您希望它们可以覆盖功能。这不就是多态性的全部意义所在吗?
  • 如果你想争辩说语言不应该是它的样子,我建议你提交Python Enhancement Proposal。你问了一个关于语言如何的问题,我回答了;很抱歉您不喜欢这个答案,但这并不影响其有效性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-12-30
  • 1970-01-01
  • 1970-01-01
  • 2018-06-30
  • 1970-01-01
  • 2014-05-30
相关资源
最近更新 更多