Я изучаю FastAPI, и он работает на моем рабочем столе Docker в Windows. Вот мой main.py, который успешно развернут в Docker:
#main.py
import fastapi
import json
from fastapi.responses import JSONResponse
app = fastapi.FastAPI()
@app.get('/api/get_weights1')
async def get_weights1():
weights = {'aa': 10, 'bb': 20}
return json.dumps(weights)
@app.get('/api/get_weights2')
async def get_weights2():
weights = {'aa': 10, 'bb': 20}
return JSONResponse(content=weights, status_code=200)
И у меня есть простой файл Python get_weights.py для запросов к этим двум API:
#get_weights.py
import requests
import json
resp = requests.get('http://127.0.0.1:8000/api/get_weights1')
print('ok', resp.status_code)
if resp.status_code == 200:
print(resp.json())
resp = requests.get('http://127.0.0.1:8000/api/get_weights2')
print('ok', resp.status_code)
if resp.status_code == 200:
print(resp.json())
Я получаю одинаковые ответы от двух API, вывод:
ok 200
{"aa": 10, "bb": 20}
ok 200
{'aa': 10, 'bb': 20}
Ответ кажется одинаковым, использую ли я json.dumps() или JSONResponse(). Я прочитал документацию FastAPI по JSONResponse, но у меня остались следующие вопросы:
Могу ли я узнать, есть ли разница между двумя методами?
Если есть разница, какой метод рекомендуется (и почему?)?
Спасибо, ответ от @Matija ясен!






В FastAPI вы можете создать ответ тремя разными способами (от самого лаконичного до самого гибкого):
return dict # Or model or ...
В документах, на которые вы ссылаетесь, мы видим, что FastAPI автоматически преобразует это dict в строку и завернет в JSONResponse. Этот способ наиболее лаконичен и охватывает большинство вариантов использования.
Однако иногда вам приходится возвращать собственные заголовки (например, REMOTE-USER=username) или другой код состояния (возможно, 201 — Создано или 202 — Принято). В этом случае вам нужно использовать JSONResponse.
return JSONResponse(content=dict) # Here we need to have dict.
Проблема в том, что теперь, если у нас нет простого словаря, мы должны использовать jsonable_encoder(some_model) # -> dict, чтобы получить его. Так что это немного более многословно. Доступные варианты смотрите в документации Starlette, так как FastAPI просто реэкспортирует ее.
Более сложный пример:
return JSONResponse(content=jsonable_encoder(some_model), status_code=201, headers = {"REMOTE-USER": username})
Наконец, вам не нужно возвращать json — вы также можете вернуть csv, html или любой другой тип файла. В этом случае мы должны использовать Response и указать media_type. Также используйте Документы Starlette.
return Response('Hello, world!', media_type='text/plain')
Обратите внимание, что в документации Fastapi указано:
Когда вы возвращаете ответ напрямую, его данные не проверяются, не преобразуются (сериализуются) и не документируются автоматически.
Итак, мы видим, в чем разница: первый метод имеет хорошую интеграцию с другими функциями FastAPI, поэтому его всегда следует отдавать предпочтение. Используйте второй вариант, только если вам нужно предоставить пользовательские заголовки или коды состояния. Наконец, используйте третий вариант, только если вы хотите вернуть что-то, что не является json.
Я экспериментировал с несколькими вариантами и нашел следующее...
(1) Оба метода не могут сериализовать объект даты и времени. Например, если веса:
weights = {'aa': 10, 'bb': 20, 'date': datetime.date.today()}
тогда оба метода будут иметь одинаковый возвращаемый статус и ошибку:
500 Внутренняя ошибка сервера
TypeError: объект типа date не сериализуем JSON
Чтобы преодолеть это использование
return json.dumps(weights, default=str)
и
from fastapi.encoders import jsonable_encoder
#blah
return JSONResponse(content=jsonable_encoder(weights), status_code=200)
(2) Я также экспериментировал с возвратом простого dict как есть, особенно если в нем есть объект datetime. Как уже упоминал @Matija, FastAPI автоматически преобразует это dict в строку и завершит его в ответ. Например:
@app.get('/api/get_weights1')
async def get_weights1():
weights = {'aa': 10, 'bb': 20, 'date': datetime.date.today()}
return weights #<--- this is dict
Выход:
ok 200
{"aa": 10, "bb": 20, "date": "2023-04-21"}
(3) Как уже упоминал @Matija, метод JSONResponse() позволяет настраивать возвращаемый ответ. Например, статус ответа можно настроить как 201 (вместо 200). А также различные типы возвращаемых объектов. Это, вероятно, преимущество использования этого метода перед методом json.dumps(). Например:
#main.py
@app.get('/api/get_weights2')
async def get_weights2():
weights = {'aa': 10, 'bb': 20}
return JSONResponse(content=weights, status_code=201) #<--201
#get_weights.py
resp = requests.get('http://127.0.0.1:8000/api/get_weights2')
print('ok', resp.status_code)
if resp.status_code == 201: #<---201
print(resp.json())
Тот же вывод, что и раньше:
ok 200
{'aa': 10, 'bb': 20}
Боюсь, вы ошибаетесь насчёт JSONResponse и json.dumps(), а также предположения о том, как работает FastAPI/Starlette под капотом. Если вы нашли время, чтобы взглянуть на ссылку, указанную в разделе комментариев выше, на ваши вопросы будут даны ответы (поскольку ваш вопрос, по сути, дублирует). Ответ @Matija также не очень точен, так как существует более трех способов вернуть Response (например, StreamingResponse, HTMLResponse и т. д.), а также можно использовать другие (более быстрые) кодировщики JSON, чем стандартная json библиотека. Все описано в связанном ответе выше
спасибо, Крис, я бы повнимательнее прочитал, как работает FastAPI/Starlette!
Пожалуйста, ознакомьтесь с приведенной выше ссылкой. Вы также можете найти этот ответ (см. Вариант 1) полезным.
новая ссылка (вариант 1) понятнее, я понял, спасибо!
Окей, сделано! Ваше здоровье!
Подводя итог: не используйте ни то, ни другое, просто верните объект, похожий на словарь, или с помощью
orm_mode=Trueв вашей модели ответа объект, который поддерживает поиск атрибутов (например, строку SQLAlchemy). Используйтеresponse_modelв декораторе маршрута или укажите тип возвращаемого значения для функции, которая определяет, как вы хотите сериализовать ответ.