В предыдущей части этой статьи мы разобрали основные аспекты работы с JSON в Python. Мы поняли, что это превосходный стандарт передачи данных. Сейчас мы продолжаем рассматривать эту тему.
Кодирование и декодирование Python
Что бывает, когда мы пробуем осуществить сериализацию класса Elf из программы Dungeons & Dragons, с которой мы взаимодействуем?
class Elf: def __init__(self, level, ability_scores=None): self.level = level self.ability_scores = { "str": 11, "dex": 12, "con": 10, "int": 16, "wis": 14, "cha": 13 } if ability_scores is None else ability_scores self.hp = 10 + self.ability_scores["con"]
Естественно, появилась ошибка. Ничего странного здесь нет. Программа показала, что невозможно выполнить сериализацию класса Elf.
elf = Elf(level=4) json.dumps(elf) TypeError: Object of type 'Elf' is not JSON serializable
Несмотря на то, что в функционал модуля json входит возможность обрабатывать целый спектр типов Python, он все же не умеет выполнять кодирование пользовательских типов данных по умолчанию. Это как пробовать положить кубик в круглое отверстие. В этом случае для того, чтобы его извлечь, необходимо иметь бензопилу и надзор специалиста.
Упрощение структур данных
Теперь попробуем разобраться в том, какие действия предпринимать в отношении более сложных структур данных. Так, вы можете попробовать выполнить ручное кодирование и декодирование JSON. Но есть решение поэлегантнее. С его помощью можно высвободить дополнительное время на разработку. Можно перейти к промежуточному шагу, а не двигаться прямо от к JSON.
Нам достаточно показать данные в рамках встроенных типов, которые изначально входят в структуру json. Фактически вы осуществляете перевод более комплексного объекта в более легкий формат. И уже его модуль json потом трансформирует в JSON.
Чтобы этот принцип был более понятен, приведем пример с двумя равенствами: A = B, B = C. Из этих двух равенств делаем вывод, что A = C.
Чтобы достичь данного результата, требуется комплексный объект. Вообще, возможно применение какого-угодно пользовательского класса. Пользователь самостоятельно решает, какой ему лучше подходит. Правда, Python содержит встроенный тип, который называется complex, с помощью которого представляются сложные числа. И для этого типа нет встроенной возможности быть сериализованным.
Итак, давайте для большего понимания приведем фрагмент кода:
z = 3 + 8j print(type(z)) # <class 'complex'> json.dumps(z) TypeError: Object of type 'complex' is not JSON serializable
Откуда приходят комплексные числа? В их состав входят два числа: реальное и представляемое. Они складываются вместе для того, чтобы и образовывать комплексные числа.
В процессе работы со сложными типами новички нередко задают следующий вопрос: «Какое самое небольшое количество данных требуется для воссоздания данного объекта?». Если речь идет о комплексных числах, необходимо владеть данными о реальном и представляемым числам, работа с которым осуществляется через объект complex:
z = 3 + 8j print(z.real) # 3.0 print(z.imag) # 8.0
Передачи тех же самых чисел в сложный конструктор хватит, чтобы удовлетворить оператор сравнения __eq__
z = 3 + 8j print( complex(3, 8) == z ) # True
Невероятно важно для кодирования и декодирования осуществлять разбитие пользовательских типов данных на составляющие.
Кодирование пользовательских типов
Чтобы конвертировать пользовательские объекты в JSON-формат, необходимо передать функцию кодирования параметру по умолчанию метода dump(). Модуль json обращается к этой функции для всех объектов, которые не могут кодироваться естественным образом.
Приведем пример небольшой функции декодирования, которая может использоваться в реальной разработке приложений:
def encode_complex(z): if isinstance(z, complex): return (z.real, z.imag) else: type_name = z.__class__.__name__ raise TypeError(f"Object of type '{type_name}' is not JSON serializable")
Учтите то, что может быть выдано исключение TypeError в случае, если вами не будет получен ожидаемый тип объекта. Так можно не допустить случайной сериализации. Одним словом, вы теперь имеете инструмент для самостоятельного кодирования комплексных объектов:
>>> json.dumps(9 + 5j, default=encode_complex) '[9.0, 5.0]' >>> json.dumps(elf, default=encode_complex) TypeError: Object of type 'Elf' is not JSON serializable
Почему в нашем примере осуществляется кодирование комплексного числа в виде кортежа? Вообще, вариантов множество. И этот также возможен. Правда, в будущем такая реализация может быть сопряжена с некоторыми недостатками.
Также можно генерировать дочерний класс JSONEncoder, после чего переопределить его метод default().
class ComplexEncoder(json.JSONEncoder): def default(self, z): if isinstance(z, complex): return (z.real, z.imag) else: super().default(self, z)
Вместо создания исключения TypeError, можно позволить классу base обработать его. Использовать его можно как напрямую в метод dump() с помощью параметра cls, либо же создать экземпляр encoder и в последующем вызвать метод encode():
>>> json.dumps(2 + 5j, cls=ComplexEncoder) '[2.0, 5.0]' >>> encoder = ComplexEncoder() >>> encoder.encode(3 + 6j) '[3.0, 6.0]'
Декодирование пользовательских типов
Тогда, когда реальные и представляемые части комплексных чисел полностью нужны, их все равно не будет достаточно для воссоздания объекта. Вот что происходит, если вы попытаетесь выполнить кодирование комплексного числа с дальнейшим декодированием получившегося результата:
>>> complex_json = json.dumps(4 + 17j, cls=ComplexEncoder) >>> json.loads(complex_json) [4.0, 17.0]
В результате выполнения такого кода будет получен список. Далее возможна передача значений в конструктор и повторное получение данного комплексного объекта.
Вспомните пример с телепортацией, который приводился в прошлой части. Чего нам в результате недостаточно? Метаданных, либо сведений о типе кодируемых данных.
Здесь может появиться вопрос о том, какое самое меньшее количество данных необходимо, и которого достаточно для воссоздания объекта? В целом, все пользовательские типы должны отображаться, как объекты стандарта JSON.
Давайте для разнообразия создадим файл JSON, который назовем, например, complex_data.json, и добавим такой объект, который показывает комплексное число:
{ "__complex__": true, "real": 42, "imag": 36 }
Увидели интересную часть? Ключ __complex__ – это метаданные, которые мы только что упоминали. Нет никакого значения, какое ассоциируемое значение имеется. Чтобы эта хитрость сработала, необходимо лишь подтвердить наличие ключа:
def decode_complex(dct): if "__complex__" in dct: return complex(dct["real"], dct["imag"]) return dct
Если __complex__ не расположен в словаре, то возможен просто возврат объекта. Можно просто разрешить декодеру по умолчанию разобраться с этим.
Каждый раз, когда метод load() осуществляет попытку парсинга объекта, у вас появляется возможность выступить в качестве посредника, перед тем, как декодер пройдет свой путь с данными. Это делается путем парсинга функции декодирования с параметром object_hook.
Теперь попробуем сделать то же самое, что и раньше:
with open("complex_data.json") as complex_data: data = complex_data.read() z = json.loads(data, object_hook=decode_complex) print(type(z)) # <class 'complex'>
Хотя объект object_hook может показаться альтернативой параметра по умолчанию dump(), но аналогия лишь в этом. Это не просто работает с одним объектом. Попробуйте внести данный список комплексных чисел в complex_data.json и запустить скрипт повторно:
[ { "__complex__":true, "real":42, "imag":36 }, { "__complex__":true, "real":64, "imag":11 } ]
Если все работает хорошо, то получите список комплексных объектов:
with open("complex_data.json") as complex_data: data = complex_data.read() numbers = json.loads(data, object_hook=decode_complex) print(numbers) # [(42+36j), (64+11j)]
Также можно попытаться создать подкласс JSONDecoder и переопределить object_hook. Правда, рекомендуется все же придерживаться более простых решений, если есть такая возможность.