Создание пользовательских контейнеров работает с ** 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()
или ключевого слова.