Skip to main content

FastAPI

  1. https://fastapi.tiangolo.com/zh/tutorial/sql-databases/

pydantic

Pydantic 是一个 Python 库,用于数据解析和验证。它基于 Python 类型注解,提供了一个简单的方法来定义数据模型。这些数据模型可以用于读取和验证复杂的数据类型,如 JSON、环境变量或表单字段,确保它们符合预期的格式,并在数据不正确时提供详细的错误信息。

在 FastAPI 中,Pydantic 被广泛使用来定义请求和响应的数据模型,因为它可以自动处理数据的序列化和反序列化,以及数据验证。

https://docs.pydantic.dev/latest/concepts/dataclasses/

最佳实践

FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API 服务,它基于标准 Python 类型提示。以下是使用 FastAPI 时的一些最佳实践:

  1. 使用 Pydantic 模型:

    • 利用 Pydantic 创建数据模型来定义请求和响应的数据结构。
    • 通过模型验证确保数据的有效性。
  2. 异步编程:

    • 当需要进行 I/O 操作如访问数据库、文件系统或进行网络调用时,使用异步方法和函数。
    • 使用 asyncawait 关键字来编写异步代码。
  3. 依赖注入:

    • 使用 FastAPI 的依赖注入系统来提供共享逻辑,如数据库会话管理、权限验证、配置设置等。
  4. 路由和 API 结构:

    • 组织代码结构,将路由分离到不同的模块中,使用 APIRouter 进行子路由的管理。
    • 使用路径操作装饰器(如 @app.get(), @app.post() 等)来清晰地定义端点和 HTTP 方法。
  5. 中间件使用:

    • 根据需要合理地使用中间件,例如处理 CORS、认证、日志记录等。
  6. 安全性:

    • 利用 FastAPI 的安全和认证特性,如 OAuth2 密码流、Bearer 令牌等。
    • 使用 HTTPS 和其他安全最佳实践来保护你的 API。
  7. 错误处理:

    • 使用异常处理器来捕获并处理预期内的错误和异常。
  8. 性能优化:

    • 使用 Starlette 的静态文件服务来提供静态内容。
    • 考虑使用缓存来提升性能。
  9. API 文档:

    • 利用 FastAPI 自动生成的 Swagger UI 和 ReDoc 来创建和维护 API 文档。
  10. 测试:

    • 编写测试用例并使用 Pytest 等工具来进行自动化测试。
    • 利用 FastAPI 测试客户端来测试 API 端点。
  11. 环境管理:

    • 使用环境变量来管理配置,例如数据库链接。
    • 可以使用 python-dotenvpydanticBaseSettings 类来加载和管理环境变量。
  12. 数据库操作:

    • 使用 ORM 工具(如 SQLAlchemy 或 Tortoise ORM)来抽象化数据库操作。
    • 确保在请求结束时关闭数据库会话。
  13. 项目结构:

    • 按照功能模块组织代码,遵循一致的项目结构。
    • 将业务逻辑、数据库模型、依赖项和路由分离到不同的文件或模块中。
  14. 部署:

    • 使用 Uvicorn 或 Gunicorn 作为 ASGI 服务器来运行你的 FastAPI 应用。
    • 考虑使用容器化技术(如 Docker)来简化部署和缩放。
  15. 持续集成/持续部署 (CI/CD):

    • 设置 CI/CD 流程以自动化测试和部署过程。

遵循这些最佳实践有助于确保你的 FastAPI 应用是可维护的、可扩展的、安全的,并且能够提供高性能。

项目结构

设计一个良好的项目结构是确保可维护性和可扩展性的关键。以下是一个典型的 FastAPI 项目结构示例,它遵循了分层架构和模块化设计原则:

my_fastapi_project/

├── app/ # 应用主目录
│ ├── api/ # 与 API 相关的定义
│ │ ├── dependencies/ # 依赖项,如数据库会话、权限验证
│ │ ├── endpoints/ # 路由和视图
│ │ │ ├── __init__.py
│ │ │ ├── item.py # 与项目相关的路由
│ │ │ └── user.py # 与用户相关的路由
│ │ └── __init__.py
│ │
│ ├── core/ # 核心配置和设置
│ │ ├── config.py # 配置相关
│ │ └── security.py # 安全和认证相关
│ │
│ ├── crud/ # 数据库 CRUD 操作
│ │ ├── __init__.py
│ │ ├── crud_item.py # 项目相关的 CRUD 操作
│ │ └── crud_user.py # 用户相关的 CRUD 操作
│ │
│ ├── db/ # 数据库相关
│ │ ├── base.py # 数据库模型的基类
│ │ ├── init_db.py # 数据库初始化脚本
│ │ ├── models/ # 数据库模型定义
│ │ │ ├── __init__.py
│ │ │ ├── item.py # 项目模型
│ │ │ └── user.py # 用户模型
│ │ └── session.py # 数据库会话管理
│ │
│ ├── models/ # Pydantic 模型定义
│ │ ├── __init__.py
│ │ ├── item.py # 项目相关的 Pydantic 模型
│ │ └── user.py # 用户相关的 Pydantic 模型
│ │
│ ├── schemas/ # 数据验证和序列化模式
│ │ ├── __init__.py
│ │ ├── item.py
│ │ └── user.py
│ │
│ ├── main.py # FastAPI 应用实例和路由的入口点
│ │
│ └── utils/ # 工具和辅助函数
│ ├── __init__.py
│ └── some_util.py

