¿Hay una manera de construir un objeto usando PyYAML construct_mapping después de que todos los nodos se hayan cargado por completo?

Estoy tratando de hacer una secuencia yaml en python que crea un objeto python personalizado. El objeto debe construirse con dictados y listas que se deconstruyen después de __init__ . Sin embargo, parece que la función construct_mapping no construye el árbol completo de secuencias incrustadas (listas) y dictados.
Considera lo siguiente:

 import yaml class Foo(object): def __init__(self, s, l=None, d=None): self.s = s self.l = l self.d = d def foo_constructor(loader, node): values = loader.construct_mapping(node) s = values["s"] d = values["d"] l = values["l"] return Foo(s, d, l) yaml.add_constructor(u'!Foo', foo_constructor) f = yaml.load(''' --- !Foo s: 1 l: [1, 2] d: {try: this}''') print(f) # prints: 'Foo(1, {'try': 'this'}, [1, 2])' 

Esto funciona bien porque f contiene las referencias a los objetos l y d , que en realidad se rellenan con datos una vez que se crea el objeto Foo .

Ahora, hagamos algo un poco más complicado:

 class Foo(object): def __init__(self, s, l=None, d=None): self.s = s # assume two-value list for l self.l1, self.l2 = l self.d = d 

Ahora obtenemos el siguiente error

 Traceback (most recent call last): File "test.py", line 27, in  d: {try: this}''') File "/opt/homebrew/lib/python2.7/site-packages/yaml/__init__.py", line 71, in load return loader.get_single_data() File "/opt/homebrew/lib/python2.7/site-packages/yaml/constructor.py", line 39, in get_single_data return self.construct_document(node) File "/opt/homebrew/lib/python2.7/site-packages/yaml/constructor.py", line 43, in construct_document data = self.construct_object(node) File "/opt/homebrew/lib/python2.7/site-packages/yaml/constructor.py", line 88, in construct_object data = constructor(self, node) File "test.py", line 19, in foo_constructor return Foo(s, d, l) File "test.py", line 7, in __init__ self.l1, self.l2 = l ValueError: need more than 0 values to unpack 

Esto se debe a que el constructor yaml está comenzando en la capa exterior de anidamiento antes y está construyendo el objeto antes de que se terminen todos los nodos. ¿Hay una manera de invertir el orden y comenzar con objetos profundamente incrustados (por ejemplo, nesteds) primero? Alternativamente, ¿hay una manera de hacer que la construcción ocurra al menos después de que se hayan cargado los objetos del nodo?

Bueno, qué sabes. La solución que encontré fue tan simple, pero no tan bien documentada.

La documentación de la clase Loader muestra claramente que el método construct_mapping solo toma en un solo parámetro ( node ). Sin embargo, después de considerar escribir mi propio constructor, comprobé la fuente, ¡y la respuesta estaba allí ! El método también toma un parámetro de deep (por defecto Falso).

 def construct_mapping(self, node, deep=False): #... 

Por lo tanto, el método constructor correcto a utilizar es

 def foo_constructor(loader, node): values = loader.construct_mapping(node, deep=True) #... 

Supongo que PyYaml podría usar alguna documentación adicional, pero estoy agradecido de que ya existe.

tl; dr:
reemplaza tu foo_constructor con el que está en el código al final de esta respuesta


Hay varios problemas con su código (y su solución), tratémoslos paso a paso.

El código que presentas no imprimirá lo que dice en el comentario final, ( 'Foo(1, {'try': 'this'}, [1, 2])' ) ya que no hay __str__() definido para Foo , imprime algo como:

 __main__.Foo object at 0x7fa9e78ce850 

Esto se puede remediar fácilmente agregando el siguiente método a Foo :

  def __str__(self): # print scalar, dict and list return('Foo({s}, {d}, {l})'.format(**self.__dict__)) 

Y si luego miras la salida:

 Foo(1, [1, 2], {'try': 'this'}) 

Esto está cerca, pero tampoco es lo que prometiste en el comentario. La list y el dict se intercambian, porque en su foo_constructor() creas Foo() con el orden incorrecto de los parámetros.
Esto apunta a un problema más fundamental que su foo_constructor() necesita saber mucho sobre el objeto que está creando . ¿Por qué esto es tan? No es solo el orden de los parámetros, intente:

 f = yaml.load(''' --- !Foo s: 1 l: [1, 2] ''') print(f) 

Uno esperaría que esto imprima Foo(1, None, [1, 2]) (con el valor predeterminado del argumento de la palabra clave d no especificado).
Lo que obtienes es una excepción KeyError en d = value['d'] .

Puede usar get('d') , etc., en foo_constructor() para resolver esto, pero debe darse cuenta de que para un comportamiento correcto debe especificar los valores predeterminados de su Foo.__init__() (que en su caso, simplemente pasa a ser todo None ), para todos y cada uno de los parámetros con un valor predeterminado:

 def foo_constructor(loader, node): values = loader.construct_mapping(node, deep=True) s = values["s"] d = values.get("d", None) l = values.get("l", None) return Foo(s, l, d) 

Mantener esto actualizado es, por supuesto, una pesadilla de mantenimiento.

Así que foo_constructor todo foo_constructor y reemplácelo con algo que se parezca más a cómo PyYAML hace esto internamente:

 def foo_constructor(loader, node): instance = Foo.__new__(Foo) yield instance state = loader.construct_mapping(node, deep=True) instance.__init__(**state) 

Esto maneja los parámetros faltantes (predeterminados) y no tiene que actualizarse si los valores predeterminados para los argumentos de sus palabras clave cambian.

Todo esto en un ejemplo completo, que incluye un uso autorreferencial del objeto (siempre es complicado):

 class Foo(object): def __init__(self, s, l=None, d=None): self.s = s self.l1, self.l2 = l self.d = d def __str__(self): # print scalar, dict and list return('Foo({s}, {d}, [{l1}, {l2}])'.format(**self.__dict__)) def foo_constructor(loader, node): instance = Foo.__new__(Foo) yield instance state = loader.construct_mapping(node, deep=True) instance.__init__(**state) yaml.add_constructor(u'!Foo', foo_constructor) print(yaml.load(''' --- !Foo s: 1 l: [1, 2] d: {try: this}''')) print(yaml.load(''' --- !Foo s: 1 l: [1, 2] ''')) print(yaml.load(''' &fooref a: !Foo s: *fooref l: [1, 2] d: {try: this} ''')['a']) 

da:

 Foo(1, {'try': 'this'}, [1, 2]) Foo(1, None, [1, 2]) Foo({'a': <__main__.Foo object at 0xba9876543210>}, {'try': 'this'}, [1, 2]) 

Esto se probó utilizando ruamel.yaml (del cual soy el autor), que es una versión mejorada de PyYAML. La solución debería funcionar igual para PyYAML.

Además de su propia respuesta , scicalculator: si no desea tener que recordar esta bandera la próxima vez, y / o desea tener un enfoque más orientado a objetos, puede usar yamlable , lo escribí para facilitar el yaml-to- Objeto vinculante para nuestro código de producción.

Así es como escribirías tu ejemplo:

 import yaml from yamlable import YamlAble, yaml_info @yaml_info(yaml_tag_ns="com.example") class Foo(YamlAble): def __init__(self, s, l=None, d=None): self.s = s # assume two-value list for l self.l1, self.l2 = l self.d = d def __str__(self): return "Foo({s}, {d}, {l})".format(s=self.s, d=self.d, l=[self.l1, self.l2]) def to_yaml_dict(self): """ override because we do not want the default vars(self) """ return {'s': self.s, 'l': [self.l1, self.l2], 'd': self.d} # @classmethod # def from_yaml_dict(cls, dct, yaml_tag): # return cls(**dct) f = yaml.safe_load(''' --- !yamlable/com.example.Foo s: 1 l: [1, 2] d: {try: this}''') print(f) 

rendimientos

 Foo(1, {'try': 'this'}, [1, 2]) 

y usted puede volcar también:

 >>> print(yaml.safe_dump(f)) !yamlable/com.example.Foo d: {try: this} l: [1, 2] s: 1 

Observe cómo los dos métodos to_yaml_dict y from_yaml_dict pueden from_yaml_dict para personalizar la asignación en ambas direcciones.