Создание пользовательских контейнеров работает с ** kwargs (как Python расширяет аргументы?)

У меня есть собственный класс контейнера в 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, (и __eq__ и __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

Ответы

Ответ 1

При создании словаря аргументов ключевого слова поведение такое же, как передача объекта в инициализатор dict(), что приводит к dict {'b': 2} для вашего объекта cm:

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

Более подробное объяснение того, почему это так, ниже, но резюме заключается в том, что ваше преобразование преобразуется в словарь Python в C-коде, который делает некоторую оптимизацию, если аргумент сам по себе является другим dict, минуя вызовы функции Python и непосредственно проверяя основной объект C.

Есть несколько способов подойти к решению для этого, либо убедитесь, что базовый dict содержит все, что вы хотите, или прекратите наследование от dict (что потребует и других изменений, по крайней мере, метода __setitem__).

edit: Похоже, предложение BrenBarn для наследования от collections.MutableMapping вместо dict сделал трюк.

Вы можете выполнить первый метод довольно просто, просто добавив self.update(parent) в ChainableMap.__init__(), но я не уверен, что это вызовет другие побочные эффекты для поведения вашего класса.

Объяснение причины dict(cm) дает {'b': 2}:

Проверьте следующий код CPython для объекта dict:
http://hg.python.org/releasing/2.7.3/file/7bb96963d067/Objects/dictobject.c#l1522

Когда вызывается dict(cm) (и когда аргументы ключевого слова распакованы), функция PyDict_Merge вызывается с cm как параметр b. Поскольку ChainableMap наследуется от dict, вводится оператор if на строке 1539:

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

Оттуда, элементы из other добавляются в новый dict, который создается путем прямого доступа к объекту C, который обходит все методы, которые вы перезаписывали.

Это означает, что любые элементы экземпляра ChainableMap, к которым осуществляется доступ через атрибут parent, не будут добавлены в новый словарь, созданный при распаковке dict() или ключевого слова.