├── tests/ # 测试目录
│ ├── api/ # API 测试
│ ├── crud/ # CRUD 操作测试
│ └── __init__.py

├── Dockerfile # Docker 容器配置文件
├── requirements.txt # 项目依赖文件
└── README.md # 项目文档和说明

这个结构清晰地分隔了不同的项目组件:

  • app/main.py 是 FastAPI 应用的入口点,它创建和配置了应用实例。
  • app/api/ 包含了所有的路由和视图,它们可能被进一步分组到不同的模块。
  • app/models/ 包含了 Pydantic 模型,用于请求和响应的数据验证。
  • app/crud/ 包含了与数据库交互的逻辑,通常是 CRUD(创建、读取、更新、删除)操作。
  • app/db/ 包含了数据库模型(ORM 模型)和数据库会话管理。
  • app/core/ 包含了核心配置和安全设置。
  • app/utils/ 包含了项目中使用的辅助函数和工具。
  • tests/ 包含了所有的测试代码,以确保应用的稳定性和可靠性。

这个项目结构是一个起点,根据你的项目需求,你可能需要调整和扩展这个结构。例如,对于大型项目,你可能需要进一步模块化你的 API,或者将业务逻辑从视图中分离出来。

github 项目 参考

是的,GitHub 上有很多优秀的 FastAPI 项目可以作为参考。这些项目不仅展示了如何使用 FastAPI 构建应用程序,还往往遵循了良好的项目结构和最佳实践。以下是一些你可以查看的开源项目:

  1. FastAPI RealWorld Example App:

  2. Full Stack FastAPI and PostgreSQL:

  3. Awesome FastAPI:

  4. FastAPI Microservices:

  5. FastAPI Example Project:

在查看这些项目时,请注意它们的项目结构、代码组织、测试策略以及如何处理配置和环境变量。这些都是你在设计自己的 FastAPI 项目时需要考虑的关键要素。

请记住,开源项目可能随时会更新,而某些项目可能不再维护或不符合最新的最佳实践。因此,在使用它们作为参考时,一定要结合当前的 FastAPI 文档和社区的最新建议。

**

是的,Python 中的 ** 字典解包操作符和 ES6 (ECMAScript 2015) 中的扩展操作符 ... 在某种程度上是类似的。它们都用于展开一个集合的元素。

在 JavaScript 中,扩展操作符 ... 可以用于数组和对象:

  • 数组: 将数组元素展开成单独的元素。
  • 对象: 将对象的可枚举属性展开为单独的键值对。

例如,使用扩展操作符合并对象:

const item = { name: 'example', price: 10.99 };
const updatedItem = { ...item, description: 'An example item' };
// updatedItem 现在是 { name: "example", price: 10.99, description: "An example item" }

在 Python 中,** 操作符是用于字典的,并且有以下用途:

  • 函数调用: 将字典的键值对作为命名参数传递给函数。
  • 创建新字典: 将一个或多个字典的键值对合并成一个新的字典。

例如,使用 ** 合并字典:

item = {"name": "example", "price": 10.99}
updated_item = {**item, "description": "An example item"}
# updated_item 现在是 {'name': 'example', 'price': 10.99, 'description': 'An example item'}

尽管这两个操作符在不同的语言中使用不同的符号,它们的作用在高层次上是相似的:它们都用于将一个集合的元素或属性“展开”到另一个上下文中。

请求参数什么情况用哪种

函数参数将依次按如下规则进行识别:

  • 如果在路径中也声明了该参数,它将被用作路径参数。
  • 如果参数属于单一类型(比如 int、float、str、bool 等)它将被解释为查询参数。
  • 如果参数的类型被声明为一个 Pydantic 模型,它将被解释为请求体。

函数中*号

在 Python 中,函数定义时的 * 符号用来指示之后的参数必须以关键字参数(keyword arguments)的形式传递

让我们通过一个简单的例子来说明这一点:

# 定义一个函数,其中 * 后面的参数必须以关键字形式传递
def greet(message, *, name):
print(f"{message}, {name}!")

# 正确的调用方式
greet("Hello", name="Alice") # 输出: Hello, Alice!

# 如果尝试使用位置参数传递 name,将会引发错误
try:
greet("Hello", "Alice") # 这会抛出异常
except TypeError as e:
print(e) # 输出: greet() takes 1 positional argument but 2 were given

在这个例子中,greet 函数定义了两个参数:messagename* 放在 name 参数前面,这意味着 name 必须通过关键字来指定,即 name="Alice"。如果尝试像传递位置参数那样调用函数(即 greet("Hello", "Alice")),Python 解释器会抛出一个 TypeError,因为 name 参数必须以关键字参数的形式传递。

这个特性在函数有多个参数且某些参数有默认值时尤其有用,因为它可以防止调用者意外地将一个值赋给错误的参数。

查询参数/body 参数

@app.put("/items/{item_id}")
async def update_item(
item_id: int, item: Item, user: User, importance: Annotated[int, Body()] #这里使用 Body()来标记是body参数
):
results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
return results



# 以下为路径-查询-body参数
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None


@app.put("/items/{item_id}")
async def update_item(
item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)],
q: str | None = None, #没有body标记,也不是BaseModel,这里是查询参数
item: Item | None = None,
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
if item:
results.update({"item": item})
return results

