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およびiteritems, (and __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()の dict{'b': 2}が生成されます。cm

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

これが当てはまる理由の詳細な説明を以下に示しますが、要約すると、マッピングは C コードの Python 辞書に変換され、引数自体が別の dict である場合、Python 関数呼び出しをバイパスして基になる C オブジェクトを直接。

これを解決するには、いくつかの方法があります。基になる dict に必要なものがすべて含まれていることを確認するか、 dict からの継承を停止します (これには、他の変更も必要であり、少なくとも__setitem__メソッドが必要です)。

編集:トリックを行う代わりに、継承するというブレンバーンの提案のように思えます。collections.MutableMappingdict

に追加self.update(parent)するだけで最初のメソッドをかなり簡単に実現できますがChainableMap.__init__()、それがクラスの動作に他の副作用を引き起こすかどうかはわかりません。

理由のdict(cm)説明{'b': 2}

次の dict オブジェクトの CPython コードを確認して
ください。

が呼び出されるときdict(cm)(およびキーワード引数がアンパックされるとき)、PyDict_Merge関数はパラメーターcmとして呼び出されbます。ChainableMap は dict を継承するため、1539 行目の if ステートメントを入力します。

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

そこから、other上書きしたすべてのメソッドをバイパスして、C オブジェクトに直接アクセスすることによって作成される新しい dict に項目が追加されます。

これは、属性を介してアクセスされる ChainableMap インスタンス内のアイテムは、またはキーワード引数のアンパックparentによって作成された新しい辞書に追加されないことを意味します。dict()

于 2012-08-09T19:23:34.313 に答える