В этой главе рассказывается о том, как использовать FastAPI для хранения и получения данных. Здесь расширяются простые примеры SQLite, приведенные в главе 10:
• другие базы данных с открытым исходным кодом (реляционные и нереляционные);
• использование SQLAlchemy на более высоком уровне;
• улучшенная проверка ошибок.
Термин «база данных», к сожалению, используется для обозначения трех вещей:
• типа сервера, например PostgreSQL, SQLite или MySQL;
• работающего экземпляра этого сервера;
• коллекции таблиц на этом сервере.
Чтобы избежать путаницы, называя экземпляр последнего из перечисленных пунктов базой данных PostgreSQL, я буду использовать другие термины, чтобы указать, какой из них имею в виду.
Обычный бэкенд для веб-сайта — это база данных. Веб-сайты и базы данных — это как арахисовое масло и желе, и хотя вы можете хранить свои данные и другими способами (или сочетать арахисовое масло с огурцами), в этой книге мы будем использовать базы данных.
Базы данных решают многие проблемы, которые в противном случае вам пришлось бы решать самостоятельно с помощью кода, например такие:
• множественный доступ;
• индексирование;
• согласованность данных.
В целом выбор баз данных выглядит следующим образом:
• реляционные базы данных с языком запросов SQL;
• нереляционные базы данных с различными языками запросов.
В Python есть стандартное определение реляционного API под названием DB-API (https://oreil.ly/StbE4). Оно поддерживается пакетами драйверов Python для всех основных баз данных. В табл. 14.1 перечислены некоторые известные реляционные базы данных и их основные пакеты драйверов для Python.
Таблица 14.1. Реляционные базы данных и драйверы Python
База данных | Драйвер Python |
С открытым исходным кодом | |
SQLite (https://www.sqlite.org) | sqlite3 (https://oreil.ly/TNNaA) |
PostgreSQL (https://www.postgresql.org) | psycopg2 (https://oreil.ly/nLn5x) и asyncpg (https://oreil.ly/90pvK) |
MySQL (https://www.mysql.com) | MySQLdb (https://oreil.ly/yn1fn) и PyMySQL (https://oreil.ly/Cmup-) |
Коммерческие | |
Oracle (https://www.oracle.com) | python-oracledb (https://oreil.ly/gynvX) |
SQL Server (https://www.microsoft.com/en-us/sql-server) | pyodbc (https://oreil.ly/_UEYq) и pymssql (https://oreil.ly/FkKUn) |
IBM Db2 (https://www.ibm.com/products/db2) | ibm_db (https://oreil.ly/3uwpD) |
Основные пакеты Python для работы с реляционными базами данных и SQL:
• SQLAlchemy (https://www.sqlalchemy.org) — полнофункциональная библиотека, которую можно использовать на разных уровнях;
• SQLModel (https://sqlmodel.tiangolo.com) — комбинация SQLAlchemy и Pydantic от автора FastAPI;
• Records (https://github.com/kennethreitz/records) — от автора пакета Requests — простой API для запросов.
Самым популярным SQL-пакетом для Python стал SQLAlchemy. Хотя во многих объяснениях SQLAlchemy обсуждаются только возможности ORM этой библиотеки, она содержит несколько слоев, и я буду рассматривать их снизу вверх.
Основа SQLAlchemy, называемая Core, включает в себя следующее:
• объект Engine, реализующий стандарт DB-API;
• URL-адреса, выражающие тип и драйвер SQL-сервера, а также конкретную коллекцию баз данных на этом сервере;
• пулы соединений «клиент — сервер»;
• транзакции (COMMIT и ROLLBACK);
• различия в диалектах SQL для различных типов баз данных;
• прямые запросы SQL (текстовые строки);
• запросы на языке выражений SQLAlchemy.
Некоторые из этих возможностей, например работа с диалектами, делают SQLAlchemy оптимальным пакетом для работы с различными типами серверов. С его помощью можно выполнять обычные SQL-запросы DB-API или использовать язык выражений SQLAlchemy.
До этого момента я работал с базовым драйвером DB-API SQLite и буду продолжать делать это. Но для больших сайтов или при необходимости воспользоваться специальными возможностями сервера стоит взять SQLAlchemy (применяя базовый DB-API, SQLAlchemy Expression Language или полноценный ORM).
Язык выражений SQLAlchemy (SQLAlchemy Expression Language) — это не ORM, а другой способ выражения запросов к реляционным таблицам. Он отображает базовые структуры хранения данных на классы Python, такие как Table и Column, и операции с методами Python, такими как select() и insert(). Эти функции преобразуются в обычные строки SQL, и вы можете обратиться к ним, чтобы посмотреть, что произошло. Язык не зависит от типов SQL-серверов. Если вам трудно дается SQL, возможно, стоит попробовать этот вариант.
Сравним несколько примеров. В примере 14.1 показана версия исключительно на языке SQL.
Пример 14.1. Прямой код SQL для функции get_one() в файле data/explorer.py
def get_one(name: str) -> Explorer:
qry = "select * from explorer where name=:name"
params = {"name": name}
curs.execute(qry, params)
return row_to_model(curs.fetchone())
В примере 14.2 показан частичный эквивалент SQLAlchemy Expression Language для настройки базы данных, создания таблицы и выполнения вставки.
Пример 14.2. SQLAlchemy Expression Language для функции get_one()
from sqlalchemy import Metadata, Table, Column, Text
from sqlalchemy import connect, insert
conn = connect("sqlite:///cryptid.db")
meta = Metadata()
explorer_table = Table(
"explorer",
meta,
Column("name", Text, primary_key=True),
Column("country", Text),
Column("description", Text),
)
insert(explorer_table).values(
name="Beau Buffette",
country="US",
description="...")
Для получения большего количества примеров можно воспользоваться альтернативной документацией (https://oreil.ly/ZGCHv) — она читается немного легче, чем официальные страницы.
ORM выражает запросы в терминах моделей данных домена, а не реляционных таблиц и логики SQL, лежащих в основе механизма базы данных. В официальной документации (https://oreil.ly/x4DCi) приведена подробная информация. ORM гораздо сложнее, чем язык выражений SQL. Разработчики, предпочитающие полностью объектно-ориентированные модели, обычно выбирают ORM.
Во многих книгах и статьях о FastAPI, начиная раздел, посвященный базам данных, авторы сразу же переходят к ORM SQLAlchemy. Я понимаю, что это привлекательно, но также знаю, что это требует изучения еще одной абстракции. SQLAlchemy — отличный пакет, но если его абстракции не всегда работают, то у вас возникают две проблемы. Самым простым решением может быть использование SQL и переход к языку выражений или ORM, если SQL становится слишком сложным.
Автор FastAPI объединил аспекты FastAPI, Pydantic и SQLAlchemy, чтобы создать библиотеку SQLModel (https://sqlmodel.tiangolo.com). Он переносит некоторые методы разработки из веб-мира в реляционные базы данных. SQLModel сочетает в себе ORM от SQLAlchemy с определением и проверкой данных от Pydantic.
Пакет SQLite был представлен в главе 10, я использовал его в примерах уровня данных. Это общественное достояние — более открытого исходного кода и не придумаешь. SQLite применяется в каждом браузере и в каждом смартфоне, что делает его одним из самых распространенных программных пакетов в мире. При выборе реляционной базы данных этот пакет часто упускают из виду, но вполне возможно, что несколько «серверов» SQLite смогут поддерживать некоторые крупные сервисы не хуже, чем такой мощный сервер, как PostgreSQL.
На заре развития реляционных баз данных пионером была система System R от IBM, а за новый рынок боролись ее ответвления — в основном Ingres с открытым исходным кодом и коммерческий продукт Oracle. В Ingres был применен язык запросов QUEL, а в System R — SQL. Хотя некоторые считали, что QUEL лучше, чем SQL, принятие Oracle SQL в качестве стандарта, а также влияние IBM помогли Oracle и SQL добиться успеха.
Спустя годы Майкл Стоунбрейкер вернулся, чтобы осуществить переход от Ingres к PostgreSQL (https://www.postgresql.org). В настоящее время разработчики систем с открытым исходным кодом чаще всего выбирают PostgreSQL, хотя система MySQL была популярна несколько лет назад и до сих пор не потеряла своей актуальности.
Несмотря на многолетний успех SQL, у него есть некоторые недостатки, делающие запросы неудобными. В отличие от математической теории, на которой основан SQL (реляционное исчисление Э.Ф. Кодда), сама конструкция языка SQL не является композиционной. В основном это означает, что сложно вложить запросы в большие запросы, что порождает более сложный и многословный код.
Поэтому просто для развлечения я создам здесь новую реляционную базу данных. EdgeDB (https://www.edgedb.com) была написана (на Python!) автором библиотеки asyncio для языка Python. Она описывается как Post-SQL или граф-реляционная. В ядре БД используется PostgreSQL для обработки сложных системных задач. Компания Edge привнесла в эту сферу свой продукт EdgeQL (https://oreil.ly/sdK4J) — новый язык запросов, стремясь избежать острых граней SQL. На самом деле он переводится на SQL для выполнения PostgreSQL. В статье Ивана Данилюка My Experience with EdgeDB (https://oreil.ly/ciNfg) приведено удобное сравнение EdgeQL и SQL. Приятная для чтения иллюстрированная официальная документация (https://oreil.ly/ce6y3) проводит параллели с книгой «Дракула».
Может ли EdgeQL распространиться за пределы EdgeDB и стать альтернативой SQL? Время покажет.
Крупные игроки в мире NoSQL или NewSQL с открытым исходным кодом перечислены в табл. 14.2.
Таблица 14.2. Базы данных NoSQL и драйверы Python
База данных | Драйвер Python |
Redis (https://redis.io) | redis-py (https://github.com/redis/redis-py) |
MongoDB (https://www.mongodb.com) | PyMongo (https://pymongo.readthedocs.io), Motor (https://oreil.ly/Cmgtl) |
Apache Cassandra (https://cassandra.apache.org) | DataStax Driver for Apache Cassandra (https://github.com/datastax/python-driver) |
Elasticsearch (https://www.elastic.co/elasticsearch) | Python Elasticsearch Client (https://oreil.ly/e_bDI) |
Иногда NoSQL означает буквально «отсутствие SQL», а иногда «не только SQL». Реляционные базы данных накладывают структуру на данные. Часто она визуализируется в виде прямоугольных таблиц с полями — столбцами и строками данных, подобно электронным таблицам. Чтобы уменьшить избыточность и повысить производительность, реляционные базы данных нормализуются с помощью нормальных форм (правил для данных и структур), например допускают только одно значение в ячейке (на пересечении строки и столбца).
Базы данных NoSQL делают эти правила менее строгими, иногда позволяя варьировать типы столбцов/полей в отдельных строках данных. Часто схемы (дизайн баз данных) могут представлять собой не реляционные ячейки, а разрозненные структуры, которые можно выразить на JSON или Python.
Redis — это сервер структур данных, работающий исключительно в оперативной памяти, хотя он может сохранять данные на диск и восстанавливать их с диска. Он полностью соответствует собственным структурам данных Python и стал очень популярным.
MongoDB — это своего рода PostgreSQL для NoSQL-серверов. Коллекция — это эквивалент таблицы SQL, а документ — эквивалент строки таблицы SQL. Еще одно отличие — и главная причина, по которой база данных NoSQL является основной, — заключается в том, что вам не нужно определять, как выглядит документ. Другими словами, нет никакой фиксированной схемы. Документ — это как словарь Python, ключом в нем может быть любая строка.
Cassandra — это крупномасштабная база данных, ее можно распределить между сотнями узлов. Она написана на языке Java.
Альтернативная база данных называется ScyllaDB (https://www.scylladb.com). Она написана на C++, и утверждается, что она совместима с Cassandra, но имеет бо́льшую производительность.
Elasticsearch (https://www.elastic.co/elasticsearch) больше похожа на индекс базы данных, чем на саму базу данных. Она часто используется для полнотекстового поиска.
Как отмечалось ранее, реляционные базы данных традиционно нормализуются и ограничиваются различными уровнями правил, называемых нормальными формами. Одним из основных правил было то, что значение в каждой ячейке должно быть скаляром — никаких массивов или других структур.
Базы данных NoSQL (или документоориентированные) поддерживали JSON напрямую и обычно оказывались единственным выбором, если у вас были неравномерные или неровные структуры данных. Часто они были денормализованными — все данные, необходимые для документа, были включены в него. В SQL для создания полного документа часто требовалось выполнить объединение данных из разных таблиц.
Однако последние изменения в стандарте SQL позволили хранить данные JSON и в реляционных базах данных. Некоторые из таких БД позволяют хранить сложные (нескалярные) данные в ячейках таблиц и даже выполнять в них поиск и индексирование. Функции JSON поддерживаются различными способами для SQLite (https://oreil.ly/h_FNn), PostgreSQL (https://oreil.ly/awYrc), MySQL (https://oreil.ly/OA_sT), Oracle (https://oreil.ly/osOYk) и других систем.
SQL с JSON может быть лучшим из двух миров. Базы данных SQL существуют гораздо дольше и поддерживают действительно полезные функции, такие как внешние ключи и вторичные индексы. Кроме того, SQL довольно хорошо стандартизирован до определенного момента, а языки запросов NoSQL все разные.
Наконец, новые языки проектирования данных и запросов пытаются объединить преимущества SQL и NoSQL, как, например, EdgeQL, о котором я упоминал ранее. Поэтому, если вы не можете уместить свои данные в прямоугольную реляционную коробку, обратите внимание на базу данных NoSQL, реляционную базу данных с поддержкой JSON или базу данных Post-SQL.
Эта книга в основном посвящена FastAPI, но веб-сайты слишком часто связаны с базами данных.
Примеры данных в этой книге были крошечными. Чтобы действительно протестировать базу данных на стрессоустойчивость, было бы неплохо использовать миллионы элементов. Вместо того чтобы их придумывать и добавлять вручную, проще воспользоваться пакетом Python, например Faker (https://faker.readthedocs.io). Он может быстро генерировать различные типы данных — имена, места или определяемые вами специальные типы.
В примере 14.3 функция Faker выкачивает имена и страны, а затем они загружаются с помощью функции load() в SQLite.
Пример 14.3. Загрузка фиктивных исследователей в файл test_load.py
from faker import Faker
from time import perf_counter
def load():
from error import Duplicate
from data.explorer import create
from model.explorer import Explorer
f = Faker()
NUM = 100_000
t1 = perf_counter()
for row in range(NUM):
try:
create(Explorer(name=f.name(),
country=f.country(),
description=f.description))
except Duplicate:
pass
t2 = perf_counter()
print(NUM, "rows")
print("write time:", t2-t1)
def read_db():
from data.explorer import get_all
t1 = perf_counter()
_ = get_all()
t2 = perf_counter()
print("db read time:", t2-t1)
def read_api():
from fastapi.testclient import TestClient
from main import app
t1 = perf_counter()
client = TestClient(app)
_ = client.get("/explorer/")
t2 = perf_counter()
print("api read time:", t2-t1)
load()
read_db()
read_db()
read_api()
Код будет улавливать исключение Duplicate в функции load(), но его стоит игнорировать, потому что Faker генерирует имена из ограниченного списка и, скорее всего, время от времени повторяет некоторые из них. Таким образом, в результате может быть загружено менее 100 000 исследователей.
Кроме того, вы вызываете функцию read_db() дважды, чтобы исключить время запуска процесса, пока SQLite выполняет запрос. Тогда время выполнения read_api() должно получиться честным. Пример 14.4 запускает тестирование.
Пример 14.4. Проверка производительности запросов к базе данных
$ python test_load.py
100000 rows
write time: 14.868232927983627
db read time: 0.4025074450764805
db read time: 0.39750714192632586
api read time: 2.597553930943832
Время чтения API для всех исследователей было намного медленнее, чем время чтения уровня данных. Вероятно, часть этих расходов связана с преобразованием ответа в JSON с помощью FastAPI. Кроме того, время первоначальной записи в базу данных было не очень быстрым. Код записывает по одному исследователю за раз, потому что в API уровня данных есть единственная функция create(), но нет функции create_many(). В части считывания API может вернуть один (get_one()) или все (get_all()) результаты. Поэтому, если вы хотите выполнять массовую загрузку, возможно, стоит добавить новую функцию загрузки данных и новую конечную точку веб-приложения (с ограниченной авторизацией). Кроме того, если вы ожидаете, что любая таблица в базе данных вырастет до 100 000 строк, возможно, не стоит позволять случайным пользователям получать их все за один вызов API. Не помешала бы пагинация или возможность загрузки одного CSV-файла из таблицы.
Python стал самым популярным языком в области науки о данных в целом и машинного обучения (Machine Learning, ML) в частности. Там необходимо много работать с данными, и Python отлично справляется с этой задачей.
Иногда разработчики используют сторонние инструменты (https://oreil.ly/WFHo9), такие как pandas, для манипулирования слишком сложными в SQL-представлении данными.
PyTorch (https://pytorch.org) — один из самых популярных инструментов ML, поскольку в работе с данными он использует сильные стороны Python. Для повышения скорости базовые вычисления могут выполняться на C или C++, а для высших задач интеграции данных хорошо подходят Python или Go. Язык Mojo (https://www.modular.com/mojo) — супернабор Python — сможет справиться с задачами и большой, и малой сложности, если все получится создать так, как задумано. Хотя это язык общего назначения, он специально предназначен для решения некоторых текущих проблем при разработке ИИ.
Новый инструмент Python под названием Chroma (https://www.trychroma.com) — это база данных, похожая на SQLite, но предназначенная для машинного обучения, в частности для больших языковых моделей (Large Language Models, LLM). Прочтите страницу Getting Started page (https://oreil.ly/W59nn), чтобы быстрее начать работу.
Хотя разработка ИИ сложна и идет быстрыми темпами, вы можете опробовать искусственный интеллект с помощью Python на собственной машине, не тратя мегасредства, затраченные на создание GPT-4 и ChatGPT. Создадим небольшой веб-интерфейс FastAPI для небольшой модели искусственного интеллекта.
Понятие «модель» имеет разные значения в области ИИ и Pydantic/FastAPI. В Pydantic модель — это класс Python, объединяющий связанные поля данных. Модели ИИ охватывают широкий спектр методов определения закономерностей в данных.
Платформа Hugging Face (https://huggingface.co) предоставляет бесплатные модели искусственного интеллекта, наборы данных и код на Python для их использования. Сначала установите PyTorch и код Hugging Face:
$ pip install torch torchvision
$ pip install transformers
В примере 14.5 показано приложение FastAPI, использующее модуль трансформеров с платформы Hugging Face для доступа к предварительно обученной модели машинного языка среднего размера с открытым исходным кодом. Эта программа попытается ответить на ваши вопросы. (Код был адаптирован из примера командной строки, приведенного на YouTube-канале CodeToTheMoon.)
Пример 14.5. Тесты высокого уровня для LLM (ai.py)
from fastapi import FastAPI
app = FastAPI()
from transformers import (AutoTokenizer,
AutoModelForSeq2SeqLM, GenerationConfig)
model_name = "google/flan-t5-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
config = GenerationConfig(max_new_tokens=200)
@app.get("/ai")
def prompt(line: str) -> str:
tokens = tokenizer(line, return_tensors="pt")
outputs = model.generate(**tokens,
generator_config=config)
result = tokenizer.batch_decode(outputs,
skip_special_tokens=True)
return result[0]
Запустите этот код с помощью выражения uvicorn ai:app (как всегда, сначала убедитесь, что у вас нет другого все еще запущенного веб-сервера на адресе localhost, порт 8000). Задавайте вопросы конечной точке /ai и получайте ответы, например, так (обратите внимание на двойной знак равенства == для параметра запроса HTTPie):
$ http -b localhost:8000/ai line=="What are you?"
"a sailor"
Это довольно маленькая модель, и, как вы можете видеть, она не особенно хорошо отвечает на вопросы. Я попробовал другие задания (line-аргументы) и получил не менее достойные ответы.
В: Лучше ли кошки, чем собаки?
О: Нет.
В: Что йети ест на завтрак?
О: Кальмара.
В: Кто спускается по дымоходу?
О: Визжащий поросенок.
В: В какой группе состоял Джон Клиз?
О: The Beatles.
В: У чего есть противные острые зубы?
О: Плюшевый мишка.
В разное время на эти вопросы можно получить разные ответы! Однажды та же самая конечная точка ответила, что йети ест на завтрак песок. В среде ИИ подобные ответы называются галлюцинациями. Вы можете получить более точные ответы, задействуя более крупную модель, например google/flan-75xl, но для загрузки данных модели на персональный компьютер и получения ответа потребуется больше времени. И конечно, такие модели, как ChatGPT, были обучены на всех данных, которые смогли найти (с использованием всех ЦП, ГП, ТП и любых других видов процессоров), и дадут отличные ответы.
В этой главе мы распространили возможности применения SQLite, описанные в главе 10, на другие базы данных SQL и даже NoSQL. Здесь также показано, как некоторые базы данных SQL могут выполнять трюки NoSQL с поддержкой JSON. Наконец, речь шла об использовании баз данных и специальных инструментов для работы с данными, которые становятся все более важными по мере того, как машинное обучение продолжает бурно развиваться.