Серіалізація – процес перетворення об'єктів складних типів даних (спеціально визначених класів, об'єктно-реляційних маперів, тощо) до власних типів, таким чином, щоб їх потім можна було легко перетворювати в JSON-списки.
У цій статті ми будемо використовувати компактну бібліотеку serpy, щоб показати як такі перетворення працюють. Потім ми інтегруємо цей код у веб-сервер на tornado, щоб продемонструвати, як ми можемо писати API, що повертає дані JSON.
Крок 1: Визначаємо тип даних
Припустимо, що ми працюємо над API, який повертає дані про людей: ID, ім'я, дату народження. Будемо вважати, що API має доступ до бази даних з інформацією про людей, а коли ми зробимо запит /person/42
, то отримаємо JSON-список даних про особу з ідентифікатором 42.
І тому перш ніж почати, швидко визначимо тип даних.
class Person(object):
def __init__(self, id, name, birth_date):
self.id = id
self.name = name
self.birth_date = birth_date
@property
def sidekick(self):
return sidekick_for(self)
Припустимо також, що тепер функція sidekick_for
повертає sidekick
(товариша) для певної особи (що є також об'єктом Person
). Пізніше ми додамо правильне визначення функції.
JSON приймає тільки власні типи даних: цілі, строки, булеві значення тощо. Зрозуміло, що Python функція json.dumps
з об'єктом Person
працювати не буде. Нам потрібно сформувати таку структуру даних, яка буде використовувати тільки власні типи даних, перед тим як ми зможемо передати їх до функції кодування JSON.
Метод №1 – Прямий
Ми можемо додати функцію to_json
до класу Person
, що повертатиме словник з даними особи. Це буде виглядати так:
def to_json(self):
return {
'id': self.id,
'name': self.name,
'birth_date': self.birth_date.isoformat(),
'sidekick': self.sidekick.to_json() if self.sidekick else None,
}
Ми повертаємо значення атрибутів Person
і, оскільки вони всі власних типів, то можна передати цей словник у функцію кодування і нарешті отримати список JSON. Виглядає добре!
Зверніть увагу на те, що нам потрібно викликати функцію isoformat
над self.birthdate
, щоб отримати рядки, оскільки об'єкт datetime
в Python не є власним типом. Також треба зауважити, що ми рекурсивно викликаємо функцію to_json
над self.sidekick
, щоб отримати його JSON відображення. Якщо цього не зробити, то змінна буде об'єктом Person
, який неможливо буде конвертувати прямо в JSON.
Поки це працює, але є декілька нюансів. По-перше, ми не можемо визначити типи полів. Якщо деякий код буде працювати з цим відображенням у JSON і ми передамо булеве значення для id
, то такий код буде працювати невірно. В ідеалі ми б хотіли обробляти такі випадки на етапі серіалізації. Додатково деякі випадки можуть потребувати, щоб повернені значення були різні за деяким контекстом. За приклад можна взяти веб застосунок, що створює чат-кімнати для спілкування двох і більше користувачів. В таких випадках кількість непрочитаних повідомлень в залежності від користувача буде різним в одній кімнаті.
Найпростішим рішенням таких проблем буде відокремлення визначення серіалізатора з оригінального визначення класу. І тут з'являється serpy. Якщо serpy у вас ще не встановлений, то наберіть у командному рядку pip install serpy==0.1.1
. Подивімося як його можна використовувати.
Метод №2 Визначення серіалізатора
from serpy import Serializer, IntField, StrField, MethodField
class PersonSerializer(Serializer):
id = IntField(required=True)
name = StrField(required=True)
birth_date = MethodField('serialize_birth_date')
sidekick = MethodField('serialize_sidekick')
def serialize_birth_date(self, person):
return person.birth_date.isoformat()
def serialize_sidekick(self, person):
if not person.sidekick:
return None
return PersonSerializer(person.sidekick).data
Що тільки що трапилось? Ми визначили PersonSerializer
, який є класом, що визначає як об'єкт Person
має бути серіалізований. Тепер ми знаємо типи полів, а це означає, що булеве значення для id
буде вважатися помилкою і не повертатиме значення, що також буде помилкою, оскільки id
зазначено як необхідне поле.
І це ще не все. Ми також досягли розподілу відповідальності, перемістивши функцію to_json
в окремий клас серіалізатора. У випадку, якщо доведеться провести якусь операцію з кодом у майбутньому, це виглядає фантастичним.
Складаємо все до купи
Напишемо маленький API-сервер в tornado (запустіть в командному рядку pip install tornado==4.5.1
), який об'єднає в собі увесь код з цієї статті. Збережіть наступний код у файлі server.py
у поточній директорії.
from datetime import datetime
from serpy import Serializer, IntField, StrField, MethodField
from tornado.escape import json_encode
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, Application
class Person(object):
def __init__(self, id, name, birth_date):
self.id = id
self.name = name
self.birth_date = birth_date
@property
def sidekick(self):
return sidekick_for(self)
class PersonSerializer(Serializer):
id = IntField(required=True)
name = StrField(required=True)
birth_date = MethodField('serialize_birth_date')
sidekick = MethodField('serialize_sidekick')
def serialize_birth_date(self, person):
return person.birth_date.isoformat()
def serialize_sidekick(self, person):
if not person.sidekick:
return None
return PersonSerializer(person.sidekick).data
batman = Person(1, 'Batman', datetime(year=1980, month=1, day=1))
robin = Person(2, 'Robin', datetime(year=1980, month=1, day=1))
def sidekick_for(person):
return robin if person == batman else None
class BatmanHandler(RequestHandler):
def get(self):
self.write(json_encode(PersonSerializer(batman).data))
if __name__ == '__main__':
Application([(r'/batman', BatmanHandler)]).listen(8888)
print('Listening on port 8888')
IOLoop.current().start()
Запустіть програму, виконавши python server.py
в командному рядку в поточній директорії. Python має запустити веб сервер з портом 8888. Відвідавши сторінку URL /batman, ви маєте отримати JSON відображення об'єкта Person
. Це працює!
Висновки!
В цій статті ми дослідили як серіалізувати об'єкти Python в JSON. Важливо також знати, що серіалізація не обмежена JSON. Існує багато інших форматів даних (наприклад XML), які можуть бути корисними. В будь-якому випадку основна ідея залишається такою ж. Цікавим завданням буде спробувати сдампити дані в різні формати та спробувати інші бібліотеки (наприклад, marshmallow). Щасливої серіалізації!
Ще немає коментарів