Path/Query/Body/Cookie/Header

## Path/Query/Body/Cookie/Header 这些都是函数符合函数的特点
## Path/Query/Body 有优先级
## async def functionName(字段:Annotated[类型,上面这些函数] = 默认值):

### Path

### Query

### Body

### Cookie
from typing import Annotated, Union

from fastapi import Cookie, FastAPI

app = FastAPI()


@app.get("/items/")
async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None):
return {"ads_id": ads_id}

### Header
# HTTP headers 是大小写不敏感的,因此,因此可以使用标准Python样式(也称为 "snake_case")声明它们。
# 因此,您可以像通常在Python代码中那样使用 user_agent ,而不需要将首字母大写为 User_Agent 或类似的东西。
# 如果出于某些原因,你需要禁用下划线到连字符的自动转换,设置Header的参数 convert_underscores 为 False:



# example1 你需要禁用下划线到连字符的自动转换,设置Header的参数 convert_underscores 为 False:
from typing import Annotated, Union

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(
# strange_header 就是接受的字段
strange_header: Annotated[
Union[str, None], Header(convert_underscores=False)
] = None,
):
return {"strange_header": strange_header}


# example2 重复heder 声明为List
from typing import Annotated, List, Union

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None):
return {"X-Token values": x_token}
### Request

### Response

fastapi 返回剔除空值

在 FastAPI 中,当你定义了一个 Pydantic 模型并使用它作为请求体或响应模型时,Pydantic 会负责序列化和反序列化数据。默认情况下,Pydantic 的模型会输出所有字段,包括那些具有默认值的字段。如果你想要排除掉具有默认值的字段,可以在 Pydantic 模型中使用 dict() 方法,并传递 exclude_unset=True 参数。

例如,如果你有以下模型:

from pydantic import BaseModel

class Item(BaseModel):
name: str
description: str = None

在你的路由处理函数中,你可以这样返回一个不包含默认值的响应:

@app.get("/items/{item_id}")
async def read_item(item_id: int):
item = get_item_by_id(item_id) # 假设这个函数返回一个 Item 实例
return item.dict(exclude_unset=True)

在这个例子中,如果 description 字段没有被设置(即它是默认值 None),它将不会被包含在响应中。

对于 Go 语言的 Gin 框架,处理默认值的方式可能会有所不同。Gin 使用 Go 的标准库 encoding/json 来序列化和反序列化 JSON 数据。在 Go 中,如果你想要在序列化 JSON 时忽略零值或空值,你可以使用结构体标签。

例如,你可以定义一个 Go 结构体并使用 omitempty 标签:

type Item struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Price float64 `json:"price"`
Tax *float64 `json:"tax,omitempty"` // 使用指针来允许空值
}

在这个例子中,如果 DescriptionTax 的值为它们类型的零值(对于 string 是空字符串,对于指针是 nil),则在序列化为 JSON 时这些字段将被忽略。

请注意,这种方式依赖于字段的零值。如果字段的零值是你想要传递的有效值,那么你可能需要使用指针或自定义的序列化逻辑来处理这种情况。

golang 剔除空值

在 Go 中,要剔除空值而非有效的零值,你可以使用指针类型来表示可选的字段。当一个指针字段为 nil 时,它可以被视为未设置或空值,并且可以在 JSON 序列化时被省略掉。这样做可以区分字段未被设置(nil)和字段设置为零值(比如 0false"" 等)的情况。

例如,下面是一个 Go 结构体,它使用指针类型来允许某些字段在序列化为 JSON 时被忽略:

package main

import (
"encoding/json"
"fmt"
)

type Item struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Price float64 `json:"price"`
Tax *float64 `json:"tax,omitempty"`
}

func main() {
// 示例:Description 和 Tax 字段未被设置,将被忽略
noDescriptionAndTax := Item{
Name: "Gadget",
Price: 99.99,
}

// 示例:Description 被设置为空字符串,Tax 被设置为 0.0,将被序列化
emptyDescriptionAndZeroTax := Item{
Name: "Widget",
Description: new(string), // 空字符串
Price: 49.99,
Tax: new(float64), // 0.0
}

// 序列化时剔除空值
jsonNoDesc, _ := json.Marshal(noDescriptionAndTax)
fmt.Println(string(jsonNoDesc)) // 输出:{"name":"Gadget","price":99.99}

// 序列化时包含有效的零值
jsonEmptyDesc, _ := json.Marshal(emptyDescriptionAndZeroTax)
fmt.Println(string(jsonEmptyDesc)) // 输出:{"name":"Widget","description":"","price":49.99,"tax":0}
}

在这个例子中:

  • DescriptionTax 字段是指针类型。
  • 如果 DescriptionTax 字段为 nil,它们将在 JSON 序列化时被忽略。
  • 如果 DescriptionTax 被设置为它们的零值(空字符串和 0.0),它们将被序列化并包含在 JSON 中,因为它们是指针指向的实际值。

omitempty 标签在 JSON 标签中用来指示,如果字段的值是空(nil),那么在序列化 JSON 时应该忽略该字段。这允许你区分字段未被设置和字段设置为零值的情况。

response_model_include/response_model_exclude

在 FastAPI 中,response_model_includeresponse_model_exclude 参数用于控制响应模型中包含或排除的字段。这两个参数可以在路径操作装饰器中设置,以便在返回响应时,只包含或排除特定的字段。

