2013-10-18 4 views
12

Ich versuche, eine YAML-Sequenz in Python zu erstellen, die ein benutzerdefiniertes Python-Objekt erstellt. Das Objekt muss mit Dicts und Listen erstellt werden, die nach __init__ dekonstruiert werden. Es scheint jedoch, dass die construct_mapping-Funktion nicht den gesamten Baum der eingebetteten Sequenzen (Listen) und dicts erstellt.
Beachten Sie Folgendes:Gibt es eine Möglichkeit, ein Objekt mithilfe von PyYAML construct_mapping zu erstellen, nachdem alle Knoten vollständig geladen wurden?

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])' 

Dies funktioniert gut, weil f die Verweise auf die l und d Objekte enthält, die mit Daten tatsächlich gefüllt sind nach das Foo Objekt erstellt wird.

Jetzt wollen wir etwas tun, ein ganz klein wenig komplizierter:

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 

Jetzt bekommen wir die Fehler folgende

Traceback (most recent call last): 
    File "test.py", line 27, in <module> 
    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 

Dies ist, weil der yaml Konstruktor wird vor der Verschachtelung an der äußeren Schicht beginnt und Konstruieren des Objekts, bevor alle Knoten beendet sind. Gibt es eine Möglichkeit, die Reihenfolge umzukehren und zuerst mit tief eingebetteten (z. B. verschachtelten) Objekten zu beginnen? Alternativ gibt es eine Möglichkeit, um die Konstruktion zu erreichen, mindestens nach die Objekte des Knotens wurden geladen?

Antwort

19

Nun, was weißt du. Die Lösung, die ich fand, war so einfach, aber nicht so gut dokumentiert.

Die Loader class documentation zeigt deutlich die construct_mapping Methode dauert nur in einem einzigen Parameter (node). Nachdem ich meinen eigenen Konstruktor geschrieben hatte, überprüfte ich die Quelle und die Antwort war right there! Die Methode übernimmt auch einen Parameter deep (Standardwert False).

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

also die richtige Konstruktor-Methode zu verwenden, ist

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

Ich denke, PyYaml einige zusätzliche Dokumentation verwenden könnte, aber ich bin dankbar, dass es bereits existiert.

+2

** Danke! ** Das ersparte mir ein paar ausgezogene Haare. –

+0

Ich wünschte, ich könnte diesem Typ mehr Punkte geben. – Dacav

+0

OMG! zu guter Letzt !Ich würde dich für diesen Post küssen :-) – Penbeuz

6

tl; dr:
ersetzen Ihre foo_constructor mit dem in der Code am Ende dieser Antwort


Es gibt mehrere Probleme mit Ihrem Code (und Ihre Lösung), lassen Sie uns adressiere sie Schritt für Schritt.

Der Code, den Sie drucken nicht vorstellen, was es in der unteren Zeile Kommentar sagt, ('Foo(1, {'try': 'this'}, [1, 2])'), da es keine __str__() für Foo definiert ist, gibt es so etwas wie:

__main__.Foo object at 0x7fa9e78ce850 

Diese leicht durch Zugabe behoben die folgende Methode Foo:

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

und wenn Sie dann am Ausgang aussehen:

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

Dies ist nahe, aber nicht das, was Sie in dem Kommentar entweder versprochen haben. Die list und die dict sind vertauscht, weil Sie in Ihrer foo_constructor()Foo() mit der falschen Reihenfolge der Parameter erstellen.
Dies deutet auf ein grundlegenderes Problem hin, das Ihr foo_constructor()zu viel über das Objekt wissen muss, das es erstellt. Warum ist das so? Es ist nicht nur der Parameter um, versuchen:

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

print(f) 

Man würde erwarten, dass dieses Foo(1, None, [1, 2]) (mit dem Standardwert des nicht spezifizierten d Schlüsselwort-Argument) zu drucken.
Was Sie erhalten, ist eine KeyError-Ausnahme auf d = value['d'].

Sie Nutzungs können get('d') usw. in foo_constructor() dieses Problem zu lösen, aber Sie müssen erkennen, dass für das richtige Verhalten Sie muss die Standardwerte von Ihrem Foo.__init__() angeben (die nur in Ihrem Fall passieren alle None sein), für jeden Parameter mit einem Standardwert:

Dies zu aktualisieren ist natürlich ein Wartungsalbtraum.

So verschrotten die ganze foo_constructor und ersetzen sie durch etwas, das aussieht wie PyYAML tut dies intern:

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

Diese Griffe (Standard) Parameter fehlt und haben nicht auf, wenn die Standardeinstellungen für aktualisiert werden Ihre Keyword-Argumente ändern sich.

All dies in einem vollständigen Beispiel einer selbst verweis Verwendung des Objekts einschließlich (immer heikel):

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']) 

gibt:

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

Dieses getestet wurde ruamel.yaml mit (von denen ich der Autor bin), die eine erweiterte Version von PyYAML ist. Die Lösung sollte für PyYAML selbst gleich funktionieren.

+0

Für diejenigen, die sich für die blutigen Details darüber interessieren, warum Sie die (teilweise) erstellte Instanz erstellen müssen, schauen Sie sich 'constructor.py:BaseConstructor.construct_object()' an, dort wird ein Test gemacht um zu sehen, ob der registrierte Konstruktor (dh 'foo_constructor()') einen 'GeneratorType' zurückgibt und die entsprechende Aktion ausgeführt wird. – Anthon

Verwandte Themen