[Python] Строгая десериализация YAML в Python c библиотекой marshmallow (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Исходная задача
- Необходимо прочитать нетривиальный конфиг из .yaml файла.
- Структура конфига описана с помощью дата-классов.
- Необходимо, чтобы при десериализации были выполнены проверки типов, и, если данные невалидны, было брошено исключение.
То есть, проще говоря, нужна функция вида:
def strict_load_yaml(yaml: str, loaded_type: Type[Any]):
"""
Here is some magic
"""
pass
И эта функция будет использоваться следующим образом:
@dataclass
class MyConfig:
"""
Here is object tree
"""
pass
try:
config = strict_load_yamp(open("config.yaml", "w").read(), MyConfig)
except Exception:
logging.exception("Config is invalid")
Классы конфигурации
Файл config.py выглядит следующим образом:
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
@dataclass
class BattleStationConfig:
@dataclass
class Processor:
core_count: int
manufacturer: str
processor: Processor
memory_gb: int
led_color: Optional[Color] = None
Вариант, который не работает
Исходная задача встречается часто, не так ли? Значит решение должно быть тривиальным. Просто импортируем стандартную yaml-библиотеку и задача решена?
Делаем импорт PyYaml и вызываем функцию load:
from pprint import pprint
from yaml import load, SafeLoader
yaml = """
processor:
core_count: 8
manufacturer: Intel
memory_gb: 8
led_color: red
"""
loaded = load(yaml, Loader=SafeLoader)
pprint(loaded)
и в результате получим:
{'led_color': 'red',
'memory_gb': 8,
'processor': {'core_count': 8, 'manufacturer': 'Intel'}}
Yaml прекрасно загрузился, но в виде словаря. Это не проблема, можно передать словарь как **args в конструктор:
parsed_config = BattleStationConfig(**loaded)
pprint(parsed_config)
и результатом будет:
BattleStationConfig(processor={'core_count': 8, 'manufacturer': 'Intel'}, memory_gb=8, led_color='red')
Вау! Легко! Но… Подождите-ка. Поле processor это словарь? Черт побери.
Python не выполняет проверку типов в конструкторе и не преобразует аргументы к классу Processor. Значит настало время идти на stackowerflow.
Решение, которое требует yaml-теги и почти работает
Я прочитал вопросы и ответы на stackowerflow и документацию к PyYaml и выяснил, что yaml-документ может быть помечен тегами для определения типов. Классы в документе должны быть потомкамиYAMLObject, и файл config_with_tag.py будет выглядеть так:
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from yaml import YAMLObject, SafeLoader
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
@dataclass
class BattleStationConfig(YAMLObject):
yaml_tag = "!BattleStationConfig"
yaml_loader = SafeLoader
@dataclass
class Processor(YAMLObject):
yaml_tag = "!Processor"
yaml_loader = SafeLoader
core_count: int
manufacturer: str
processor: Processor
memory_gb: int
led_color: Optional[Color] = None
а код для загрузки так:
from pprint import pprint
from yaml import load, SafeLoader
from config_with_tag import BattleStationConfig
yaml = """
--- !BattleStationConfig
processor: !Processor
core_count: 8
manufacturer: Intel
memory_gb: 8
led_color: red
"""
a = BattleStationConfig
loaded = load(yaml, Loader=SafeLoader)
pprint(loaded)
И что получится в результате десериализации?
BattleStationConfig(processor=BattleStationConfig.Processor(core_count=8, manufacturer='Intel'), memory_gb=8, led_color='red')
Неплохо. Но теперь yaml-документ наполовину состоит из тегов и потерял читаемость. К тому же, Color по-прежнему читается как строка. Может нужно просто добавить YAMLObject в список родительских классов? Так? Увы, нет. Код
class Color(Enum, YAMLObject):
RED = "red"
GREEN = "green"
BLUE = "blue"
приведет к ошибке:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Я не нашел решения этой проблемы за разумное время. К тому же я не хотел добавлять теги к yaml-документу, поэтому продолжил искать другие варианты решения исходной задачи.
Решение с библиотекой marshmallow
На stackowerflow я нашел рекомендацию использовать библиотеку marshmallow для парсинга словаря, полученного при десериализации JSON-объекта. Я решил, что это случай аналогичной исходной задаче, за исключением того, что в нашей задаче используется yaml вместо JSON. Попробуем использовать генератор class_schema, чтобы получить схему дата-класса:
from pprint import pprint
from yaml import load, SafeLoader
from marshmallow_dataclass import class_schema
from config import BattleStationConfig
yaml = """
processor:
core_count: 8
manufacturer: Intel
memory_gb: 8
led_color: red
"""
loaded = load(yaml, Loader=SafeLoader)
pprint(loaded)
BattleStationConfigSchema = class_schema(BattleStationConfig)
result = BattleStationConfigSchema().load(loaded)
pprint(result)
и, в результате, получим:
marshmallow.exceptions.ValidationError: {'led_color': ['Invalid enum member red']}
Значит, marshmallow хочет имя enum, а не его значение. Можно немного изменить исходный yaml-документ на:
processor:
core_count: 8
manufacturer: Intel
memory_gb: 8
led_color: RED
И, в результате, мы получим идеально десериализованный объект:
BattleStationConfig(processor=BattleStationConfig.Processor(core_count=8, manufacturer='Intel'), memory_gb=8, led_color=<Color.RED: 'red'>)
Но у меня все еще остается чувство, что можно использовать оригинальный yaml-документ. Я продолжил исследование документации marshmallow и нашел следующие строчки:
Setting by_value=True. This will cause both dumping and loading to use the value of the enum.
Оказывается, можно передать следующую конфигурацию в словарь metadata генератора датакласса field:
@dataclass
class BattleStationConfig:
led_color: Optional[Color] = field(default=None, metadata={"by_value": True})
И таким образом, мы получим ту самую "магическую" функцию, которая сможет распарсить исходный yaml-документ.
Магическая функция
Теперь мы знаем, как выглядит тело магической функции:
def strict_load_yaml(yaml: str, loaded_type: Type[Any]):
schema = class_schema(loaded_type)
return schema().load(load(yaml, Loader=SafeLoader))
Эта функция может потребовать дополнительной настройки для дата-классов, но решает исходную задачу и не требует наличия тегов в yaml.
Небольшая заметка о ForwardRef
Если определить дата-классы с ForwardRef (строка с именем класса) marshmallow будет озадачена и не сможет распарсить этот класс.
Например, такая конфигурация
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, ForwardRef
@dataclass
class BattleStationConfig:
processor: ForwardRef("Processor")
memory_gb: int
led_color: Optional["Color"] = field(default=None, metadata={"by_value": True})
@dataclass
class Processor:
core_count: int
manufacturer: str
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
приведет к ошибке
marshmallow.exceptions.RegistryError: Class with name 'Processor' was not found. You may need to import the class.
И если переместить класс Processor выше, marshmallow потеряет класс Color с аналогичной ошибкой. Так что, по возможности, не используйте ForwardRef для ваших классов, если хотите парсить их с помощью marshmallow.
Код
Весь код доступен в репозитории на GitHub.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Kirill Sulim
===========Похожие новости:
- [Python, Сетевые технологии] Implementing Offline traceroute Tool Using Python (перевод)
- [Программирование, Машинное обучение] Распознавание речи с помощью инструментов машинного обучения
- [Программирование микроконтроллеров, Компьютерное железо, DIY или Сделай сам] Raspberry Pi Pico на МК RP2040: начало и первые шаги. Что есть поесть за $4
- [Python, Программирование, Алгоритмы, Машинное обучение, Искусственный интеллект] Нейронная Сеть CLIP от OpenAI: Классификатор, который не нужно обучать. Да здравствует Обучение без Обучения
- [Python, Разработка мобильных приложений, Интерфейсы, Промышленное программирование] Открываем четыре Школы: разработки интерфейсов, бэкенда, мобильной разработки и дизайна
- [Python, Big Data, Data Engineering] Coins Classification using Neural Networks
- В пакетном менеджере PIP прекращена поддержка Python 2
- [Python, Разработка на Raspberry Pi, DIY или Сделай сам] Я сделаю свою «умную» колонку… «with blackjack and hookers!»
- [Python, Визуализация данных, Машинное обучение] Как изменился Datalore за 2020 год: мощная онлайн-среда для Jupyter-ноутбуков
- [Python, API, GitHub, Учебный процесс в IT] Как найти email пользователя GitHub и написать Telegram бота для решения этой задачи?
Теги для поиска: #_python, #_python3, #_dataclass, #_yaml, #_marshmallow, #_python
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 20:32
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Исходная задача
То есть, проще говоря, нужна функция вида: def strict_load_yaml(yaml: str, loaded_type: Type[Any]):
""" Here is some magic """ pass И эта функция будет использоваться следующим образом: @dataclass
class MyConfig: """ Here is object tree """ pass try: config = strict_load_yamp(open("config.yaml", "w").read(), MyConfig) except Exception: logging.exception("Config is invalid") Классы конфигурации Файл config.py выглядит следующим образом: from dataclasses import dataclass
from enum import Enum from typing import Optional class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue" @dataclass class BattleStationConfig: @dataclass class Processor: core_count: int manufacturer: str processor: Processor memory_gb: int led_color: Optional[Color] = None Вариант, который не работает Исходная задача встречается часто, не так ли? Значит решение должно быть тривиальным. Просто импортируем стандартную yaml-библиотеку и задача решена? Делаем импорт PyYaml и вызываем функцию load: from pprint import pprint
from yaml import load, SafeLoader yaml = """ processor: core_count: 8 manufacturer: Intel memory_gb: 8 led_color: red """ loaded = load(yaml, Loader=SafeLoader) pprint(loaded) и в результате получим: {'led_color': 'red',
'memory_gb': 8, 'processor': {'core_count': 8, 'manufacturer': 'Intel'}} Yaml прекрасно загрузился, но в виде словаря. Это не проблема, можно передать словарь как **args в конструктор: parsed_config = BattleStationConfig(**loaded)
pprint(parsed_config) и результатом будет: BattleStationConfig(processor={'core_count': 8, 'manufacturer': 'Intel'}, memory_gb=8, led_color='red')
Вау! Легко! Но… Подождите-ка. Поле processor это словарь? Черт побери. Python не выполняет проверку типов в конструкторе и не преобразует аргументы к классу Processor. Значит настало время идти на stackowerflow. Решение, которое требует yaml-теги и почти работает Я прочитал вопросы и ответы на stackowerflow и документацию к PyYaml и выяснил, что yaml-документ может быть помечен тегами для определения типов. Классы в документе должны быть потомкамиYAMLObject, и файл config_with_tag.py будет выглядеть так: from dataclasses import dataclass
from enum import Enum from typing import Optional from yaml import YAMLObject, SafeLoader class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue" @dataclass class BattleStationConfig(YAMLObject): yaml_tag = "!BattleStationConfig" yaml_loader = SafeLoader @dataclass class Processor(YAMLObject): yaml_tag = "!Processor" yaml_loader = SafeLoader core_count: int manufacturer: str processor: Processor memory_gb: int led_color: Optional[Color] = None а код для загрузки так: from pprint import pprint
from yaml import load, SafeLoader from config_with_tag import BattleStationConfig yaml = """ --- !BattleStationConfig processor: !Processor core_count: 8 manufacturer: Intel memory_gb: 8 led_color: red """ a = BattleStationConfig loaded = load(yaml, Loader=SafeLoader) pprint(loaded) И что получится в результате десериализации? BattleStationConfig(processor=BattleStationConfig.Processor(core_count=8, manufacturer='Intel'), memory_gb=8, led_color='red')
Неплохо. Но теперь yaml-документ наполовину состоит из тегов и потерял читаемость. К тому же, Color по-прежнему читается как строка. Может нужно просто добавить YAMLObject в список родительских классов? Так? Увы, нет. Код class Color(Enum, YAMLObject):
RED = "red" GREEN = "green" BLUE = "blue" приведет к ошибке: TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Я не нашел решения этой проблемы за разумное время. К тому же я не хотел добавлять теги к yaml-документу, поэтому продолжил искать другие варианты решения исходной задачи. Решение с библиотекой marshmallow На stackowerflow я нашел рекомендацию использовать библиотеку marshmallow для парсинга словаря, полученного при десериализации JSON-объекта. Я решил, что это случай аналогичной исходной задаче, за исключением того, что в нашей задаче используется yaml вместо JSON. Попробуем использовать генератор class_schema, чтобы получить схему дата-класса: from pprint import pprint
from yaml import load, SafeLoader from marshmallow_dataclass import class_schema from config import BattleStationConfig yaml = """ processor: core_count: 8 manufacturer: Intel memory_gb: 8 led_color: red """ loaded = load(yaml, Loader=SafeLoader) pprint(loaded) BattleStationConfigSchema = class_schema(BattleStationConfig) result = BattleStationConfigSchema().load(loaded) pprint(result) и, в результате, получим: marshmallow.exceptions.ValidationError: {'led_color': ['Invalid enum member red']}
Значит, marshmallow хочет имя enum, а не его значение. Можно немного изменить исходный yaml-документ на: processor:
core_count: 8 manufacturer: Intel memory_gb: 8 led_color: RED И, в результате, мы получим идеально десериализованный объект: BattleStationConfig(processor=BattleStationConfig.Processor(core_count=8, manufacturer='Intel'), memory_gb=8, led_color=<Color.RED: 'red'>)
Но у меня все еще остается чувство, что можно использовать оригинальный yaml-документ. Я продолжил исследование документации marshmallow и нашел следующие строчки: Setting by_value=True. This will cause both dumping and loading to use the value of the enum.
@dataclass
class BattleStationConfig: led_color: Optional[Color] = field(default=None, metadata={"by_value": True}) И таким образом, мы получим ту самую "магическую" функцию, которая сможет распарсить исходный yaml-документ. Магическая функция Теперь мы знаем, как выглядит тело магической функции: def strict_load_yaml(yaml: str, loaded_type: Type[Any]):
schema = class_schema(loaded_type) return schema().load(load(yaml, Loader=SafeLoader)) Эта функция может потребовать дополнительной настройки для дата-классов, но решает исходную задачу и не требует наличия тегов в yaml. Небольшая заметка о ForwardRef Если определить дата-классы с ForwardRef (строка с именем класса) marshmallow будет озадачена и не сможет распарсить этот класс. Например, такая конфигурация from dataclasses import dataclass, field
from enum import Enum from typing import Optional, ForwardRef @dataclass class BattleStationConfig: processor: ForwardRef("Processor") memory_gb: int led_color: Optional["Color"] = field(default=None, metadata={"by_value": True}) @dataclass class Processor: core_count: int manufacturer: str class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue" приведет к ошибке marshmallow.exceptions.RegistryError: Class with name 'Processor' was not found. You may need to import the class.
И если переместить класс Processor выше, marshmallow потеряет класс Color с аналогичной ошибкой. Так что, по возможности, не используйте ForwardRef для ваших классов, если хотите парсить их с помощью marshmallow. Код Весь код доступен в репозитории на GitHub. =========== Источник: habr.com =========== =========== Автор оригинала: Kirill Sulim ===========Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 20:32
Часовой пояс: UTC + 5