response_model_include 参数允许你指定一个字段集合,只有这些字段会被包含在响应中。不在这个集合中的字段将不会被包含在响应数据中。

在你提供的代码示例中,有两个路由处理函数:

  1. read_item_name:它使用 response_model_include 来指定只包含 namedescription 字段在响应中。
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
return items[item_id]

当你访问这个路由时,例如 /items/foo/name,即使 Item 模型中定义了 namedescriptionpricetax 四个字段,返回的 JSON 响应体将只包含 namedescription 字段。对于 foo 这个示例,由于 description 没有在字典中设置,所以最终响应将只包含 name 字段。

  1. read_item_public_data:它使用 response_model_exclude 来指定排除 tax 字段在响应中。
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
return items[item_id]

当你访问这个路由时,例如 /items/foo/public,返回的 JSON 响应体将包含 Item 模型中定义的除 tax 外的所有字段,即 namedescriptionprice

这种方式特别有用,因为它允许你根据不同的 API 端点需求,重用同一个 Pydantic 模型,同时控制响应数据的粒度。

from

在 FastAPI 中,要处理 HTML 表单数据,你可以使用 Form 类来定义表单字段。Form 是一个特殊的类,它会告诉 FastAPI 从表单的数据中获取信息,而不是从 JSON 中获取。这对于接收表单字段非常有用,比如用户登录或注册场景。

首先,要使用 Form,你需要从 fastapi 导入它:

from fastapi import FastAPI, Form

然后,你可以在路径操作函数中定义表单参数,如下所示:

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}

在上面的示例中,Form 的使用与 QueryPathBody 类似,它们都是 FastAPI 的依赖注入系统的一部分。使用 Form(...) 表示该字段是必填的。如果你想要一个可选字段,可以使用 None 作为默认值,例如 Form(None)

当你向这个 /login/ 端点发送一个 application/x-www-form-urlencoded 类型的 POST 请求时,FastAPI 会从表单数据中获取 usernamepassword 字段的值,然后返回一个包含 username 的 JSON 响应。

为了使上述示例工作,你需要确保你的客户端发送的是表单数据,而不是 JSON。在 HTML 中,这通常意味着使用一个 <form> 标签,并且其 enctype 属性被设置为 application/x-www-form-urlencoded

<form method="post" action="/login/" enctype="application/x-www-form-urlencoded">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="submit" />
</form>

如果你使用的是像 curl 这样的命令行工具,你需要确保数据以正确的格式发送:

curl -X POST "http://127.0.0.1:8000/login/" -H "Content-Type: application/x-www-form-urlencoded" -d "username=johndoe&password=secret"

这样,FastAPI 就可以正确处理表单数据了。

fastapi 保活

在 FastAPI 或任何其他 Python web 框架中,建立一个守护进程来确保应用程序持续运行通常不是应用程序本身的职责。相反,这通常是由外部的进程管理器或服务管理器来处理的。这些工具可以监控你的 FastAPI 应用程序,并在它崩溃或关闭时重新启动它。

以下是一些常用的进程管理器和服务管理器:

  1. systemd(适用于 Linux 系统):

    • 你可以将你的 FastAPI 应用程序配置为 systemd 服务。这样,systemd 就可以在应用程序崩溃时自动重启它,并且可以在系统启动时自动启动应用程序。
  2. Supervisor:

    • Supervisor 是一个流行的 Python 进程管理工具,它可以用来启动、重启和管理应用程序。你可以配置 supervisor 来监控你的 FastAPI 应用程序,并在必要时重启。
  3. Docker:

    • 如果你使用 Docker 容器来部署你的 FastAPI 应用程序,你可以利用 Docker 的重启策略来保持应用程序的运行。例如,你可以在 docker-compose 文件中设置restart: always来确保容器始终运行。
  4. Kubernetes:

    • 在 Kubernetes 集群中部署时,它会自动处理容器的生命周期,包括在失败时重启容器。

如果你想要在 Linux 系统上使用 systemd 来管理你的 FastAPI 应用程序,你可以创建一个 systemd 服务文件,例如/etc/systemd/system/myfastapi.service,内容可能如下:

[Unit]
Description=My FastAPI application
After=network.target

[Service]
User=myuser
Group=myusergroup
WorkingDirectory=/path/to/your/application
ExecStart=/path/to/your/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
Restart=always

[Install]
WantedBy=multi-user.target

在这个配置文件中,Restart=always指令告诉 systemd 在应用程序崩溃时自动重启它。

配置完成后,你可以使用以下命令来启动、停止、重启和查看服务状态:

sudo systemctl start myfastapi.service
sudo systemctl stop myfastapi.service
sudo systemctl restart myfastapi.service
sudo systemctl status myfastapi.service

要使服务在系统启动时自动启动,请使用:

sudo systemctl enable myfastapi.service

请记住,这些方法不是由 FastAPI 内部提供的,而是依赖于你的部署环境和操作系统。在生产环境中,始终建议使用这样的进程管理器来确保服务的可靠性和稳定性。

不用 selfdestruct 用什么?

在 Solidity 中,如果你希望避免使用 selfdestruct,那么你可以考虑实现一个所谓的“暂停模式”或“升级模式”,这些模式可以让你停止合约的操作或者迁移到新的合约实例。下面是一些替代 selfdestruct 的策略:

暂停模式 (Pausable)

