Flask or FastAPI? Python服务端初体验

1. 引言

最近由于工作需要,又去了解了一下简单的python服务搭建的相关工作,主要是为了自己开发的模型或者工具给同组的人使用。之前介绍的针对于数据科学研究比较友好的一个可以展示的前端框架Streamlit可以说是一个利器。不过,随着ChatGPT的流行,基于chat的服务越来越多了起来,streamlit有一个chat衍生物streamlit-chat,但是它能提供的只是一个简单的聊天功能,并不能具有更高级显示,例如支持markdown和流式输出等。因此,更加适合大模型前端的FastChat可能是更好的选择。

话说回来,前端只是一个展示的界面,而真正提供服务的,需要后端才行。严格意义上讲,后端都是提供数据服务的,也就是对数据进行增删查改的,本质是离不开的,但是现在不是和数据库链接,而是和模型或者模型服务链接了。

这时候,当我去搜索python的后端库的时候,最长用的中小应用web框架Flask出现在我面前。但是我感觉还是有点复杂,于是我想到了一个基于Starlette二次开发的FastAPI,也许更适合。(这里有他们之间的比较介绍,介绍1,介绍2,介绍3,介绍4)。当然,也有人指出,用Flask与FastAPI比较式不公平的,就像是比较苹果和橙汁哪个更甜一样。Flask作为通用web框架,应该和Starlette比。我关于这点也是认同的。但这更能说明,在一个需要快速开发而需求不是那么重的时候,FastAPI是一个更好的选择。(当然我也用Flask开发过多线程支持的服务,两者各有千秋。)

2. Hello world

正如任何语言的第一个案例一样,我们首先用一个非常简单的例子介绍使用FastAPI提供服务的。

第一步,准备代码app.py

from fastapi import FastAPI
from pydantic import BaseModel
from gpt import GPT

app = FastAPI()
model = GPT()

class Message(BaseModel):
    new_message: str
    role: str = ""
    args: dict = {}

@app.post('/gpt')
def gpt_endpoint(message: Message):
    new_message = message.new_message
    role = message.role
    args = message.args

    response = model.call(new_message=new_message, role=role, args=args)
    return response

从这段不足50行的代码里,凸显出了大部分的FastAPI的特性。可以看到主体app,自己的资源model以及一个数据类Message,一个post接口服务(名为/gpt),并做了一些操作,返回response。

这里的GPT是简化的类,其处理代码可以写到另一个文件中。

第二步,安装依赖库

pip install fastapi
pip install uvicorn
pip install pydantic

第三步,运行服务

uvicorn app:app --host 0.0.0.0 --port 8000

经历以上三步,一个FastAPI服务就搭建好了,访问地址例如:http://localhost:8000/gpt,如何快速的测试它呢?FastAPI自带了Docs,可以通过URL访问(http://localhost:8000/docs),如下图所示:
Flask or FastAPI? Python服务端初体验_第1张图片

如果代码中注释写的足够多的话,都不用另写手册了。

但是刚才的代码只是向我们简单的介绍了一下FastAPI的特性,如果我需要构建更加复杂的服务呢?下面我们通过2个实战例子更加全面的介绍FastAPI的使用。

3. 扩充实战

3.1 使用FastAPI提供增删查改的例子

假设我们正在开发一个简单的待办事项管理应用程序。我们希望实现以下功能:

  • 获取所有待办事项的列表
  • 创建一个新的待办事项
  • 更新特定待办事项的内容
  • 标记特定待办事项为已完成
  • 删除特定待办事项

我们可以使用FastAPI来实现这些功能。以下是使用不同类型装饰器定义的API端点的示例代码:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 待办事项数据模型
class TodoItem(BaseModel):
    id: int
    title: str
    completed: bool = False

# 模拟的待办事项存储
todo_items = []

# 获取所有待办事项的列表
@app.get("/todos")
def get_todo_list():
    return todo_items

# 创建一个新的待办事项
@app.post("/todos")
def create_todo_item(item: TodoItem):
    todo_items.append(item)
    return item

# 更新特定待办事项的内容
@app.put("/todos/{item_id}")
def update_todo_item(item_id: int, item: TodoItem):
    for i in range(len(todo_items)):
        if todo_items[i].id == item_id:
            todo_items[i] = item
            return item

# 标记特定待办事项为已完成
@app.patch("/todos/{item_id}")
def complete_todo_item(item_id: int):
    for item in todo_items:
        if item.id == item_id:
            item.completed = True
            return item

# 删除特定待办事项
@app.delete("/todos/{item_id}")
def delete_todo_item(item_id: int):
    for item in todo_items:
        if item.id == item_id:
            todo_items.remove(item)
            return {"message": "Item deleted"}


