2012-08-09 7 views
15

Ho una classe contenitore personalizzata in Python 2.7 e tutto funziona come previsto eccetto se passo provo ad espandere un'istanza come **kwargs per un funzione:Creazione di contenitori personalizzati con ** kwargs (in che modo Python espande gli argomenti?)

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 

ho sovrascritto __getitem__, __iter__, iterkeys, keys, items, e iteritems, (e __eq__ e __repr__) eppure nessuno di loro sembrano essere coinvolti nell'espansione come **kwargs, cosa sto facendo sbagliato?

Edit - Il lavoro fonte aggiornata che ora eredita da MutableMapping e aggiunge i metodi mancanti:

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

Provare a passare con un debugger (o scrivere istruzioni 'print' in ogni funzione sovraccaricata) per vedere quale funzione viene chiamata all'espansione degli argomenti. – Lanaru

+0

Nota che anche se dovesse funzionare, 'check_args' otterrebbe un dizionario * nuovo *, non la sottoclasse. Vedi [la documentazione delle definizioni delle funzioni] (http://docs.python.org/reference/compound_stmts.html#function-definitions); specificatamente * "Se il form" ** identificatore "è presente, viene inizializzato in un nuovo dizionario che riceve gli argomenti di parole chiave in eccesso, per impostazione predefinita in un nuovo dizionario vuoto." *. –

+0

@Lanaru che mette un 'import pdb; pdb.set_trace() 'immediatamente prima della chiamata a' check_kwargs' e facendo un singolo passo mi metto al di là degli argomenti del punto che sono espansi. Mettere lo stesso 'set_trace' in ogni funzione sovrascritta mostra che nessuno di loro è stato chiamato. – grncdr

risposta

9

Quando si crea un dizionario argomento chiave, il comportamento è lo stesso di passare il vostro oggetto nel dict() inizializzatore, che risultati nel dict {'b': 2} per la vostra cm oggetto:

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

Una spiegazione più dettagliata del motivo per cui questo è il caso è al di sotto, ma la sintesi è che il vostro mappatura è conv erted ad un dizionario Python in codice C che fa qualche ottimizzazione se l'argomento è esso stesso un altro dict, aggirando le chiamate alla funzione Python e ispezionando direttamente l'oggetto C sottostante.

ci sono alcuni modi per affrontare la soluzione per questo, o assicurarsi che il dict sottostante contiene tutto quello che volete, o interrompere eredita da dict (che richiederà altri cambiamenti così, per lo meno un metodo __setitem__) .

edit: Sembra BrenBarn's suggestion di ereditare da collections.MutableMapping invece di dict ha fatto il trucco.

È possibile eseguire il primo metodo semplicemente aggiungendo self.update(parent) a ChainableMap.__init__(), ma non sono sicuro che ciò causerà altri effetti collaterali sul comportamento della classe.

Spiegazione del perché dict(cm){'b': 2}:

Partenza il seguente codice CPython per l'oggetto dict:
http://hg.python.org/releasing/2.7.3/file/7bb96963d067/Objects/dictobject.c#l1522

Quando dict(cm) si chiama (e quando gli argomenti delle parole chiave vengono decompressi), il PyDict_Merge la funzione viene chiamata con cm come parametro b. Perché ChainableMap eredita da dict, viene inserito l'istruzione if alla riga 1539:

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

Da lì in poi, gli elementi da other vengono aggiunti alla nuova dict che si sta creando accedendo l'oggetto C direttamente, che bypassa tutti i metodi che hai sovrascritto.

Ciò significa che qualsiasi elemento in un'istanza ChainableMap a cui si accede tramite l'attributo parent non verrà aggiunto al nuovo dizionario creato da dict() o dall'argomento della parola chiave decompressione.

+0

Sfortunatamente gli altri effetti collaterali non sarebbero accettabili per il mio caso d'uso. Sono passato ad ereditare da 'collections.MutableMapping' e questo ha risolto i miei problemi. – grncdr

+0

@grncdr: Anche se si eredita da 'collections.MutableMapping', è comunque necessario fornire' __len __() ', che manca nel codice di esempio. –

+0

@Fj: L'argomento keyword unpacking in realtà non chiama 'dict()' sull'argomento che segue '**', ma piuttosto 'PyDict_Update()' con un dizionario appena creato e la mappatura, ma questa chiamata finirà anche a 'PyDictMerge()', quindi è essenzialmente come hai detto tu. –

Problemi correlati