通过实现可暂停的合约,你可以在发现漏洞或需要紧急停止合约操作时,暂停合约的关键功能。OpenZeppelin 提供了一个 Pausable 合约,你可以在自己的合约中继承它来轻松实现这一功能。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/Pausable.sol";

contract MyContract is Pausable {
// ...

function criticalFunction() public whenNotPaused {
// ...
}

function pause() public onlyOwner {
_pause();
}

function unpause() public onlyOwner {
_unpause();
}
}

在这个例子中,criticalFunction 只能在合约未暂停时调用,而 pause()unpause() 函数可以由合约的所有者调用来控制合约的暂停状态。

升级模式 (Upgradeable Contracts)

如果你想要修改合约的逻辑或修复漏洞,可以使用可升级的合约模式。在这种模式下,你的合约逻辑被放在一个单独的合约中(通常称为逻辑合约),而状态和代理逻辑则存储在另一个合约中(代理合约)。通过更改代理合约指向的逻辑合约地址,你可以“升级”合约的逻辑。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

contract LogicContractV1 {
// Initial logic contract implementation
}

contract LogicContractV2 {
// New logic contract implementation with a fix or new features
}

使用 OpenZeppelin 的代理合约库可以简化升级过程。

销毁模式 (Self-Destruct Replacement)

如果你确实需要一种方法来“销毁”合约,你可以移除合约的所有关键功能,这样用户就不能再与合约交互了。这可以通过将合约的关键函数修改为仅在特定条件下可用来实现。

contract MyContract {
address public owner;
bool public isDeactivated = false;

modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}

modifier isActive() {
require(!isDeactivated, "Contract is deactivated");
_;
}

function deactivateContract() public onlyOwner {
isDeactivated = true;
}

function criticalFunction() public isActive {
// ...
}
}

在这个例子中,合约所有者可以通过调用 deactivateContract 函数来停止所有关键操作。这样,合约虽然在技术上仍然存在于区块链上,但实际上已经被“销毁”,因为它不再有任何功能。

使用这些策略可以更安全地管理智能合约,因为它们不会突然移除合约,而是提供了一种更平滑的过渡或停止操作的方式。在实际应用中,你应该根据你的具体需求和场景选择合适的策略。

状态变量遮蔽

在 Solidity 中,状态变量遮蔽(Shadowing)是指在派生合约中声明一个与基类中同名的状态变量。这会导致基类中的状态变量被“遮蔽”,即在派生合约的上下文中,访问这个同名的变量时,将访问到派生合约中的变量,而不是基类中的变量。

遮蔽是一个潜在的错误来源,因为它可能会无意中导致基类的状态被隐藏,从而使得基类的状态变量难以被访问或者意外地不被使用。Solidity 编译器在发现这种情况时会发出警告,以提醒开发者注意这可能是一个错误。

为了避免状态变量遮蔽,Solidity 0.6.0(及以后的版本)中引入了一个编译器检查,如果派生合约中声明了与基类中同名的状态变量,编译器将报错。这意味着您不能在派生合约中声明一个与基类中同名的状态变量,除非您有意覆盖(override)一个变量。但是,Solidity 目前不支持状态变量的覆盖。

这是一个例子,展示了状态变量遮蔽:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Base {
uint public x = 1;
}

contract Derived is Base {
uint public x = 2; // 这会在Solidity 0.6.0及以后的版本中引起编译器错误
}

在这个例子中,Derived 合约试图声明一个同名的状态变量 x,这会导致编译器报错,因为它遮蔽了基类 Base 中的 x

为了避免这个问题,您应该使用不同的变量名,或者如果您需要扩展基类变量的行为,您可以使用函数来实现:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Base {
uint private _x = 1; // 私有变量,不会被派生合约遮蔽

function getX() public view returns (uint) {
return _x;
}

function setX(uint newX) public {
_x = newX;
}
}

contract Derived is Base {
function setX(uint newX) public {
super.setX(newX + 1); // 调用基类的 setX,并增加新的逻辑
}
}

在这个改进的例子中,Derived 合约通过增加新的逻辑来扩展了基类 Base 中的 setX 函数的行为,而不是尝试声明一个同名的状态变量。

一个点和两个点的区别在于,一个点是从当前模块开始找,两个点是从当前模块的父模块开始找

是的,您的理解是正确的。在 Python 中使用相对导入时,一个点(.)和两个点(..)代表不同的导入起点:

  • .(单个点):表示从当前模块的目录开始寻找目标模块。它用来导入同一目录下的模块或包。
  • ..(两个点):表示从当前模块的父目录开始寻找目标模块。它用来导入上一级目录(父目录)中的模块或包。

这种导入方式是相对于当前文件的位置,并且只能在包内部使用。这里有一个简化的示例来进一步解释这一点:

假设你有以下的目录结构:

my_package/

├── __init__.py
├── module1.py
└── subpackage/
├── __init__.py
└── module2.py

如果你在 module2.py 中需要导入 module1.py 中定义的某些内容,你可以这样做:

# 在 module2.py 文件中
from .. import module1

这里的 .. 指的是从 subpackage 目录跳出到 my_package 目录,然后导入 module1

相对导入是基于当前模块的 __name__ 属性来计算的,所以它们只能用在包内部。如果你尝试在主脚本中使用相对导入,或者直接运行一个模块(而不是作为一个包的一部分),相对导入就会失败,因为此时模块的 __name__ 属性被设定为 "__main__",并没有提供足够的信息来解析相对路径。

