Python 后端开发
参考资料
Pydantic
- 定义模型, 以供类型检查
- 模型还具有许多高级特性, 例如:
from pydantic import BaseModel
class User(BaseModel):
id: int # 类型
name: str = 'Jane Doe' # 默认值
key = Column(String(63), unique=True) # 从实例属性中获取类型
user = User(id='123', name='John Doe')
User.model_validate({'id': 123, 'name': 'John Doe'}) # 类型检查
User.model_validate_json('{"id": 123, "name": "John Doe"}') # 类型检查
User.model_dump() # 转换为字典
User.model_dump_json() # 转换为 json
FastAPI
- 依赖 OpenAPI 与 Pydantic 库实现
- 数据验证
- 自动生成交互式文档(可以自定义任何基于 OpenAPI 的文档)
- 可使用
async
与 await
实现异步
- 可以定义元数据, 提供给文档
- 集成 HTTPX 库进行测试
from fastapi import FastAPI
app = FastAPI() # 创建一个 FastAPI 实例
@app.get("/") # 路由
async def root(): # 异步函数
return {"message": "Hello World"} # 返回 json
- 一个 url
协议://主机:端口/路径
路径亦称为端点 / 路由
- 使用 HTTP 方法来区分不同的操作
- GET - 查询
- POST - 创建
- PUT - 更新
- DELETE - 删除
路径参数
- 对于路径参数函数的类型重载, 注意声明顺序决定检查顺序
- 可以使用
Path
对象来声明路径参数类型, 默认值等
@app.get("/items/{id}") # 路径参数
async def read_item(id: int): # fastapi 会进行类型检查
return {"item_id": id}
from enum import Enum
# 声明一个枚举类进行检查
class ModelName(str, Enum): # 继承 str 的原因是为了兼容生成文档
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
查询参数
- 形如
/items/?skip=0&limit=10
/items/
会使用默认值
- 可使用
None
类型表示可选参数
- 也可以是
bool
类型, 会自动转换为 True
和 False
- 这回不依赖声明顺序了, 检查查询参数来决定使用哪个函数
- 可以使用
Query
对象来声明查询参数类型, 默认值等
Annotated[FilterParams, Query()]
用一个模型声明查询参数类型
from fastapi import FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10): # 没同名, 代表查询参数
return fake_items_db[skip : skip + limit]
请求体
- 直接实现类型检查 + JSON
- 和上面的可以叠加
- 也可以用多个单独的参数来实现
Annotated[class, Cookie()]
声明 Cookie 参数
- Header / Cookie / Query / Path 同理
- 请求体不是 JSON 格式的, 而是表单格式的
- 使用
Form
对象来声明表单参数
Annotated[class, From()]
亦可
- 也可以使用
File
对象来声明文件参数
- 也可以使用
UploadFile
对象来声明文件参数类型
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
wsl --set-default-version 2
1
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item): # 请求体
return item
更新
- 可以单独定义需要更新的部分的模型, 这样就不会覆盖所有字段
响应
jsonable_encoder
可以将任意类型转换为 JSON 格式兼容
@app.post("/user/", response_model=UserOut) # 用装饰器声明响应模型
@app.post("/items/", status_code=201) # 声明状态码
后台任务
- 可以使用
BackgroundTasks
对象来声明后台任务
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def write_notification(email: str, message=""): # 后台任务
with open("log.txt", mode="w") as email_file:
content = f"notification for {email}: {message}"
email_file.write(content)
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(write_notification, email, message="some notification") # 进行后台任务
return {"message": "Notification sent in the background"}
静态文件
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static") # 挂载静态文件
# 指定路径, 实例, 名称
依赖注入
- 可以使用
Depends
对象来声明依赖
- 依赖可以是任何可调用对象, 包括函数, 类, 协程函数
- 可以嵌套依赖, 菱形依赖自动处理
- 路径装饰器也可以声明依赖, 依赖会被调用 (比如用来判断请求头), 但依赖的值不会被传递给响应函数
- 相应的
app
实例也可以声明依赖, 相当于为所有路由声明依赖
async def get_db(): # 这么声明的依赖会停在 yield 处, 并将 db 传递给响应函数
db = DBSession()
try:
yield db
finally: # 直到响应函数返回后, 才会执行 finally 块
db.close()
from typing import Union
from fastapi import Depends, FastAPI
app = FastAPI()
async def common_parameters(
q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
return commons
@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
return commons
中间件
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
安全
- 可以使用
CORSMiddleware
来添加 CORS 支持 (指定允许的域名, 方法等)
- 基于 OAuth2 规范实现安全认证
- fastapi 会接收账号密码, 验证后返回 token
- 前端保存 token, 每次请求时在请求头添加 Authorization : "Bearer+token"
- 后端验证 token, 并返回用户信息
- 使用 passlib 库来哈希密码
- 使用 pyJWT 库来生成 JWT 令牌
from typing import Union
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # 验证器
class User(BaseModel): # 声明用户模型
username: str
email: Union[str, None] = None
full_name: Union[str, None] = None
disabled: Union[bool, None] = None
def fake_decode_token(token): # 返回用户
return User(
username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
)
async def get_current_user(token: str = Depends(oauth2_scheme)): # 验证函数, 依赖于验证器
user = fake_decode_token(token)
return user
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)): # 依赖于验证函数
return current_user
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()): # 登陆函数, 表单依赖于 OAuth2PasswordRequestForm
# 注意, 表单模型不能自定义, 是 OAuth2 规范约定的模型
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
user = UserInDB(**user_dict)
hashed_password = fake_hash_password(form_data.password) # 哈希密码, 一般使用 passlib 库
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
return {"access_token": user.username, "token_type": "bearer"}
架构
- 可以使用
APIRouter
来声明本文件的路由组 (app 的分身)
router = APIRouter()
@router.get("/")
- app 用
include_router
来包含路由组
- 可以使用
prefix
和 tags
来声明路由组的前缀和标签
router = APIRouter(prefix="/users", tags=["users"])
- 亦可
app.include_router(router, prefix="/admin", tags=["admin"])
from fastapi import Depends, FastAPI
from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users
app = FastAPI(dependencies=[Depends(get_query_token)])
app.include_router(users.router)
app.include_router(items.router)
app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
dependencies=[Depends(get_token_header)],
responses={418: {"description": "I'm a teapot"}},
)
@app.get("/")
async def root():
return {"message": "Hello Bigger Applications!"}
mysql-connector-python
- 一个 mysql 数据库的 python 驱动程序
- 几乎是 sql 语句的一对一实现
SQLAlchemy
连接
from sqlalchemy import create_engine
engine = create_engine("sqlite+pysqlite:///:memory:", echo=True) # 内存中的 sqlite
# 数据库+驱动://用户名:密码@主机:端口/数据库名
# echo=True 打印 sql 语句
# 不多介绍文本 SQL
with engine.connect() as conn: # 通过连接对数据库进行操作
conn.execute(
text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
[{"x": 1, "y": 1}, {"x": 2, "y": 4}],
) # 操作会被缓存, 直到 commit 才会被执行
conn.commit()
result = conn.execute(...).all # 结果作为元组返回
with Session(engine) as session: # 对 connect 的封装
result = session.execute(...)
session.commit() # 提交事务
对象关系映射
from sqlalchemy import MetaData
metadata_obj = MetaData() # 元数据对象
from sqlalchemy import Table, Column, Integer, String
user_table = Table( # 声明表结构
"user_account", # 表名
metadata_obj, # 元数据对象
Column("id", Integer, primary_key=True), # 声明列
Column("name", String(30)),
Column("fullname", String),
)
print(user_table.c.keys) # 列名
Column("user_id", ForeignKey("user_account.id"), nullable=False) # 外键
# 更好的方法
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase): # 声明基类
pass
class User(Base): # 声明模型
__tablename__ = "user_account" # 表名
id = Column(Integer, primary_key=True)
name = Column(String(30))
fullname = Column(String)
addresses = relationship(
"Address", back_populates="user", cascade="all, delete-orphan"
) # 关系
some_table = Table("some_table", metadata_obj, autoload_with=engine) # 自动加载表结构
操作
from sqlalchemy import insert, select, update, delete
stmt = insert(user_table).values(x=6, y=8, z=10) # 插入语句
stmt = select(user_table).where(...) # 查询语句
stmt = select(user_table).where(...).join_from(another_table) # 查询语句
stmt = update(user_table).where(...).values(x=6, y=8) # 更新语句
stmt = delete(user_table).where(...) # 删除语句
# 还有很多, 大体与 SQL 语句对应
with Session(engine) as session: # 操作
session.execute(stmt)
session.commit()
ORM 操作
from sqlalchemy.orm import Session
with Session(engine) as session: # 操作
session.add_all([User(name="spongebob", fullname="Spongebob Squarepants"), ...]) # 插入
session.flush() # 缓存
session.commit() # 提交
session.query(User).filter(User.name.in_(["sandy", "susan"])).all() # 查询
session.query(User).filter(User.name == "sandy").first() # 查询
session.query(User).filter(User.name.like("%ed")).all() # 查询
session.query(User).filter(User.name == "sandy").name = "666" # 更新
session.delete(result) # 删除
session.commit() # 提交
session.rollback() # 回滚
session.close() # 关闭
relationship
- 可以定义延迟加载, 预加载, 级联删除等特性
cascade="all, delete-orphan"
级联删除
- 正常声明一对多
uselist=False
表示单个对象而非列表
- 辅以外键约束, 实现其它关系
class Parent(Base):
__tablename__ = 'parents'
id = Column(Integer, primary_key=True)
children = relationship("Child", back_populates="parent")
class Child(Base):
__tablename__ = 'children'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parents.id'))
parent = relationship("Parent", back_populates="children")
class Enrollment(Base):
__tablename__ = 'enrollments'
student_id = Column(Integer, ForeignKey('students.id'), primary_key=True)
course_id = Column(Integer, ForeignKey('courses.id'), primary_key=True)
enrollment_date = Column(DateTime)
# 定义与 Student 和 Course 的关系
student = relationship("Student", back_populates="enrollments")
course = relationship("Course", back_populates="enrollments")
class Student(Base):
__tablename__ = 'students'
id = Column(Integer, primary_key=True)
name = Column(String)
enrollments = relationship("Enrollment", back_populates="student")
class Course(Base):
__tablename__ = 'courses'
id = Column(Integer, primary_key=True)
title = Column(String)
enrollments = relationship("Enrollment", back_populates="course")
Uvicorn
- 基于 ASGI 协议的异步 web 服务器 (不同于 WSGI)
uvicorn filename:objname # 启动服务器
# --reload 热重载
# --port 端口
# --host IP
# --workers 进程数
# --log-level 日志级别
# --log-config 日志文件位置
# --ssl-keyfile=SSL密钥文件 --ssl-certfile=SSL证书文件