在上面的示例中,我们使用了以下不同类型的装饰器:

  1. @app.get(path: str):定义GET请求的端点。使用@app.get(“/todos”)装饰器定义了获取所有待办事项的列表的端点。这个端点不需要接收任何参数,直接返回待办事项列表。

  2. @app.post(path: str):定义POST请求的端点。使用@app.post(“/todos”)装饰器定义了创建新待办事项的端点。这个端点接收一个请求体参数item,它是一个TodoItem模型的实例,包含了待办事项的内容。在处理函数中,我们将新的待办事项添加到todo_items列表中,并返回添加的待办事项。

  3. @app.put(path: str):定义PUT请求的端点。使用@app.put(“/todos/{item_id}”)装饰器定义了更新待办事项的端点。这个端点接收一个路径参数item_id用于指定待办事项的ID,以及一个请求体参数item,它是一个TodoItem模型的实例,包含了待办事项的新内容。在处理函数中,我们遍历todo_items列表找到对应ID的待办事项,将其更新为新的内容,并返回更新后的待办事项。

  4. @app.patch(path: str):定义PATCH请求的端点。使用@app.patch(“/todos/{item_id}”)装饰器定义了标记待办事项为已完成的端点。这个端点接收一个路径参数item_id用于指定待办事项的ID。在处理函数中,我们遍历todo_items列表找到对应ID的待办事项,并将其标记为已完成(item.completed = True),然后返回已更新的待办事项。

  5. @app.delete(path: str):定义DELETE请求的端点。使用@app.delete(“/todos/{item_id}”)装饰器定义了删除待办事项的端点。这个端点接收一个路径参数item_id用于指定待办事项的ID。在处理函数中,我们遍历todo_items列表找到对应ID的待办事项,并将其从列表中删除,然后返回一个简单的消息表示删除成功。

  6. 这些装饰器允许我们根据HTTP方法(GET、POST、PUT、PATCH、DELETE)来定义不同类型的端点,并通过路径参数和请求体参数来接收和处理不同的数据。这样,我们可以使用统一的应用程序来处理各种操作,并根据RESTful API的原则设计我们的API。

这里,我们注意到PUT和PATCH方法是很相似的,都是用于更新的,但是两者有以下不同:

PUT请求用于完全替换(Replace)服务器上的资源或实体。当客户端发送一个PUT请求时,它需要提供完整的资源表示,包括要更新的所有字段。
如果资源不存在,则会创建一个新的资源;如果资源存在,则会完全替换(覆盖)现有资源的内容。
客户端通常应该提供完整的资源表示,即使只有部分字段发生更改。这意味着客户端需要提供所有字段,而不仅仅是要更改的字段。
PUT请求是幂等的,多次执行相同的PUT请求不会对资源产生额外的影响。

PATCH请求用于对服务器上的资源或实体进行部分更新。当客户端发送一个PATCH请求时,它只需要提供要更新的部分字段或属性。
PATCH请求可以用于增量更新资源的特定字段,而不需要发送整个资源的表示。这使得它适用于只更新部分字段的情况。
服务器根据客户端提供的更新内容,选择性地更新资源的相应字段。未提供的字段将保持不变。
PATCH请求可以是幂等的,但也可以是非幂等的,这取决于具体实现和使用情况。

在实际应用中,可以根据具体的业务需求和设计准则来选择使用PUT还是PATCH请求。如果要更新整个资源或实体,应该使用PUT请求。如果只需更新部分字段或属性,应该使用PATCH请求。

3.2 使用FastAPI提供GPU加载语言模型的例子

import uvicorn
import torch
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel

# 示例GPT模型类
class GPTModel:
    def __init__(self):
        # 这里是你的GPT模型的初始化代码
        # ...

    def generate_text(self, input_text):
        # 这里是生成文本的代码
        # ...

# 示例后台任务函数
def load_model(background_tasks):
    # 加载和初始化GPT模型
    model = GPTModel()
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.to(device)
    model.eval()

    # 将模型保存到FastAPI应用的state中
    app.state.model = model

# 示例请求模型的输入
class TextRequest(BaseModel):
    text: str

app = FastAPI()

@app.post("/generate")
def generate_text(request: TextRequest, background_tasks: BackgroundTasks):
    # 在后台任务中加载模型
    background_tasks.add_task(load_model, background_tasks)

    # 从应用程序状态中获取模型
    model = app.state.model

    # 在GPU上进行推理
    input_text = request.text
    generated_text = model.generate_text(input_text)

    # 返回生成的文本结果
    return {"generated_text": generated_text}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

4. 小结

本文主要简单的介绍了FastAPI的一些简单用法,方便快速搭建原型服务。

(随着ChatGPT等大模型逐渐渗透到我们的生活中,可能如此记录技术细节的博客愈发的没有用了。因为不会的东西ChatGPT可以交互式的给予指导,而不需要再这样看博客了。尽管这博客也离不开ChatGPT的指示,我还是看了一些相关博客,也自己亲手试了ChatGPT给的代码,多少增加一些可信度。)

你可能感兴趣的:(python,python,flask,fastapi)