Annotated[str | None, Header()] = None

在你提供的 FastAPI 代码示例中,Annotated 类型是从 typing 模块导入的,它被用来给类型注解添加元数据。这在 Python 3.9+ 中作为 PEP 593 的一部分被引入。在 FastAPI 中,这种用法特别有用,因为它允许你为依赖注入系统提供额外的信息。

下面是代码中 Annotated 的使用解释:

from typing import Annotated
from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/items/")
async def read_items(user_agent: Annotated[str | None, Header()] = None):
return {"User-Agent": user_agent}

在这个 read_items 函数中,参数 user_agent 使用了 Annotated 来指定类型注解,并附加了额外的元数据:

  • str | None:表示 user_agent 参数可以是一个字符串或者 None 类型。这里的 | 是 Python 3.10 引入的类型联合操作符,它是 Union[str, None] 的简化写法。
  • Header():这是 FastAPI 的一个类,用来告诉 FastAPI 这个参数应该从请求头中获取。当你使用 Header 作为参数的默认值时,FastAPI 会从请求头中提取对应的值。

Annotated 的第一个参数是类型注解,后面可以跟着任意数量的元数据注解。在这个例子中,Header() 是一个元数据注解,它指示 FastAPI 框架如何处理这个参数。

将这些组合起来,Annotated[str | None, Header()] = None 的意思是:

  • user_agent 参数的类型可以是 strNone
  • FastAPI 应该从请求头中获取 user_agent 参数的值。
  • 如果请求中没有提供 User-Agent 头部,那么 user_agent 参数的默认值将是 None

因此,当一个请求到达 /items/ 路径时,FastAPI 会查找 User-Agent 请求头,并将其值作为 user_agent 参数传递给 read_items 函数。如果请求中没有 User-Agent 头部,user_agent 将默认为 None

Header 为什么所有的框架都是转化成中划线

在 HTTP 标准中,头字段名称是不区分大小写的,并且通常使用连字符(-)来分隔单词,而不是下划线(_)。这是由 HTTP 规范定义的,而不是由 Web 框架决定的。例如,常见的头字段如 Content-TypeUser-AgentAccept-Encoding 都遵循这种命名约定。

这种约定背后的原因部分是历史原因。在早期的互联网协议设计中,连字符用于头字段名称,这一传统被保留了下来。此外,某些系统和软件(如 CGI(Common Gateway Interface))可能会将下划线与环境变量混淆,因为环境变量和某些编程语言(如 C 语言)的常量通常使用下划线而不是连字符。

由于 Python 中的变量名不能包含连字符(因为它被解释为减号),所以在 Python 代码中通常会使用下划线。然而,当这些变量名需要映射到 HTTP 头字段时,Web 框架如 FastAPI 会自动将下划线转换为连字符,以确保遵守 HTTP 标准。

例如,在 FastAPI 中,如果你有一个参数名为 user_agent,FastAPI 会在内部将其转换为 User-Agent,因为这是正确的 HTTP 头字段名称。这样的自动转换使得开发者可以在 Python 代码中使用习惯的命名规则,同时确保生成的 HTTP 响应符合标准。

序列化与反序列化

在 FastAPI 和许多其他现代 Web 框架中,序列化和反序列化是处理数据转换的重要部分。序列化通常指的是将复杂数据类型(如 Python 的类实例)转换为可以被存储或传输的格式(如 JSON)。反序列化是相反的过程,它将存储或传输的格式(如 JSON)转换回原始的数据类型。

序列化(Serialization)

序列化是将数据结构或对象状态转换成可存储或可传输的形式的过程。在 Web 开发中,序列化通常指的是将数据结构转换为一种格式,如 JSON,以便可以通过 HTTP 请求发送。

在 FastAPI 中,jsonable_encoder 是一个实用函数,用于将数据转换成可以被 JSON 序列化的格式。这个函数处理了许多常见的数据类型转换,包括:

  • 将日期和时间转换为 ISO 格式的字符串。
  • UUID 类型转换为字符串。
  • 将数据库模型转换为字典。
  • 确保复杂数据类型(如 Pydantic 模型)可以被转换为 JSON 格式。

例如:

from fastapi.encoders import jsonable_encoder
from datetime import datetime
from pydantic import BaseModel

class Item(BaseModel):
title: str
timestamp: datetime

item = Item(title="Hello", timestamp=datetime.utcnow())

# 序列化 Pydantic 模型实例
json_compatible_item_data = jsonable_encoder(item)

反序列化(Deserialization)

反序列化是将数据从存储或传输的格式(如 JSON)转换回其原始的数据结构的过程。在 Web 开发中,这通常涉及将 JSON 请求体转换为 Python 数据结构,如字典或对象。

在 FastAPI 中,当你使用 Pydantic 模型定义你的请求体时,FastAPI 将自动处理反序列化。当发送一个请求到你的 FastAPI 应用时,FastAPI 会读取请求体中的 JSON 数据,并将其转换成你在函数参数中定义的 Pydantic 模型实例。

例如:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
title: str
description: str

@app.post("/items/")
async def create_item(item: Item):
return item

在这个例子中,当你向 /items/ 路径发送一个 POST 请求,包含 JSON 请求体,FastAPI 会将请求体中的 JSON 数据转换成 Item 类的实例。这就是反序列化的过程。

