web-dev-qa-db-ja.com

すべてのノードのロードが完了した後、PyYAMLconstruct_mappingを使用してオブジェクトを構築する方法はありますか?

pythonでカスタムpythonオブジェクトを作成するyamlシーケンスを作成しようとしています。オブジェクトは、__init__の後に分解されるdictとリストで構築する必要があります。ただし、construct_mapping関数は、埋め込まれたシーケンス(リスト)とdictのツリー全体を構築しないようです。
次のことを考慮してください。

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

flおよびdオブジェクトへの参照を保持しているため、これは正常に機能します。これらのオブジェクトは実際にはデータで満たされていますafterFooオブジェクトが作成されます。

それでは、もっと複雑なsmidgenを実行してみましょう。

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

次のエラーが発生します

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

これは、yamlコンストラクターがネストの外側のレイヤーから開始し、すべてのノードが終了する前にオブジェクトを構築しているためです。順序を逆にして、最初に深く埋め込まれた(ネストされたなどの)オブジェクトから始める方法はありますか?あるいは、ノードのオブジェクトがロードされた後、少なくともに構築を実行する方法はありますか?

16
scicalculator

さて、あなたは何を知っていますか。私が見つけた解決策はとても単純でしたが、十分に文書化されていませんでした。

ローダークラスのドキュメント は、construct_mappingメソッドが単一のパラメーター(node)のみを受け取ることを明確に示しています。しかし、独自のコンストラクターを作成することを検討した後、ソースをチェックアウトしたところ、答えは すぐそこにあります !このメソッドは、パラメーターdeep(デフォルトはFalse)も受け取ります。

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

したがって、使用する正しいコンストラクタメソッドは次のとおりです。

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

PyYamlはいくつかの追加のドキュメントを使用できると思いますが、すでに存在していることに感謝しています。

27
scicalculator

tl; dr:
_foo_constructor_をこの回答の下部にあるコードの1つに置き換えます


コード(およびソリューション)にはいくつかの問題があります。それらに段階的に対処しましょう。

提示するコードは、Fooに対して'Foo(1, {'try': 'this'}, [1, 2])'が定義されていないため、最終コメント(__str__())にある内容を出力しません。次のように出力します。

___main__.Foo object at 0x7fa9e78ce850
_

これは、次のメソッドをFooに追加することで簡単に修正できます。

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

そして、出力を見ると:

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

これは近いですが、コメントで約束したことでもありません。 listdictは交換されます。これは、foo_constructor()でパラメーターの順序が間違っているFoo()を作成するためです。
これは、foo_constructor()が作成しているオブジェクトについて多くのことを知る必要があるというより根本的な問題を示しています 。なぜそうなのですか?パラメータの順序だけではありません。次のことを試してください。

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

print(f)
_

これにより、Foo(1, None, [1, 2])が出力されることが期待されます(指定されていないdキーワード引数のデフォルト値を使用)。
取得するのは_d = value['d']_のKeyError例外です。

これを解決するには、get('d')foo_constructor()などを使用できますが、正しい動作を行うには、が必要であることを理解する必要がありますデフォルト値を持つすべてのパラメーターについて、Foo.__init__()(この場合はすべてNone)からデフォルト値を指定します。

_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)
_

もちろん、これを最新の状態に保つことは、メンテナンスの悪夢です。

したがって、_foo_constructor_全体を廃棄し、PyYAMLが内部でこれを行う方法に似たものに置き換えます。

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

これは欠落している(デフォルトの)パラメーターを処理し、キーワード引数のデフォルトが変更された場合に更新する必要はありません。

オブジェクトの自己参照使用を含む、完全な例でのこれらすべて(常にトリッキー):

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

与える:

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

これは、PyYAMLの拡張バージョンである ruamel.yaml (私が作成者です)を使用してテストされました。このソリューションは、PyYAML自体でも同じように機能するはずです。

9
Anthon

あなた自身の答え に加えて、scicalculator:次回このフラグを覚える必要がない場合、および/またはよりオブジェクト指向のアプローチが必要な場合は、 yamlable 、本番コードのyamlからオブジェクトへのバインドを簡単にするために作成しました。

これはあなたがあなたの例を書く方法です:

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)

収量

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

そしてあなたも捨てることができます:

>>> print(yaml.safe_dump(f))

!yamlable/com.example.Foo
d: {try: this}
l: [1, 2]
s: 1

2つの方法がどのようにto_yaml_dictおよびfrom_yaml_dictをオーバーライドして、両方向のマッピングをカスタマイズできます。

1
smarie