JSON в Python (Часть 2)

В предыдущей части этой статьи мы разобрали основные аспекты работы с 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. Правда, рекомендуется все же придерживаться более простых решений, если есть такая возможность.

ОфисГуру
Adblock
detector