总结一下,序列化和反序列化是 Web API 中数据交换的核心,它们允许复杂的数据结构在客户端和服务器之间安全、有效地传输。FastAPI 提供了工具来简化这些过程,使得处理 HTTP 请求和响应更加容易。

反序列化转化成实例,与 golang 的序列化类似吗?

是的,FastAPI 中的反序列化过程与 Go 语言(Golang)中的序列化过程有一定的相似之处,尽管它们是用不同的语言实现的,并且具体的库和工具会有所不同。

在 Go 语言中,序列化(通常称为编组,Marshalling)和反序列化(解组,Unmarshalling)通常是通过标准库中的 encoding/json 包来实现的。当你序列化一个 Go 结构体为 JSON 时,你会使用 json.Marshal 函数。而当你从 JSON 反序列化为 Go 结构体时,你会使用 json.Unmarshal 函数。

Go 语言中的序列化示例:

package main

import (
"encoding/json"
"fmt"
"log"
)

type Item struct {
Title string `json:"title"`
Description string `json:"description"`
}

func main() {
item := Item{
Title: "An example item",
Description: "This is a description for the example item.",
}

jsonData, err := json.Marshal(item)
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", jsonData)
}

Go 语言中的反序列化示例:

package main

import (
"encoding/json"
"fmt"
"log"
)

type Item struct {
Title string `json:"title"`
Description string `json:"description"`
}

func main() {
jsonData := []byte(`{"title":"An example item","description":"This is a description for the example item."}`)

var item Item
err := json.Unmarshal(jsonData, &item)
if err != nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Printf("%+v\n", item)
}

在这两种语言中,序列化和反序列化的目标是相同的:将数据结构转换为 JSON 格式(序列化),以及将 JSON 格式转换回原始数据结构(反序列化)。不过,实现细节和使用的库会有所不同,但概念是通用的。

在 FastAPI 中,Pydantic 负责定义数据模型和自动处理 JSON 的序列化和反序列化。在 Go 中,结构体(struct)和 encoding/json 包负责同样的任务。在两种语言中,都可以通过标签(在 Go 中是字段标签,而在 Python/Pydantic 中是类型注解)来控制序列化和反序列化的行为。

为什么 golang 代码看上去比 python 复杂?

Go 语言(Golang)和 Python 在设计哲学、语法简洁性和类型系统等方面有显著的差异,这些差异可能会导致它们在代码的复杂性上有所不同。下面是一些可能导致 Go 代码看起来比 Python 复杂的原因:

  1. 静态类型系统:Go 是一种静态类型语言,这意味着变量的类型在编译时就已经确定,并且不能改变。这通常需要更多的类型声明和转换代码。Python 是动态类型语言,类型通常在运行时确定,因此在编写代码时不需要指定类型,这使得代码更简洁。

  2. 错误处理:Go 语言采取了显式的错误处理策略,这意味着几乎所有可能出错的函数都会返回一个错误值,而调用者必须检查这个错误值。这导致了大量的错误检查代码。相比之下,Python 使用异常处理机制,通常代码中不需要显式地检查错误,而是通过异常捕获来处理错误情况。

  3. 语法糖:Python 提供了大量的语法糖,使得代码更加简洁易读。例如,列表推导式、生成器表达式、动态属性访问等。Go 的设计哲学倾向于简单和明确,它没有提供太多的语法糖。

  4. 内置功能:Python 有一个庞大的标准库和第三方库,这些库通常提供了简洁的 API 来完成各种任务。Go 也有标准库,但相对较小,且在某些情况下可能不得不编写更多的代码来完成同样的任务。

  5. 泛型:直到 Go 1.18 版本之前,Go 语言没有泛型。这意味着在需要类型抽象的地方,你可能不得不使用接口(interface)和类型断言(type assertion),这些都会增加代码的复杂性。Python 是动态类型的,所以它天然就支持泛型编程。

  6. 代码格式:Go 有一个强制的代码格式标准(通过 gofmt 工具实现),这意味着所有 Go 代码在格式上看起来都非常相似,但也可能因为严格的格式要求而看起来更加繁琐。Python 也有 PEP 8 代码风格指南,但它更加灵活。

尽管如此,Go 语言的设计哲学是为了提供简单、高效和可靠的编程环境,特别是在并发编程和系统编程方面。Go 的代码可能在某些情况下看起来比 Python 更加复杂,但它的简单性和性能优势在大型项目和高性能应用中非常重要。此外,对于熟悉 Go 的开发者来说,Go 代码的可读性和可维护性通常是非常高的。

golang 性能真的比 python 高吗?为什么 fastapi 说有 golang 和 nodejs 的性能

是的,通常情况下,Go 语言(Golang)的性能确实比 Python 高。这主要是由于以下几个原因:

  1. 编译语言 vs 解释语言:Go 是一种编译语言,这意味着 Go 代码在执行前会被编译成机器码,这通常可以提供更优的性能和更快的执行速度。而 Python 是一种解释语言,它的代码通常在运行时逐行解释执行,这会带来额外的性能开销。

  2. 类型系统:Go 是静态类型的,这有助于编译器优化代码执行。Python 是动态类型的,类型检查在运行时进行,这会增加运行时开销。

  3. 并发模型:Go 的并发模型是它的一个核心特性。Go 协程(Goroutines)是轻量级的线程,它们的调度是由 Go 运行时管理的,这使得在 Go 中实现高效的并发程序变得非常简单。Python 通过线程和进程支持并发,但受全局解释器锁(GIL)的限制,纯 Python 代码在多线程中不能有效地利用多核处理器。

  4. 内存管理:Go 有一个内置的垃圾回收机制,它旨在优化内存使用,并减少内存泄漏的风险。虽然 Python 也有垃圾回收,但 Go 的垃圾回收器经过了调优,以适应并发执行和低延迟的需求。

