16

我在 Python 2.7 中有一个自定义容器类,一切都按预期工作,除非我通过尝试将实例扩展**kwargs为函数:

cm = ChainableMap({'a': 1})
cm['b'] = 2
assert cm == {'a': 1, 'b': 2} # Is fine
def check_kwargs(**kwargs):
   assert kwargs == {'a': 1, 'b': 2}
check_kwargs(**cm) # Raises AssertionError

我已经覆盖了__getitem__, __iter__, iterkeys, keys, items, and iteritems, (和__eq__and __repr__)但它们似乎都没有参与扩展**kwargs,我做错了什么?

编辑 - 现在从 MutableMapping 继承并添加缺少的方法的工作更新源:

from itertools import chain
from collections import MutableMapping

class ChainableMap(MutableMapping):
    """
    A mapping object with a delegation chain similar to JS object prototypes::

        >>> parent = {'a': 1}
        >>> child = ChainableMap(parent)
        >>> child.parent is parent
        True

    Failed lookups delegate up the chain to self.parent::

        >>> 'a' in child
        True
        >>> child['a']
        1

    But modifications will only affect the child::

        >>> child['b'] = 2
        >>> child.keys()
        ['a', 'b']
        >>> parent.keys()
        ['a']
        >>> child['a'] = 10
        >>> parent['a']
        1

    Changes in the parent are also reflected in the child::

        >>> parent['c'] = 3
        >>> sorted(child.keys())
        ['a', 'b', 'c']
        >>> expect = {'a': 10, 'b': 2, 'c': 3}
        >>> assert child == expect, "%s != %s" % (child, expect)

    Unless the child is already masking out a certain key::

        >>> del parent['a']
        >>> parent.keys()
        ['c']
        >>> assert child == expect, "%s != %s" % (child, expect)

    However, this doesn't work::

        >>> def print_sorted(**kwargs):
        ...     for k in sorted(kwargs.keys()):
        ...         print "%r=%r" % (k, kwargs[k])
        >>> child['c'] == 3
        True
        >>> print_sorted(**child)
        'a'=10
        'b'=2
        'c'=3

    """
    __slots__ = ('_', 'parent')

    def __init__(self, parent, **data):
        self.parent = parent
        self._ = data

    def __getitem__(self, key):
        try:
            return self._[key]
        except KeyError:
            return self.parent[key]

    def __iter__(self):
        return self.iterkeys()

    def __setitem__(self, key, val):
        self._[key] = val

    def __delitem__(self, key):
        del self._[key]

    def __len__(self):
        return len(self.keys())

    def keys(self, own=False):
        return list(self.iterkeys(own))

    def items(self, own=False):
        return list(self.iteritems(own))

    def iterkeys(self, own=False):
        if own:
            for k in self._.iterkeys():
                yield k
            return
        yielded = set([])
        for k in chain(self.parent.iterkeys(), self._.iterkeys()):
            if k in yielded:
                continue
            yield k
            yielded.add(k)

    def iteritems(self, own=False):
        for k in self.iterkeys(own):
            yield k, self[k]

    def __eq__(self, other):
        return sorted(self.iteritems()) == sorted(other.iteritems())

    def __repr__(self):
        return dict(self.iteritems()).__repr__()

    def __contains__(self, key):
        return key in self._ or key in self.parent

    def containing(self, key):
        """
        Return the ancestor that directly contains ``key``

        >>> p2 = {'a', 2}
        >>> p1 = ChainableMap(p2)
        >>> c = ChainableMap(p1)
        >>> c.containing('a') is p2
        True
        """
        if key in self._:
            return self
        elif hasattr(self.parent, 'containing'):
            return self.parent.containing(key)
        elif key in self.parent:
            return self.parent

    def get(self, key, default=None):
        """
        >>> c = ChainableMap({'a': 1})
        >>> c.get('a')
        1
        >>> c.get('b', 'default')
        'default'
        """
        if key in self:
            return self[key]
        else:
            return default

    def pushdown(self, top):
        """
        Pushes a new mapping onto the top of the delegation chain:

        >>> parent = {'a': 10}
        >>> child = ChainableMap(parent)
        >>> top = {'a': 'apple', 'b': 'beer', 'c': 'cheese'}
        >>> child.pushdown(top)
        >>> assert child == top

        This creates a new ChainableMap with the contents of ``child`` and makes it
        the new parent (the old parent becomes the grandparent):

        >>> child.parent.parent is parent
        True
        >>> del child['a']
        >>> child['a'] == 10
        True
        """
        old = ChainableMap(self.parent)
        for k, v in self.items(True):
            old[k] = v
            del self[k]
        self.parent = old
        for k, v in top.iteritems():
            self[k] = v
4

1 回答 1

10

创建关键字参数字典时,行为与将对象传递给dict()初始化程序相同,这会导致对象的{'b': 2}字典cm

>>> cm = ChainableMap({'a': 1})
>>> cm['b'] = 2
>>> dict(cm)
{'b': 2}

下面是对这种情况的更详细解释,但总结是,您的映射被转换为 C 代码中的 Python 字典,如果参数本身是另一个字典,则通过绕过 Python 函数调用并检查直接底层 C 对象。

有几种方法可以解决这个问题,要么确保底层 dict 包含您想要的所有内容,要么停止从 dict 继承(这也需要其他更改,至少是一种__setitem__方法)。

编辑:听起来BrenBarn 的建议是继承collections.MutableMapping而不是dict成功。

您只需添加self.update(parent)to即可完成第一种方法ChainableMap.__init__(),但我不确定这是否会对您的类的行为产生其他副作用。

为什么dict(cm)给出{'b': 2}的解释:

查看 dict 对象的以下 CPython 代码:http:
//hg.python.org/releasing/2.7.3/file/7bb96963d067/Objects/dictobject.c#l1522

dict(cm)被调用时(并且当关键字参数被解包时),PyDict_Merge函数cm作为b参数被调用。因为 ChainableMap 继承自 dict,所以在第 1539 行输入 if 语句:

if (PyDict_Check(b)) {
    other = (PyDictObject*)b;
    ...

从那里开始,来自的项目other被添加到通过直接访问 C 对象创建的新 dict 中,这绕过了您覆盖的所有方法。

This means that any items in a ChainableMap instance that are accessed through the parent attribute will not be added to the new dictionary created by dict() or keyword argument unpacking.

于 2012-08-09T19:23:34.313 回答