2012-08-09 11 views
15

Tengo una clase de contenedor personalizada en Python 2.7 y todo funciona como esperaba excepto si paso intente expandir una instancia como **kwargs para un función:Haciendo que los contenedores personalizados funcionen con ** kwargs (¿cómo amplía Python los argumentos?)

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 

hemos anulado __getitem__, __iter__, iterkeys, keys, items y iteritems, (y __eq__ y __repr__) sin embargo, ninguno de ellos parecen estar implicados en la expansión como **kwargs, ¿qué estoy haciendo ¿incorrecto?

Editar - La fuente actualizada de trabajo que ahora hereda de MutableMapping y añade los métodos que faltan:

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

Intente avanzar con un depurador (o escribir declaraciones 'print' en cada función sobrecargada) para ver qué función se invoca cuando se expande el argumento. – Lanaru

+0

Tenga en cuenta que incluso si esto fuera a funcionar, 'check_args' obtendría un * nuevo * diccionario, no su subclase. Ver [la documentación de definiciones de funciones] (http://docs.python.org/reference/compound_stmts.html#function-definitions); específicamente * "Si el formulario" ** identificador "está presente, se inicializa a un nuevo diccionario que recibe cualquier exceso de argumentos de palabra clave, por defecto a un nuevo diccionario vacío." *. –

+0

@Lanaru poniendo 'import pdb; pdb.set_trace() 'inmediatamente antes de la llamada a' check_kwargs' y haciendo un solo paso me pasa el punto args se expanden. Poner el mismo 'set_trace' en cada función anulada muestra que no se llama a ninguno de ellos. – grncdr

Respuesta

9

Al crear un diccionario argumento de palabra clave, el comportamiento es el mismo que el que pasa el objeto en el dict() inicializador, que da como resultado la dict {'b': 2} para su cm objeto:

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

Una explicación más detallada de por qué esto es así es abajo, pero el resumen es que su asignación es conv creado a un diccionario de Python en código C que hace algo de optimización si el argumento es en sí mismo otro dict, evitando las llamadas a la función Python e inspeccionando directamente el objeto C subyacente.

Hay algunas maneras de acercarse a la solución para esto, ya sea que se asegure de que el dict subyacente contenga todo lo que desee o deje de heredar de dict (que requerirá otros cambios también, al menos un método __setitem__) .

edición: Suena como BrenBarn's suggestion heredar de collections.MutableMapping en lugar de dict hizo el truco.

Puede completar el primer método simplemente agregando self.update(parent) a ChainableMap.__init__(), pero no estoy seguro si eso causará otros efectos secundarios al comportamiento de su clase.

Explicación de por qué dict(cm) da {'b': 2}:

revisa el siguiente código CPython para el objeto dict:
http://hg.python.org/releasing/2.7.3/file/7bb96963d067/Objects/dictobject.c#l1522

Cuando se llama dict(cm) (y cuando argumentos son sin embalaje), la PyDict_Merge función se llama con cm como el parámetro b. Debido ChainableMap hereda de dict, se introduce la instrucción if en la línea 1539:

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

A partir de ahí, artículos de other se añaden a la nueva dict que está siendo creado por el acceso al objeto C directamente, lo que deja sin efecto todos los métodos que sobrescribió.

Esto significa que los elementos de una instancia de ChainableMap a los que se accede a través del atributo parent no se agregarán al nuevo diccionario creado por dict() o desempaquetar palabras clave.

+0

Lamentablemente, los otros efectos secundarios no serían aceptables para mi caso de uso. He cambiado a la herencia de 'collections.MutableMapping' y eso ha solucionado mis problemas. – grncdr

+0

@grncdr: Aun cuando heredes de 'collections.MutableMapping', aún deberías proporcionar' __len __() ', que falta en tu código de ejemplo. –

+0

@Fj: el desempaquetado de palabras clave no llama a 'dict()' en el argumento que sigue a '**', sino a 'PyDict_Update()' con un diccionario recién creado y la asignación, pero esta llamada también terminará en 'PyDictMerge()', así que es esencialmente como dijiste. –

Cuestiones relacionadas