当 FastAPI 说它具有 Go 和 Node.js 的性能时,它是在强调 FastAPI 框架的高效性能特点。FastAPI 本身是用 Python 编写的,但它使用了 Starlette(用于 Web 服务)和 Pydantic(用于数据验证和序列化),这两个库都是异步的并且高度优化的。FastAPI 的异步支持意味着它可以非常高效地处理并发请求,这在某些情况下可以接近或匹敌 Go 和 Node.js 的性能。

此外,FastAPI 通常部署在 ASGI 服务器上,如 Uvicorn,这是一个异步服务器网关接口,能够提供高性能的异步通信。这样的部署方式使得 FastAPI 能够利用现代硬件的并行处理能力,从而提供更好的性能。

总结来说,尽管纯 Python 代码的性能可能不如 Go,FastAPI 通过异步编程和高效的底层库,提供了优秀的性能表现,在某些用例中可以与 Go 和 Node.js 竞争。然而,对于 CPU 密集型任务或需要高度优化的系统级工作,Go 通常会提供更好的性能。

List[Item] = field(default_factory=list)

https://fastapi.tiangolo.com/zh/advanced/dataclasses/#_2

在 Python 的 dataclasses 模块中,@dataclass 装饰器用于快速创建数据类,它自动添加特殊方法,如 __init__()__repr__(),基于类中定义的字段。

在你给出的代码示例中:

from dataclasses import dataclass, field
from typing import List, Union

@dataclass
class Item:
name: str
description: Union[str, None] = None

@dataclass
class Author:
name: str
items: List[Item] = field(default_factory=list)

Author 类有两个字段:nameitemsname 是一个字符串类型的字段,而 items 是一个 Item 类型的列表。

对于 items 字段,field(default_factory=list) 用于指定该字段的默认值。由于我们不能直接将可变类型(如列表或字典)作为类属性的默认值(因为这会在所有实例之间共享同一个列表),default_factory 参数接受一个无参数的可调用对象,每次需要一个默认值时,都会调用这个可调用对象来创建一个新的列表。

因此,items: List[Item] = field(default_factory=list) 的意思是:每个 Author 实例的 items 字段都将默认为一个新的空列表。这确保了每个 Author 实例都有自己独立的 items 列表,而不是所有实例共享同一个列表。

总结来说,field(default_factory=list) 确保了每次创建 Author 类的新实例时,items 都会被初始化为一个新的空列表。

fastapi 自定义中间件

在 FastAPI 中,中间件是一个函数,它在每个请求被处理之前和每个响应返回给客户端之前运行。你可以使用中间件来执行各种任务,比如处理跨源资源共享(CORS),添加安全头,进行请求日志记录,认证等。

要在 FastAPI 应用中添加自定义中间件,你需要使用 @app.middleware("http") 装饰器。下面是一个简单的例子,展示了如何创建和注册一个自定义中间件:

from fastapi import FastAPI, Request
from starlette.responses import Response

app = FastAPI()

# 定义中间件
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# 请求处理之前的操作
response = await call_next(request)
# 响应处理之后的操作
response.headers["X-Process-Time"] = "Process time header"
return response

# 创建一个路由
@app.get("/")
async def main():
return {"message": "Hello World"}

在这个例子中,自定义中间件 add_process_time_header 会在处理每个请求之前和之后运行。它使用 call_next 函数来调用后续的请求处理部分,并接收响应。在这个响应返回给客户端之前,中间件可以修改响应,比如在这个例子中,它添加了一个名为 X-Process-Time 的自定义响应头。

中间件函数 add_process_time_header 接收两个参数:

  • request:当前的请求对象。
  • call_next:一个函数,将当前的请求传递给下一个中间件或实际的请求处理器。

中间件可以是同步的或异步的,但是当你需要等待异步操作(如 await call_next(request))时,中间件应该是异步的。

在中间件中进行异常处理时,你需要确保所有的异常都被捕获并相应地处理,因为如果中间件中发生未捕获的异常,它可能会导致请求处理流程异常中断。

通过这种方式,你可以创建多个中间件,并且按照它们被添加的顺序执行它们。每个中间件都可以对请求和响应进行操作,为 FastAPI 应用提供强大的扩展性和灵活性。

模型 vs 字典

好的,来简化一下:

模型(Model)

  • 是一个类,定义数据的结构和行为。
  • 包括类型、验证规则和可能的默认值。
  • 在 Pydantic 中用于数据验证和自动文档生成。

字典(Dictionary)

  • 是 Python 的内置数据类型,存储键值对。
  • 键必须是唯一的,值可以是任何数据类型。
  • 没有内置的数据验证或自动文档。

区别

  • 模型提供类型安全和数据验证,字典不提供。
  • 模型可以序列化成字典,但模型本身包含更多信息和功能。
  • 字典更灵活,可以随时添加或删除键值对。模型的结构通常是固定的。