Я хотів би отримати завантажувач 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])