什么是FastAPI
FastAPI,轻量级的Web框架。
安装
需要安装FastAPI
以及Uvicorn
。
FastAPI
FastAPI的安装命令:
pip工具在安装Pyhton库的时候,会自动检查并安装依赖库。这里核心的依赖库包括Pydantic
、Starlette
等。
Uvicorn
Uvicorn是一个ASGI(Asynchronous Server Gateway Interface,异步服务器网关接口)服务器框架,Uvicorn为FastAPI提供了快速异步运行环境功能。
安装命令:
入门案例
代码
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from fastapi import FastAPIimport uvicornapp = FastAPI() @app.get("/") async def root () : return {"msg" : "hello" } if __name__ == '__main__' : uvicorn.run(app=app)
运行结果:
1 2 3 4 5 INFO: Started server process [6616] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: 127.0.0.1:50538 - "GET / HTTP/1.1" 200 OK
然后我们访问http://127.0.0.1:8000
,会收到返回如下:
特别的,FastAPI已经集成了Swagger。通过http://127.0.0.1:8000/docs
,即可访问Swagger文档;通过http://127.0.0.1:8000/redoc
,可以访问另一种风格的Swagger文档。
如果我们把函数定义为def
而不是async def
,那么FastAPI会把它放到单独的线程池中,异步执行。也就是说无论是否使用async
,FastAPI都将异步工作,以达到"Fast"的运行速度。
问题解决
我们可能会遇到如下的报错:
1 ImportError: cannot import name Deque
这个可能因为我们的Python版本小于3.6.1
,需要3.6.1
及以上版本的Python。
框架构成
框架功能
当用户通过浏览器发起请求数据时,FastAPI服务器端对请求数据做以下操作:
由FastAPI中间件接收请求数据,对数据进行初步的处理。
将请求的URL中的路径与FastAPI定义的路由列表进行匹配。
FastAPI对请求数据进行数据验证和数据转换,得到符合要求的数据,并将数据传递给路径操作函数。
路径操作函数接收请求数据后,调用"业务处理"对数据进行加工、对资源进行读写,再将处理结果封装成响应数据。
将响应数据传递给FastAPI中间件,由FastAPI中间件对数据进行再次处理后,返回给浏览器。
在一些关键技术方面:
FastAPI以Starlette库作为Web服务器的底层,提供了异步技术接收客户端发起的请求数据。
通过高性能的数据模型框架Pydantic库对数据进行验证和转换,响应数据也通过Pydantic库转换成符合JSON模式的响应数据。
Pydantic
Pydantic,基于Python的类型注解的数据模型定义及验证框架。
基本用法
Pydantic中使用自定义类的方式定义数据模型类,而且数据模型类必须继承Pydantic的BaseModel。
数据模型的基本用法,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pydantic import BaseModelclass User (BaseModel) : id: int name = 'Kaka' u = User(id="123" ) print(u.id) print(u.name) print(u.__fields_set__) print(u.dict()) uu = User(id='aaa' )
运行结果:
1 2 3 4 123 Kaka {'id'} {'id': 123, 'name': 'Kaka'}
在上述代码中,从Pydantic模块导入了BaseModel类,定义了一个数据模型类User并继承了BaseModel类,数据模型类中有两个属性id和name。字段id类型为int,是必填项,字段name未指定数据类型,并且初始值为"Kaka"。
特别的,如果给id赋值为非数字,会报错,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pydantic import BaseModelclass User (BaseModel) : id: int name = 'Kaka' u2 = User(id='aaa' ) print(u2.id)
运行结果:
1 2 3 4 5 6 7 Traceback (most recent call last): File "C:\Dev\f\main.py", line 14, in <module> u2 = User(id='aaa') File "pydantic\main.py", line 341, in pydantic.main.BaseModel.__init__ pydantic.error_wrappers.ValidationError: 1 validation error for User id value is not a valid integer (type=type_error.integer)
解释说明:Pydantic在验证数据时,对无效的数据会进行错误提示。
属性和方法
Pydantic模型类为定义的数据模型类实例提供如下方法和属性:
dict()
,将数据模型的字段和值封装成字典。
json()
,将数据模型的字段和值封装成JSON格式字符串。
copy()
,生成数据模型实例的副本。
parse_obj()
,将Python字典数据解析为数据模型实例。
parse_raw()
,将字符串解析为数据模型实例。
parse_file()
,传入文件路径,并将路径所对应的文件解析为数据模型实例。
from_orm()
,将任何自定义类的实例转换成数据模型对象。
schema()
,将数据模型转换成JSON模式数据。
schema_json()
,返回schema()
生成的字符串。
construct()
类方法,创建数据模型实例时不进行验证。
__fields_set__
属性,创建数据模型实例的初始化字段列表。
__fields__
属性,罗列数据模型的全部字段的字典。
__config__
属性,显示数据模型的配置类。
嵌套模型
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from typing import Listfrom pydantic import BaseModelclass Blackboard (BaseModel) : size = 4000 color: str class Table (BaseModel) : position: str class ClassRoom (BaseModel) : blackboard: Blackboard tables: List[Table] m = ClassRoom( blackboard={'color' : 'green' }, tables=[{'position' : '第一排左1' }, {'position' : '第一排左2' }] ) print(m) print(m.dict())
运行结果:
1 2 blackboard=Blackboard(color='green', size=4000) tables=[Table(position='第一排左1'), Table(position='第一排左2')] {'blackboard': {'color': 'green', 'size': 4000}, 'tables': [{'position': '第一排左1'}, {'position': '第一排左2'}]}
在上述代码中,首先定义了两个数据模型类BlackBoard和Table,第三个数据模型ClassRoom的字段类型分别定义为前两个数据模型类。之后创建了数据模型ClassRoom的实例m,并打印出实例m的数据,数据中包含了数据模型全部的字段和值。
Starlette
Starlette是一个轻量级的、高性能异步服务网关接口框架(ASGI)。FastAPI框架中高性能异步操作的特性主要来源于Starlette。
Starlette的使用,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from starlette.applications import Starlettefrom starlette.responses import JSONResponsefrom starlette.routing import Routeimport uvicornasync def root (request) : return JSONResponse({'msg' : 'hello' }) app = Starlette(debug=True , routes=[Route('/' , root)]) if __name__ == '__main__' : uvicorn.run(app=app)
运行结果:
1 2 3 4 INFO: Started server process [2256] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
然后我们访问http://127.0.0.1:8000
,会收到返回如下:
在上述代码中,使用async def
关键字定义了一个异步函数root
,函数返回了一个响应对象(JSONResponse);然后创建了一个Starlette实例,实例中传入了路由列表,列表中第一个路由对象将路径/
指向了root
路径操作函数,路径操作函数将响应对象返回给浏览器。
在上文的FastAPI的例子中,我们没有定义路由列表,而是使用装饰器的方式设置"路由路径"与路径操作函数的绑定关系。但,其实,FastAPI在运行时,还是会在内部建立"路由表",将装饰器设置的"路由"保存到"路由表"中,然后启动服务。FastAPI只是给我们提供了一种更简洁的方式。
请求
路径参数
所谓的路径参数,即REST风格、RESTful。
关于REST风格、RESTful,可以参考《基于Java的后端开发入门:17.SpringMVC》 的"REST风格"部分,本文不赘述。
简单路径参数
在FastAPI中需要先用带装饰符{
和}
的@app.get("/items/{id_value}")
方法注册路由,用方法中的参数id_value
接收URL地址传递过来的路径参数值,同时,路径操作函数的参数名需要与上一行路由中的路径参数名一致。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from fastapi import FastAPIimport uvicornapp = FastAPI() @app.get("/items/{item_id}") async def read_item (item_id) : print(item_id) return {"item_id" : item_id} if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/items/123
,返回如下:
有类型的路径参数
有类型的路径参数,指的是在定义路径操作函数的参数时,需要指定数据类型,如整型、字符串等。
具体通过类型注解指定参数的数据类型。
如果我们在参数定义时,没有指定数据类型,则会默认为str类型。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from fastapi import FastAPIimport uvicornapp = FastAPI() @app.get("/items/{item_id}") async def read_item (item_id: int) : print(item_id) return {"item_id" : item_id} if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/items/123
,返回如下:
特别的,如果我们传入非数字的路径参数,例如abc
,访问http://127.0.0.1:8000/items/abc
,会收到报错,返回如下:
1 {"detail":[{"loc":["path","item_id"],"msg":"value is not a valid integer","type":"type_error.integer"}]}
使用枚举类型参数
我们可以使用枚举(Enum),对路径参数中接收到的值进行验证,并转换为枚举类型的数据。
在FastAPI中使用枚举类型定义参数的方法,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from enum import Enumfrom fastapi import FastAPIimport uvicornclass ModelName (str, Enum) : a = "aaa" b = "bbb" c = "ccc" app = FastAPI() @app.get("/models/{model_name}") async def get_model (model_name: ModelName) : print(model_name) return {"model_name" : model_name} if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/models/aaa
,返回如下:
解释说明:定义了枚举类ModelName,继承str类型和枚举类型(Enum)。
特别的,我们可以看看API文档中的Available values
。
路由访问顺序
在FastAPI中使用装饰器注册路由路径时,路由路径按照代码中的顺序保存到后端服务器的路由表中,如果后端服务器注册了多个路由路径时,则涉及到匹配顺序要求。
基本的匹配访问原则为从上到下。
在下文的例子中,注册了2个路由,第一个是静态的路由路径"/users/me",第二个是定义了带路径参数的路由路径"/users/{user_id}“。
访问URL/users/me
,按照顺序,会与第一个”/users/me"进行匹配,不与之后的进行匹配。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from fastapi import FastAPIimport uvicornapp = FastAPI() @app.get("/users/me") async def read_users_me () : print('read_users_me' ) return {"user_id" : 'read_users_me' } @app.get("/users/{user_id}") async def read_user (user_id: str) : print('read_user' ) return {"user_id" : user_id} if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/users/me
,返回如下:
1 {"user_id":"read_users_me"}
同时后台打印如下:
访问http://127.0.0.1:8000/users/some
,返回如下:
同时后台打印如下:
特别的,如果我们调整@app.get("/users/me")
和@app.get("/users/{user_id}")
的顺序,访问http://127.0.0.1:8000/users/me
,返回如下:
同时后台打印如下:
查询参数
标准查询参数
本文的"标准查询参数",是指类似如下的查询方式,其中p为键、v为值。当出现两个及以上的查询参数时,参数之间用&
关联。
例如,http://127.0.0.1:8000/items?skip=0&limit=10
,则?
后跟着skip=0
、limit=10
两个查询参数。
在路径操作函数里,通过定义函数参数的方式定义查询参数,与路由路径注册无关。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from fastapi import FastAPIimport uvicornapp = FastAPI() items = ['a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' ] @app.get("/items/") async def read_item (skip: int = 0 , limit: int = 10 ) : print('参数 skip:' , skip) print('参数 limit:' , limit) return items[skip:skip + limit] if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/items/?skip=1&limit=2
,返回如下:
后台打印日志如下:
在上例中,路径操作函数read_item
定义了两个参数skip
和limit
,这样定义的参数称为查询参数。
查询参数其实是URL地址的一部分,因此"原始输入值"是字符串;在本例中,因为在路径操作函数里定义时,显式指定了int,FastAPI自动将其转换为整型。
可选查询参数
可选查询参数,在路径操作函数里用Optional
关键字定义。
在下文的例子中,在路径操作函数read_item
中使用Optional
关键字定义了一个可选参数q
,如果没有传递该参数,其值为None。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from typing import Optionalimport uvicornfrom fastapi import FastAPIapp = FastAPI() @app.get('/items/{item_id}') async def read_item (item_id: str, q: Optional[str] = None) : if q: return {'item_id' : item_id, 'q' : q} else : return {'item_id' : item_id} if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/items/123?q=abc
,返回如下:
1 {"item_id":"123","q":"abc"}
访问http://127.0.0.1:8000/items/12345
,返回如下:
必选查询参数
在路径操作函数中定义带常规类型的查询参数,并且不带默认值,则该参数是必选参数。
在下文代码中,函数read_item
定义了两个必选查询参数,这两个参数都没有指定默认值,其中item_id
在装饰器中定义为路径参数,是必传的;q
未在装饰器中定义,所以是查询参数。这两个参数的值都是必须传的,如果不传将会导致验证错误。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import uvicornfrom fastapi import FastAPIapp = FastAPI() @app.get('/items/{item_id}') async def read_item (item_id: str, q: str) : if q: return {'item_id' : item_id, 'q' : q} else : return {'item_id' : item_id} if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/items/123?q=abc
,返回如下:
1 {"item_id":"123","q":"abc"}
访问http://127.0.0.1:8000/items/123
,返回如下,缺少必选参数。
1 {"detail":[{"loc":["query","q"],"msg":"field required","type":"value_error.missing"}]}
参数类型转换(布尔类型)
关于"参数类型转换",其实在上文我们已经提到了。通过URL传递的查询参数,参数值的原始类型是字符串,在上文中,将参数类型定义为int类型,FastAPI会验证参数并将其转换为int类型。
在这里我们讨论FastAPI解析bool类型。
True
、true
、yes
、1
,会转换成布尔值True。
False
、false
、no
、0
,会转换成布尔值False。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from typing import Optionalimport uvicornfrom fastapi import FastAPIapp = FastAPI() @app.get("/items/{item_id}") async def read_item (item_id: str, q: Optional[str] = None, short: bool = False) : item = {"item_id" : item_id} if q: item.update({"q" : q}) if short: item.update({"description" : "描述描述" }) return item if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/items/123
,返回如下:
访问http://127.0.0.1:8000/items/123?short=True
,返回如下:
1 {"item_id":"123","description":"描述描述"}
访问http://127.0.0.1:8000/items/123?short=true
,返回如下:
1 {"item_id":"123","description":"描述描述"}
访问http://127.0.0.1:8000/items/123?short=yes
,返回如下:
1 {"item_id":"123","description":"描述描述"}
访问http://127.0.0.1:8000/items/123?short=1
,返回如下:
1 {"item_id":"123","description":"描述描述"}
请求体
在本文,我们讨论如何在FastAPI中接收请求体。
定义请求体的数据模型
我们通过继承Pydantic的BaseModel,定义请求体的数据模型。
在下文的例子中,定义了一个继承BaseModel的数据模型类Item。
Item类中定义了4个字段,其中name和price是必选参数,另外两个参数description和tax使用了Optional,是可选参数。
在注册路由路径时,需要使用@app.post
装饰器,即将请求方法设置为POST,以支持请求体的传递。
@app.get
装饰器注册路由,不支持传递请求体。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from typing import Optionalimport uvicornfrom fastapi import FastAPIfrom pydantic import BaseModelclass Item (BaseModel) : name: str description: Optional[str] = None price: float tax: Optional[float] = None app = FastAPI() @app.post("/items/") async def create_item (item: Item) : return item if __name__ == '__main__' : uvicorn.run(app=app)
传送请求体,示例代码:
1 2 3 4 5 6 7 8 9 10 curl -X 'POST' \ 'http://127.0.0.1:8000/items/' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "名名名", "description": "描述描述", "price": 100, "tax": 0.123 }'
运行结果:
1 2 3 4 5 6 { "name": "名名名", "description": "描述描述", "price": 100, "tax": 0.123 }
可选的请求体参数
上文,我们在查询参数中介绍了使用Optional关键字将参数设置为可选参数的方法。
类似的,我们也可以使用Optional关键字将请求体参数设置为可选参数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from typing import Optionalimport uvicornfrom fastapi import FastAPI, Pathfrom pydantic import BaseModelapp = FastAPI() class Item (BaseModel) : name: str description: Optional[str] = None price: float tax: Optional[float] = None @app.put("/items/{item_id}") async def update_item (*, item_id: int = Path(..., title="元素ID" , ge=0 , le=1000 ) , q: Optional[str] = None, item: Optional[Item] = None) : results = {"item_id" : item_id} if q: results.update({"q" : q}) if item: results.update({"item" : item}) return results if __name__ == '__main__' : uvicorn.run(app=app)
同时使用多个请求体
在下文的例子中,我们定义了两个数据模型,都继承了BaseModel。
在路径操作函数的参数列表中,也定义了两个请求体对象,分别对应两个数据模型。
Web后端服务器期望的请求体对象的格式,使用参数名(对应函数中的item
、user
)作力JSON模式中的键,参数名对应的数据是每个键对应的JSON模式文本。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 { "item": { "name": "名名", "description": "描述", "price": 123.45, "tax": 0.123 }, "user": { "username": "用户名", "full_name": "完整名" } }
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from typing import Optionalimport uvicornfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI() class Item (BaseModel) : name: str description: Optional[str] = None price: float tax: Optional[float] = None class User (BaseModel) : username: str full_name: Optional[str] = None @app.put("/items/{item_id}") async def update_item (item_id: int, item: Item, user: User) : results = {"item_id" : item_id, "item" : item, "user" : user} return results if __name__ == '__main__' : uvicorn.run(app=app)
同时传送多个请求体,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 curl -X 'PUT' \ 'http://127.0.0.1:8000/items/123' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "item": { "name": "名名", "description": "描述", "price": 123.45, "tax": 0.123 }, "user": { "username": "用户名", "full_name": "完整名" } }'
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "item_id": 123, "item": { "name": "名名", "description": "描述", "price": 123.45, "tax": 0.123 }, "user": { "username": "用户名", "full_name": "完整名" } }
常规数据类型作为请求体使用
在上文,定义请求体对象的方法是,在路径操作函数中将参数的数据类型设置成继承自Pydantic的BaseModel类。
在有些情况下,请求数据可能不是对象,而是一个Python常规数据类型的值,此时可以使用Body类管理这样的数据。
使用Body类,可以将指定参数设置为请求体对象中的另外一个键。
在下文的例子中,增加一个请求体参数importance
,其数据类型为int。同时,在调用Body类时,使用了一个参数gt=0
,这是Pydantic模型中的用法,可以给参数增加校验规则。gt=0
的含义是importance
的值要大于0。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from typing import Optionalimport uvicornfrom fastapi import Body, FastAPIfrom pydantic import BaseModelapp = FastAPI() class Item (BaseModel) : name: str description: Optional[str] = None price: float tax: Optional[float] = None class User (BaseModel) : username: str full_name: Optional[str] = None @app.put("/items/{item_id}") async def update_item (item_id: int, item: Item, user: User, importance: int = Body(..., gt=0 ) ) : results = {"item_id" : item_id, "item" : item, "user" : user, "importance" : importance} return results if __name__ == '__main__' : uvicorn.run(app=app)
发送请求提,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 curl -X 'PUT' \ 'http://127.0.0.1:8000/items/123' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "item": { "name": "名名名名", "description": "描述描述", "price": 0, "tax": 0 }, "user": { "username": "用户名", "full_name": "完整名" }, "importance": 123 }'
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "item_id": 123, "item": { "name": "名名名名", "description": "描述描述", "price": 0, "tax": 0 }, "user": { "username": "用户名", "full_name": "完整名" }, "importance": 123 }
特别的,如果我们的importance
小于等于0,会收到报错信息,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 curl -X 'PUT' \ 'http://127.0.0.1:8000/items/123' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "item": { "name": "名名名名", "description": "描述描述", "price": 0, "tax": 0 }, "user": { "username": "用户名", "full_name": "完整名" }, "importance": 0 }'
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "detail": [ { "loc": [ "body", "importance" ], "msg": "ensure this value is greater than 0", "type": "value_error.number.not_gt", "ctx": { "limit_value": 0 } } ] }
同时使用路径参数、查询参数和请求体
我们可以同时使用路径参数、查询参数和请求体。
FastAPI会依次按照如下顺序对路径操作函数的参数进行解析:
在注册的路由路径中匹配参数名称,匹配到的参数会被解析为路径参数,未匹配到路径参数名称的进入第二步匹配。
如果参数属于Python的常规类型(str
、int
、float
、 bool
等),则参数被解析为查询参数。
如果参数类型是数据模型类,则参数被解析为请求体参数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from typing import Optionalimport uvicornfrom fastapi import FastAPIfrom pydantic import BaseModelclass Item (BaseModel) : name: str description: Optional[str] = None price: float tax: Optional[float] = None app = FastAPI() @app.post("/items/{item_id}") async def create_item (item_id: int, item: Item, q: Optional[str] = None) : result = {"item_id" : item_id, **item.dict()} if q: result.update({"q" : q}) return result if __name__ == '__main__' : uvicorn.run(app=app)
发送请求,示例代码:
1 2 3 4 5 6 7 8 9 10 curl -X 'POST' \ 'http://127.0.0.1:8000/items/123?q=abc' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "名名名名", "description": "描述描述", "price": 123.45, "tax": 0.123 }'
运行结果:
1 2 3 4 5 6 7 8 { "item_id": 123, "name": "名名名名", "description": "描述描述", "price": 123.45, "tax": 0.123, "q": "abc" }
表单和文件
python-multipart
如果要在FastAPI中操作表单数据,首先需要安装一个第三方库python-multipart
,安装命令如下:
1 pip install python-multipart
表单数据
在下文的例子中,导入模块Form,定义路径操作函数的参数时,请求体参数的数据类型是str,初始值调用了Form(...)
函数获取到的Form对象。
发送表单字段格式时,在HTTP的请求头中指定数据编码为application/x-www-form-urlencoded
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from fastapi import FastAPI, Formimport uvicornapp = FastAPI() @app.post("/login/") async def login (username: str = Form(...) , password: str = Form(...) ) : if password == '123456' : return {"username" : username} if __name__ == '__main__' : uvicorn.run(app=app)
发送表单请求,示例代码:
1 2 3 4 5 curl -X 'POST' \ 'http://127.0.0.1:8000/login/' \ -H 'accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'username=%E7%94%A8%E6%88%B7&password=%E5%AF%86%E7%A0%81'
运行结果:
文件上传
文件上传同样以来python-multipart
。
与使用Body类、Form类的方式类似,上传文件时,需要引入File类。
在下文的例子中,首先导入了File类对象,然后定义了两个路径操作函数,两个路径操作函数中的参数都调用File类获取File对象实例。
第一个路径操作函数中的参数类型是bytes,可以用来接收文件流,因为在HTTP中的文件上传使用的是文件流,FastAPI接收文件流时,使用的就是bytes数据类型。当上传的文件比较小的时候,可以使用这种方式接收上传文件。
第二个路径操作函数的参数类型是UploadFile,这个类型使用了一种名为"假脱机文件"的技术,当内存中的数据尺寸超过最大限制后,会将部分数据存储在磁盘中。也就是说,这种方式适合处理大文件。
需要注意的是,上传文件时使用的编码格式为multipart/form-data
。
在上传文件的同时,可以提交表单数据,但是不能提交请求体,因为请求体使用的格式为application/json
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from fastapi import FastAPI, File, UploadFileimport uvicornapp = FastAPI() @app.post("/files/") async def create_file (file: bytes = File(...) ) : return {"file_size" : len(file)} @app.post("/uploadfile/") async def create_upload_file (file: UploadFile = File(...) ) : return {"filename" : file.filename} if __name__ == '__main__' : uvicorn.run(app=app)
发送请求"/files/",示例代码:
1 2 3 4 5 curl -X 'POST' \ 'http://127.0.0.1:8000/files/' \ -H 'accept: application/json' \ -H 'Content-Type: multipart/form-data' \ -F 'file=@2024-01-30.png;type=image/png'
运行结果:
发送请求"/uploadfile/",示例代码:
1 2 3 4 5 curl -X 'POST' \ 'http://127.0.0.1:8000/uploadfile/' \ -H 'accept: application/json' \ -H 'Content-Type: multipart/form-data' \ -F 'file=@2024-01-30.png;type=image/png'
运行结果:
1 2 3 { "filename": "2024-01-30.png" }
UploadFile
UploadFile还提供了其他属性和方法,用来获取原始上传文件的元数据。
主要属性有:
filename,原始文件名。
content_type,文件类型,比如image/jpeg
。
file,文件对象。
主要方法有:
write(data),写入str或bytes类型的数据。
read(size),从文件对象中的当前位置开始,读取size大小的数据。
seck(offset),将当前位置指向文件中指定的位置,一般配合read(size)方法使用。
close(),关闭文件对象。
表单和多文件上传
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from fastapi import FastAPI, File, Form, UploadFileimport uvicornapp = FastAPI() @app.post("/files/") async def create_file (file: UploadFile = File(...) , file2: UploadFile = File(...) , token: str = Form(...) ) : return { "token" : token, "fileb_content_type" : file.filename, "fileb_content_type" : file.content_type, } if __name__ == '__main__' : uvicorn.run(app=app)
发送请求,示例代码:
1 2 3 4 5 6 7 curl -X 'POST' \ 'http://127.0.0.1:8000/files/' \ -H 'accept: application/json' \ -H 'Content-Type: multipart/form-data' \ -F 'file=@2024-01-30.png;type=image/png' \ -F 'file2=@2024-01-30.png;type=image/png' \ -F 'token=123456'
运行结果:
1 2 3 4 { "token": "123456", "fileb_content_type": "image/png" }
响应
响应模型
响应模型(Response Model),指在处理响应数据时,将响应数据转换成Pydantic数据模型实例,以保证响应数据的规范性。同时,响应数据模型在API文档中体现为JSON模式,这样也增加了文档的可读性及接口的标准化。
自定义响应模型
自定义响应模型,步骤如下:
定义响应数据模型(Response Data Model)类。
注册路由路径的装饰器方法中通过response_model
参数指定响应数据模型来确定响应模型对象。
响应数据被转换为响应数据模型格式返回给客户端
在下文的例子中,定义了两个数据模型,请求数据模型UserIn、响应数据模型UserOut。其中UserOut少了password字段。
在路径操作函数的装饰器中,使用参数response_model
定义响应模型为UserOut,所以FastAPI会使用UserOut模型将返回数据转换为响应数据,响应数据中将不包含password字段。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from typing import Optionalimport uvicornfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI() class UserIn (BaseModel) : username: str password: str email: str full_name: Optional[str] = None class UserOut (BaseModel) : username: str email: str full_name: Optional[str] = None @app.post("/user/", response_model=UserOut) async def create_user (user: UserIn) : return user if __name__ == '__main__' : uvicorn.run(app=app)
发送请求,示例代码:
1 2 3 4 5 6 7 8 9 10 curl -X 'POST' \ 'http://127.0.0.1:8000/user/' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "username": "用户名", "password": "密码", "email": "邮箱", "full_name": "全名" }'
运行结果:
1 2 3 4 5 { "username": "用户名", "email": "邮箱", "full_name": "全名" }
自定义控制规则
在使用Pydantic定义数据模型时,允许字段有默认值。如果在输出响应数据时,这些带有默认值的字段没有明确设置字段值,客户端也可以按照默认值接收这些字段的数据。
若不想让客户端接收数据模型的默认值字段,则在路径操作函数的装饰器中设置响应模型参数时,可以同时设置response_model_exclude_unset
参数,以忽略带有默认值的字段。
在下文的例子中,在路径操作函数read_item
的装饰器参数里,设置了响应模型为Data,该数据模型中包含多个具有默认值的字段。但在返回内容中,只包含了明确设置字段值的字段name
、price
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from typing import List, Optionalimport uvicornfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI() class Data (BaseModel) : name: str description: Optional[str] = None price: float tax: float = 10.5 tags: List[str] = [] datas = { "min" : {"name" : "最小化" , "price" : 50.2 }, "max" : {"name" : "最大化" , "description" : "都有值" , "price" : 62 , "tax" : 20.2 }, "same" : {"name" : "默认" , "description" : None , "price" : 50.2 , "tax" : 10.5 , "tags" : []}, } @app.get("/data/{data_id}", response_model=Data, response_model_exclude_unset=True) async def read_item (data_id: str) : return datas[data_id] if __name__ == '__main__' : uvicorn.run(app=app)
发送请求,示例代码:
1 2 3 curl -X 'GET' \ 'http://127.0.0.1:8000/data/min' \ -H 'accept: application/json'
运行结果:
1 2 3 4 { "name": "最小化", "price": 50.2 }
除了response_model_exclude_unset
,还可以在路径操作函数的装饰器中使用以下参数:
response_model_exclude_defaults=True
,忽略与默认值相同的字段。
response_model_exclude_none=True
,忽略值为None的字段。
response_model_include={}
,输出数据中仅包含指定的字段。
response_model_exclude={}
,输出数据中仅排除指定的字段。
这些参数都来自于Pydantic库,更多的内容可以参考Pydantic的官方文档中的模型导出部分。
使用多个响应模型
在有些场景下,一个路径操作函数需要返回不同的数据模型。
可以将Python中的类型联合体(Union)类设置为response_model
对象值,在联合体中配置多个响应模型。
在下文的例子中,在路径操作函数的装饰器中,设置了响应模型为联合体,在联合体中设置了响应数据模型Dog和Cat。代码会根据逻辑,返回Dog或者Cat的响应模型。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 from typing import Unionimport uvicornfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI() class BaseItem (BaseModel) : description: str type: str class Cat (BaseItem) : type = "cat" class Dog (BaseItem) : type = "dog" color: str items = { "item1" : { "description" : "三酷猫" , "type" : "cat" }, "item2" : { "description" : "中华田园犬" , "type" : "dog" , "color" : "yellow" , }, } @app.get("/items/{item_id}", response_model=Union[Dog, Cat]) async def read_item (item_id: str) : return items[item_id] if __name__ == '__main__' : uvicorn.run(app=app)
内置响应类
七种内置响应类
FastAPI中内置了以下响应类:
纯文本响应(PlainTextResponse)
HTML响应(HTMLResponse)
重定向响应(RedirectResponse)
JSON响应(JSONResponse)
通用响应(Response)
流响应(StreamingResponse)
文件响应(FileResponse)
纯文本响应
纯文本响应(PlainTextResponse),服务器端将一段纯文本写入响应后,直接返回给客户端,FastAPI不会对纯文本响应的内容做任何校验和转换。
使用纯文本响应的方法,在路径操作函数的装饰器中使用参数response_class
指定响应类为PlainTextResponse
类,然后FastAPI会将路径操作函数的返回值转换成字符串直接返回。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from fastapi import FastAPIfrom fastapi.responses import PlainTextResponseimport uvicornapp = FastAPI() @app.get("/", response_class=PlainTextResponse) async def main () : return "Hello World" if __name__ == '__main__' : uvicorn.run(app=app)
HTML响应
FastAPI内置了HTMLResponse类,用于处理HTML数据的响应返回。
在下文的例子中,用了两种方式返回HTML内容。
路径操作函数read_items1
在装饰器中设置了参数response_class
的值为HTMLResponse,浏览器会将返回的内容按照HTML格式解析。
路径操作函数read_items2
将HTML内容文本写入HTMLResponse对象并返回响应,没有在装饰器中设置response_class。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from fastapi import FastAPIfrom fastapi.responses import HTMLResponseimport uvicornapp = FastAPI() html_content = """ <html> <head> <title>浏览器顶部的标题</title> </head> <body> <h1>三酷猫</h1> </body> </html> """ @app.get("/html/", response_class=HTMLResponse) async def read_items1 () : return html_content @app.get("/default/") async def read_items2 () : return HTMLResponse(content=html_content) if __name__ == '__main__' : uvicorn.run(app=app)
重定向响应
在FastAPI中,内置的RedirectResponse类用于处理HTTP重定向的需求。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from fastapi import FastAPIfrom fastapi.responses import RedirectResponseimport uvicornapp = FastAPI() @app.get("/kaka") async def read_three () : return RedirectResponse("https://kakawanyifan.com" ) if __name__ == '__main__' : uvicorn.run(app=app)
JSON响应
在路径操作函数中,可以返回Response类或者任意Response的子类,比如JSONResponse。
默认情况下,FastAPI会使用jsonable_encoder()
方法将模型数据转换成JSON格式,然后FastAPI会将这些JSON格式的数据放到一个JSONResponse类实例中,接着将该类的实例作为响应数据返回给客户端。
在下文的例子中,没有直接返回数据模型,而是使用jsonable_encoder()
方法将数据模型转换成JSON格式的数据,再写入JSONResponse中并返回,这就是 FastAPI返回数据的过程。
在通常情况下,我们可以直接返回数据模型Item的实例item,不需要转换之后再返回。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from datetime import datetimefrom typing import Optionalfrom fastapi import FastAPIfrom fastapi.encoders import jsonable_encoderfrom fastapi.responses import JSONResponsefrom pydantic import BaseModelimport uvicornclass Item (BaseModel) : title: str timestamp: datetime description: Optional[str] = None app = FastAPI() @app.post("/item/") def update_item (item: Item) : json_compatible_item_data = jsonable_encoder(item) return JSONResponse(content=json_compatible_item_data) if __name__ == '__main__' : uvicorn.run(app=app)
通用响应
当返回数据不是JSON格式时,可以直接使用Response类响应对象。
在下文用Response类对象返回一段XML。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from fastapi import FastAPI, Responsefrom typing import Optionalimport uvicornapp = FastAPI() @app.get("/document/") def get_legacy_data (id: Optional[int] = None) : data = """<?xml version="1.0" encoding="utf-8" ?> <Document> <Header> 这里是页头 </Header> <Body> 这里是内容 </Body> <Footer> 这里是页脚 </Footer> </Document> """ return Response(content=data, media_type="application/xml" ) if __name__ == '__main__' : uvicorn.run(app=app)
访问http://127.0.0.1:8000/document/
,响应如下:
通过响应内容,我们可以看到,网页上显示的内容不是JSON格式,而是程序中指定的XML格式,并且FastAPI也没有对Response类对象的数据进行校验和转换。
流响应
在客户端和Web服务器端之间进行的数据传输,除了使用带有格式的文本数据以外,还可以使用字节流(Stream)进行传输。字节流响应的内容是二进制格式的,比如音频、视频、图片等。
在FastAPI中通过StreamingResponse类实现字节流响应。
在下文的例子中,使用open方法打开视频文件,得到文件流,接着使用StreamingResponse类的实例返回文件流的内容。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from fastapi import FastAPIfrom fastapi.responses import StreamingResponseimport uvicornsome_file_path = "large-video-file.mp4" app = FastAPI() @app.get("/") def main () : file_like = open(some_file_path, mode="rb" ) return StreamingResponse(file_like, media_type="video/mp4" ) if __name__ == '__main__' : uvicorn.run(app=app)
文件响应
在FastAPI中,通过FileResponse类处理异步文件响应,与上一节的流响应相比,文件响应类在实例化时可以接收更多的参数。例如:
path,要流式传输的文件的文件路径。
headers,任何自定义响应头,传入字典类型。
media_type,给出媒体类型的字符串;如果未设置,则文件名或路径将用于推断媒体类型。
filename,如果给出,它将包含在响应Header的Content-Disposition中。
文件响应将包含Content-Length、Last-Modified和ETag的响应头,这些信息都将传递给浏览器,作为浏览器处理文件响应的依据。
在下文的例子中,导入文件响应类,并且在路径操作函数中将FileResponse类实例化,指定文件路径为参数,通过这种方式,就可以实现文件的异步下载功能。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from fastapi import FastAPIfrom fastapi.responses import FileResponseimport uvicornsome_file_path = "large-video-file.mp4" app = FastAPI() @app.get("/") async def main () : return FileResponse(some_file_path) if __name__ == '__main__' : uvicorn.run(app=app)
与使用StreamingResponse类不同。
使用StreamingResponse
类,需要先将文件打开,载入文件对象中进行返回,文件内容是一次性读取的,如果文件很大,就会占用大量的内存。
使用FileResponse
类,通过文件路径指定生成了一个FileResponse类实例,文件是异步读取的,会占用更少的内存。
所以,在实际的场景中,当需要直接处理流(Stream)时,使用StreamingResponse类; 当处理文件时,使用FileResponse类。