Я хотів би отримати завантажувач PyYAML для завантаження відображень (і замовлених відображень) у тип Python 2.7+ OrdersDict замість ванілі dictта списку пар, якими він користується.
Який найкращий спосіб зробити це?
Я хотів би отримати завантажувач PyYAML для завантаження відображень (і замовлених відображень) у тип Python 2.7+ OrdersDict замість ванілі dictта списку пар, якими він користується.
Який найкращий спосіб зробити це?
Відповіді:
Оновлення: у python 3.6+ вам, мабуть, взагалі не потрібно OrderedDictчерез нову реалізацію dict , яка використовується в pypy протягом певного часу (хоча зараз розглядається детальна версія CPython).
Оновлення: У python 3.7++ характер збереження порядку вставки об’єктів dict був оголошений офіційною частиною специфікації мови Python , див. Що нового в Python 3.7 .
Мені подобається рішення @James за його простоту. Однак це змінює глобальний yaml.Loaderклас за замовчуванням , що може призвести до проблемних побічних ефектів. Тим більше, що при написанні бібліотечного коду це погана ідея. Крім того, це не працює безпосередньо yaml.safe_load().
На щастя, рішення можна вдосконалити без особливих зусиль:
import yaml
from collections import OrderedDict
def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict):
class OrderedLoader(Loader):
pass
def construct_mapping(loader, node):
loader.flatten_mapping(node)
return object_pairs_hook(loader.construct_pairs(node))
OrderedLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
construct_mapping)
return yaml.load(stream, OrderedLoader)
# usage example:
ordered_load(stream, yaml.SafeLoader)
Щодо серіалізації, я не знаю очевидного узагальнення, але принаймні це не повинно мати жодних побічних ефектів:
def ordered_dump(data, stream=None, Dumper=yaml.Dumper, **kwds):
class OrderedDumper(Dumper):
pass
def _dict_representer(dumper, data):
return dumper.represent_mapping(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
data.items())
OrderedDumper.add_representer(OrderedDict, _dict_representer)
return yaml.dump(data, stream, OrderedDumper, **kwds)
# usage:
ordered_dump(data, Dumper=yaml.SafeDumper)
Модуль yaml дозволяє вказати власні "представники" для перетворення об'єктів Python в текст та "конструктори" для зміни процесу.
_mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
def dict_representer(dumper, data):
return dumper.represent_dict(data.iteritems())
def dict_constructor(loader, node):
return collections.OrderedDict(loader.construct_pairs(node))
yaml.add_representer(collections.OrderedDict, dict_representer)
yaml.add_constructor(_mapping_tag, dict_constructor)
from six import iteritemsа потім змінити його iteritems(data)так, щоб він працював однаково добре в Python 2 & 3.
represent_dictі DEFAULT_MAPPING_TAG). Це тому, що документація неповна, або ці функції не підтримуються і можуть бути змінені без попереднього повідомлення?
dict_constructorтого, що вам потрібно буде зателефонувати, loader.flatten_mapping(node)або ви не зможете завантажити <<: *...(синтаксис злиття)
oyamlє заміною для випаду PyYAML, яка зберігає впорядкованість. Підтримуються і Python 2, і Python 3. Просто pip install oyamlта імпортуйте, як показано нижче:
import oyaml as yaml
Ви більше не будете дратуватися накрученими картами під час скидання / завантаження.
Примітка: Я автор oyaml.
ruamel.yaml - це крапля заміни PyYAML (відмова від відповідальності: я автор цього пакету). Збереження порядку відображень було однією з речей, доданих у першій версії (0.1) ще у 2015 році. Він не лише зберігає порядок у ваших словниках, але також зберігатиме коментарі, прив’язує назви, теги та підтримує YAML 1.2 специфікація (випущена 2009 р.)
У специфікації сказано, що замовлення не гарантується, але, звичайно, є замовлення у файлі YAML, і відповідний аналізатор може просто дотримуватися цього і прозоро генерувати об'єкт, який зберігає впорядкування. Вам просто потрібно вибрати правильний аналізатор, навантажувач та самоскид¹:
import sys
from ruamel.yaml import YAML
yaml_str = """\
3: abc
conf:
10: def
3: gij # h is missing
more:
- what
- else
"""
yaml = YAML()
data = yaml.load(yaml_str)
data['conf'][10] = 'klm'
data['conf'][3] = 'jig'
yaml.dump(data, sys.stdout)
дасть вам:
3: abc
conf:
10: klm
3: jig # h is missing
more:
- what
- else
dataмає тип, CommentedMapякий функціонує як дикт, але має додаткову інформацію, яка зберігається навколо, поки не буде скинуто (включаючи збережений коментар!)
CommentedMapбезпосередньо, але це не працює, і OrderedDictставить !!omapвсюди, що не дуже зручно для користувачів.
CommentedMapс, safe=Trueу YAMLякому не вийшло (використовуючи safe=Falseроботи). У мене також виникло питання про CommentedMapте, що його не можна змінити, але я не можу його відтворити зараз ... Я відкрию нове запитання, якщо я знову зустрінусь із цією проблемою.
yaml = YAML(), ви отримуєте аналізатор / самоскид в зворотному напрямку, який є похідним від безпечного аналізатора / самоскиду, який знає про CommentedMap / Seq тощо.
Примітка : існує бібліотека, заснована на такій відповіді, яка реалізує також CLoader та CDumpers: Phynix / yamlloader
Я дуже сумніваюся, що це найкращий спосіб зробити це, але це я так придумав, і він спрацьовує. Також доступний як суть .
import yaml
import yaml.constructor
try:
# included in standard lib from Python 2.7
from collections import OrderedDict
except ImportError:
# try importing the backported drop-in replacement
# it's available on PyPI
from ordereddict import OrderedDict
class OrderedDictYAMLLoader(yaml.Loader):
"""
A YAML loader that loads mappings into ordered dictionaries.
"""
def __init__(self, *args, **kwargs):
yaml.Loader.__init__(self, *args, **kwargs)
self.add_constructor(u'tag:yaml.org,2002:map', type(self).construct_yaml_map)
self.add_constructor(u'tag:yaml.org,2002:omap', type(self).construct_yaml_map)
def construct_yaml_map(self, node):
data = OrderedDict()
yield data
value = self.construct_mapping(node)
data.update(value)
def construct_mapping(self, node, deep=False):
if isinstance(node, yaml.MappingNode):
self.flatten_mapping(node)
else:
raise yaml.constructor.ConstructorError(None, None,
'expected a mapping node, but found %s' % node.id, node.start_mark)
mapping = OrderedDict()
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
try:
hash(key)
except TypeError, exc:
raise yaml.constructor.ConstructorError('while constructing a mapping',
node.start_mark, 'found unacceptable key (%s)' % exc, key_node.start_mark)
value = self.construct_object(value_node, deep=deep)
mapping[key] = value
return mapping
key_node.start_markатрибут у своєму повідомленні про помилку, я не бачу очевидного способу спростити ваш центральний цикл побудови. Якщо ви спробуєте скористатися тим фактом, що OrderedDictконструктор прийме ітерабельний ключ ключів, пар значень, ви втратите доступ до цієї деталі під час генерації повідомлення про помилку.
add_constructorу своєму __init__методі.
Оновлення : бібліотека була застаріла на користь завантажувача yamlloader (що базується на yamlordeddictloader)
Щойно я знайшов бібліотеку Python ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ), яка була створена на основі відповідей на це питання і досить проста у використанні:
import yaml
import yamlordereddictloader
datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)
yodlна github.
У моїй установці PyYaml для Python 2.7 я оновив __init__.py, constructor.py та loader.py. Тепер підтримується параметр object_pairs_hook для команд завантаження. Різниця змін, які я внесла, наведена нижче.
__init__.py
$ diff __init__.py Original
64c64
< def load(stream, Loader=Loader, **kwds):
---
> def load(stream, Loader=Loader):
69c69
< loader = Loader(stream, **kwds)
---
> loader = Loader(stream)
75c75
< def load_all(stream, Loader=Loader, **kwds):
---
> def load_all(stream, Loader=Loader):
80c80
< loader = Loader(stream, **kwds)
---
> loader = Loader(stream)
constructor.py
$ diff constructor.py Original
20,21c20
< def __init__(self, object_pairs_hook=dict):
< self.object_pairs_hook = object_pairs_hook
---
> def __init__(self):
27,29d25
< def create_object_hook(self):
< return self.object_pairs_hook()
<
54,55c50,51
< self.constructed_objects = self.create_object_hook()
< self.recursive_objects = self.create_object_hook()
---
> self.constructed_objects = {}
> self.recursive_objects = {}
129c125
< mapping = self.create_object_hook()
---
> mapping = {}
400c396
< data = self.create_object_hook()
---
> data = {}
595c591
< dictitems = self.create_object_hook()
---
> dictitems = {}
602c598
< dictitems = value.get('dictitems', self.create_object_hook())
---
> dictitems = value.get('dictitems', {})
loader.py
$ diff loader.py Original
13c13
< def __init__(self, stream, **constructKwds):
---
> def __init__(self, stream):
18c18
< BaseConstructor.__init__(self, **constructKwds)
---
> BaseConstructor.__init__(self)
23c23
< def __init__(self, stream, **constructKwds):
---
> def __init__(self, stream):
28c28
< SafeConstructor.__init__(self, **constructKwds)
---
> SafeConstructor.__init__(self)
33c33
< def __init__(self, stream, **constructKwds):
---
> def __init__(self, stream):
38c38
< Constructor.__init__(self, **constructKwds)
---
> Constructor.__init__(self)
ось просте рішення, яке також перевіряє наявність дублюваних клавіш верхнього рівня на вашій карті.
import yaml
import re
from collections import OrderedDict
def yaml_load_od(fname):
"load a yaml file as an OrderedDict"
# detects any duped keys (fail on this) and preserves order of top level keys
with open(fname, 'r') as f:
lines = open(fname, "r").read().splitlines()
top_keys = []
duped_keys = []
for line in lines:
m = re.search(r'^([A-Za-z0-9_]+) *:', line)
if m:
if m.group(1) in top_keys:
duped_keys.append(m.group(1))
else:
top_keys.append(m.group(1))
if duped_keys:
raise Exception('ERROR: duplicate keys: {}'.format(duped_keys))
# 2nd pass to set up the OrderedDict
with open(fname, 'r') as f:
d_tmp = yaml.load(f)
return OrderedDict([(key, d_tmp[key]) for key in top_keys])