2012-08-09 14 views
15

私はPython 2.7でカスタムコンテナクラスを持っていますが、以外は以外はすべて**kwargs機能:カスタムコンテナを** kwargsと連携させる(Pythonはどのように引数を展開するのですか?)

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 

私は、keysiterkeys__iter____getitem__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 
+0

引数展開時にどの関数が呼び出されているのかを確認するには、デバッガを使用してステップオーバーするか、オーバーロードされたすべての関数に 'print'文を記述します。 – Lanaru

+0

これが有効であったとしても、 'check_args'はサブクラスではなく* new *辞書を取得することに注意してください。 [関数定義のドキュメント](http://docs.python.org/reference/compound_stmts.html#function-definitions)を参照してください。 * "フォームの" ** identifier "が存在する場合は、余分なキーワード引数を受け取った新しい辞書に初期化され、新しい空の辞書にデフォルト設定されます。 –

+0

@Lanaruは 'pdbをインポートします; 'check_kwargs'の呼び出しの直前に' pdb.set_trace() 'を実行して、単一のステップを実行すると、引数argが展開されてしまいます。上書きされた各関数に同じ 'set_trace'を置くと、それらのどれも呼び出されていないことがわかります。 – grncdr

答えて

9

キーワード引数辞書を作成する場合、動作はdict()初期化子にあなたのオブジェクトを渡すのと同じであるが、これあなたのcmオブジェクトのためのdict {'b': 2}での結果:

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

このようなケースである理由のより詳細な説明は以下の通りですが、概要はあなたのマッピングがコンバージョンであるということですPythonの関数呼び出しを迂回し、基礎をなすCオブジェクトを直接検査することによって、引数が別のdictである場合に最適化を行うCコードのPython辞書に抹消されました。

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

編集:それはcollections.MutableMapping代わりのdictから継承するBrenBarn's suggestionように聞こえるトリックをしました。

self.update(parent)ChainableMap.__init__()に追加するだけで簡単に最初の方法を実行できますが、クラスの動作に他の副作用が生じるかどうかはわかりません。

のdictオブジェクトに対して次のCPythonのコードチェックアウト:dict(cm)が呼び出されると(とキーワード引数がアンパックされたとき)、PyDict_Merge
http://hg.python.org/releasing/2.7.3/file/7bb96963d067/Objects/dictobject.c#l1522

dict(cm){'b': 2}を与える理由の

説明ファンクションは、cmbパラメータとして呼び出されます。 ChainableMapは辞書から継承しているので、ライン1539でのif文が入力されます。そこにから

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

otherからのアイテムは全てをバイパスされ、直接Cのオブジェクトにアクセスすることにより作成される新しい辞書に追加されますあなたが上書きしたメソッド。

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

+0

残念ながら、他の副作用は私の使用事例では受け入れられません。私は 'collections.MutableMapping'から継承するように変更しました。それが私の問題を解決しました。 – grncdr

+0

@grncdr: 'collections.MutableMapping'を継承しても、あなたのサンプルコードから抜けている' __len __() 'を提供する必要があります。 –

+0

@Fj:キーワード引数のアンパックは、実際には '**'の後の引数で 'dict()'を呼び出すのではなく、新しく作成された辞書とマッピングで 'PyDict_Update()'を呼びますが、 'PyDictMerge()'のように、本質的にあなたが言った通りです。 –

関連する問題