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])'
f
はl
および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コンストラクターがネストの外側のレイヤーから開始し、すべてのノードが終了する前にオブジェクトを構築しているためです。順序を逆にして、最初に深く埋め込まれた(ネストされたなどの)オブジェクトから始める方法はありますか?あるいは、ノードのオブジェクトがロードされた後、少なくともに構築を実行する方法はありますか?
さて、あなたは何を知っていますか。私が見つけた解決策はとても単純でしたが、十分に文書化されていませんでした。
ローダークラスのドキュメント は、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はいくつかの追加のドキュメントを使用できると思いますが、すでに存在していることに感謝しています。
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'})
_
これは近いですが、コメントで約束したことでもありません。 list
とdict
は交換されます。これは、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自体でも同じように機能するはずです。
あなた自身の答え に加えて、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
をオーバーライドして、両方向のマッピングをカスタマイズできます。