在 AI 的帮助下,只花了 5 个小时就入门并上线了我的 Python 短网址应用(02)

14. 准备实战:URL 短网址应用 - 数据库先行

我的目标是用 FastAPI 写一个 URL 短网址应用。要实现这个,第一步就是得搞定数据存储。我需要一个地方来保存原始的长 URL 和我生成的短代码之间的对应关系。

AI 告诉我,虽然可以用简单的文件存储,但对于这种需要快速查找和可能扩展的应用,使用数据库是更标准、更高效的做法。对于初学和开发阶段,SQLite 是个不错的选择,因为它就是一个文件,不需要单独安装数据库服务,Python 也内置了支持。但 AI 也提醒我,实际生产环境可能需要更强大的数据库,比如 PostgreSQL 或 MySQL。

为了更方便地用 Python 操作数据库,AI 推荐我学习 SQLAlchemy。它是一个非常流行的 Python ORM (Object-Relational Mapper)。ORM 的作用就像一个翻译官,让我可以用 Python 的类和对象来操作数据库表和数据,而不需要(或者很少需要)手写 SQL 语句。这对于熟悉面向对象编程的我来说,应该会比较自然。

By author: ORM 技术:Object-Relational Mapping,把关系数据库的表结构映射到对象上。

任务 14:安装 SQLAlchemy 并定义数据模型

  1. 确保在虚拟环境里:我需要先激活我的 fastapi-project.venv 环境。

    source .venv/bin/activate
  2. 安装 SQLAlchemy:SQLAlchemy 需要单独安装。因为 FastAPI 是基于异步的,AI 建议我安装支持异步的版本(尽管基础 ORM 用法同步异步差别不大)。同时,由于我们选择了 SQLite,还需要安装对应的数据库驱动 aiosqlite

    pip install sqlalchemy "aiosqlite"

    (记得更新 requirements.txtpip freeze > requirements.txt)

  3. 定义数据模型 (models.py):就像在 Node.js 里用 Mongoose 或 Sequelize 定义 Schema 一样,我要用 SQLAlchemy 定义一个映射到数据库表的 Python 类。我在项目根目录下创建一个新文件 models.py

    # models.py
    from sqlalchemy import Column, Integer, String, DateTime
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.sql import func # 用于获取数据库服务器时间
    
    # 创建一个 Base 类,我们的模型类将继承它
    Base = declarative_base()
    
    class URLMap(Base):
        __tablename__ = "url_maps" # 指定映射的数据库表名
    
        # 定义表的列 (字段)
        id = Column(Integer, primary_key=True, index=True) # 主键,自增,加索引
        short_code = Column(String, unique=True, index=True) # 短码,唯一,加索引
        long_url = Column(String, index=True) # 原始长 URL,加索引方便查找
        # default=func.now() 让数据库在插入时自动设置当前时间
        created_at = Column(DateTime(timezone=True), server_default=func.now())
        # onupdate=func.now() 让数据库在更新时自动更新时间
        updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
    
        # 这个 __repr__ 方法是为了方便打印对象时查看信息,可选
        def __repr__(self):
            return f"<URLMap(short_code='{self.short_code}', long_url='{self.long_url}')>"
  4. 创建数据库和表 (database.py):我需要设置数据库连接,并提供一个创建数据库表的方法。创建一个 database.py 文件:

    # database.py
    from sqlalchemy import create_engine
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker
    
    # SQLite 数据库文件将保存在项目根目录下的 shortener.db 文件里
    SQLALCHEMY_DATABASE_URL = "sqlite:///./shortener.db"
    # 如果用 PostgreSQL: "postgresql://user:password@postgresserver/db"
    
    # 创建 SQLAlchemy 引擎
    # connect_args  SQLite 特有的,建议加上以提高并发性能
    engine = create_engine(
        SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
    )
    
    # 创建一个 SessionLocal 类,每个实例将是一个数据库会话
    # autocommit=False  autoflush=False 是常用的设置
    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    
    # 重新导入在 models.py 中定义的 Base
    # (也可以直接从 models 导入 Base = models.Base)
    Base = declarative_base()
    
    def create_db_and_tables():
        # 这会查找所有继承自 Base 的类,并在数据库中创建对应的表
        # 如果表已存在,则不会重复创建
        Base.metadata.create_all(bind=engine)
    
    # 我们可以在应用启动时调用 create_db_and_tables()
  5. 在主应用启动时创建表:修改 main.py,在应用启动时调用创建表的函数。

    # main.py
    from fastapi import FastAPI
    # ... 其他导入 ...
    from database import engine, SessionLocal # 导入数据库设置
    import models # 导入模型定义
    
    # 在应用启动时创建数据库表
    # 注意:这里需要确保 models.py 中的 Base 被正确关联
    # 更稳妥的方式是在 database.py 中导入并绑定 models.Base
    # 或者直接在这里调用 models.Base.metadata.create_all(bind=engine)
    models.Base.metadata.create_all(bind=engine)
    
    app = FastAPI()
    
    # ... 路由定义保持不变 ...

    修正:确保 create_db_and_tablesmodels.Base.metadata.create_all 被正确调用。直接在 main.py 调用 models.Base.metadata.create_all(bind=engine) 更直接。

我的小练习: 运行 python main.py (或者直接运行 uvicorn),观察项目目录下是否生成了 shortener.db 文件。你可以使用 DB Browser for SQLite 等工具打开看看里面是否创建了 url_maps 表。

(感受:SQLAlchemy 的概念比我想象的要多,engine, sessionmaker, Base 这些都需要理解。但是用 Python 类来定义表结构确实很直观,有点像定义 TypeScript 接口然后 ORM 自动处理数据库的感觉。create_all 这个函数很方便,不用手动写 CREATE TABLE 语句了。)

问题 1: pip install sqlalchemy "aiosqlite" 为什么有的有引号,有的没有呢

pip install 命令中,引号(单引号 ' 或双引号 ")的主要作用是防止 shell(命令行解释器,如 Bash 或 Zsh)对包名或版本说明符进行特殊处理

具体来说:

  1. 简单包名(如 sqlalchemy

    • 这些包名通常只包含字母、数字、下划线和连字符,这些字符对于 shell 来说没有特殊含义。
    • 因此,可以直接写 sqlalchemy,shell 会将其作为一个单独的参数传递给 pip 命令。
    • 加上引号(如 "sqlalchemy")也可以,但通常没有必要。shell 会移除引号,然后将 sqlalchemy 传递给 pip
  2. 包含特殊字符的包说明符(如 "aiosqlite""requests>=2.20""uvicorn[standard]"

    • 对于 "aiosqlite":这个包名本身很简单,不包含特殊字符,所以引号不是必需的pip install sqlalchemy aiosqlitepip install sqlalchemy "aiosqlite" 的效果是一样的。加上引号可能是个人习惯,或者是在一个包含其他需要引号的包的命令中为了保持一致性。
    • 版本控制符(>, <, =, ~:像 requests>=2.20 这样的写法包含了 >=。在某些 shell 中,> 有重定向的特殊含义。为了确保 shell 不会尝试进行重定向,而是将整个 requests>=2.20 字符串传递给 pip,需要加上引号,例如 pip install "requests>=2.20"
    • Extras(方括号 []:像 uvicorn[standard] 这样的写法包含了方括号 []。在 shell 中,方括号通常用于通配符(globbing)。为了防止 shell 尝试查找匹配 uvicorn[standard] 模式的文件名,需要加上引号,例如 pip install "uvicorn[standard]"
    • URL 或路径:如果从 URL 或本地路径安装,其中可能包含 &, ; 等特殊字符,也需要引号。

总结:

  • 对于只包含字母数字和 -/_ 的简单包名,引号不是必需的
  • 对于包含版本说明符 (>=, ==, < 等) 或 extras ([]) 的包强烈建议使用引号,以防止 shell 错误地解释这些特殊字符。
  • 在你的例子 pip install sqlalchemy "aiosqlite" 中,给 aiosqlite 加引号是可选的,但没有坏处。

15. 核心逻辑与 CRUD 操作

定义好了数据库模型,接下来就是实现短网址应用的核心功能:

  1. 生成短码 (Create):接收一个长 URL,生成一个唯一的短代码。
  2. 存储映射 (Create):将短代码和长 URL 的对应关系存入数据库。
  3. 查找长 URL (Read):根据短代码从数据库中查找对应的长 URL。

我们需要在数据库会话 (Session) 中执行这些操作。

任务 15:实现数据库的增查操作 (CRUD 中的 C 和 R)

  1. 创建 crud.py 文件:为了将数据库操作逻辑和 API 路由逻辑分开,我创建一个 crud.py 文件来存放这些函数。

    # crud.py
    from sqlalchemy.orm import Session
    import models
    import random
    import string
    
    # 生成随机短码的函数 (简单实现)
    def generate_short_code(length: int = 6) -> str:
        # 使用大小写字母和数字生成随机字符串
        characters = string.ascii_letters + string.digits
        return ''.join(random.choice(characters) for _ in range(length))
    
    # 根据短码获取 URL 映射记录
    def get_url_map_by_short_code(db: Session, short_code: str):
        # db.query(models.URLMap) 开始一个查询
        # .filter(models.URLMap.short_code == short_code) 添加过滤条件
        # .first() 返回第一个匹配的记录,如果没有则返回 None
        return db.query(models.URLMap).filter(models.URLMap.short_code == short_code).first()
    
    # 根据长 URL 获取 URL 映射记录 (防止重复创建)
    def get_url_map_by_long_url(db: Session, long_url: str):
        return db.query(models.URLMap).filter(models.URLMap.long_url == long_url).first()
    
    # 创建新的 URL 映射记录
    def create_url_map(db: Session, long_url: str):
        # 循环生成短码,直到找到一个数据库里不存在的
        while True:
            short_code = generate_short_code()
            db_url_map = get_url_map_by_short_code(db, short_code)
            if not db_url_map:
                break # 找到未使用的短码
    
        # 创建模型实例
        db_url_map = models.URLMap(short_code=short_code, long_url=long_url)
        # 添加到会话
        db.add(db_url_map)
        # 提交事务到数据库
        db.commit()
        # 刷新实例,获取数据库生成的值 (比如 id, created_at)
        db.refresh(db_url_map)
        return db_url_map
    
  2. 理解数据库会话 (Session):AI 解释说,Session 对象是与数据库交互的主要接口。所有的数据库操作(添加、查询、删除)都是通过 Session 进行的。你需要先 db.add() 把对象添加到会话"暂存区",然后 db.commit() 把这些更改一次性写入数据库。如果中间出错,可以 db.rollback() 回滚。

我的小练习:crud.py 中添加一个函数 get_all_url_maps(db: Session, skip: int = 0, limit: int = 100),用于获取数据库中所有的 URL 映射记录(支持分页)。(提示:使用 db.query(...).offset(skip).limit(limit).all()

(感受:把数据库操作封装在 crud.py 里是好习惯,保持了代码分离。SQLAlchemy 的查询语法 .query().filter().first() 读起来很流畅。生成唯一短码的逻辑需要考虑碰撞,这里的简单循环重试在量小的时候可行,量大时可能需要更优化的策略。db.add, db.commit, db.refresh 这套流程需要记住。)

16. FastAPI 路由与依赖注入

现在有了数据库模型和 CRUD 操作函数,是时候把它们连接到 FastAPI 的 API 端点了。我们需要创建两个主要的端点:

  • POST /shorten:接收长 URL,返回短 URL。
  • GET /{short_code}:接收短代码,重定向到原始长 URL。

这里要用到 FastAPI 的一个核心特性:依赖注入 (Dependency Injection)。简单来说,就是让 FastAPI 自动为我们的路径操作函数提供所需的依赖项(比如数据库会话 Session)。

任务 16:创建 API 端点并注入数据库会话

  1. 定义 Pydantic 模型 (schemas.py):为了清晰定义 API 的输入输出数据结构,并与数据库模型 (ORM model) 分离,通常会创建 Pydantic 模型。我创建一个 schemas.py 文件:

    # schemas.py
    from pydantic import BaseModel, HttpUrl
    from datetime import datetime
    from typing import Optional
    
    # 请求体模型:用于 POST /shorten
    class URLCreate(BaseModel):
        long_url: HttpUrl # Pydantic 自带的 HttpUrl 类型可以做 URL 校验
    
    # 基础响应模型 (用于读取)
    class URLMapBase(BaseModel):
        short_code: str
        long_url: HttpUrl
    
    # 完整的响应模型 (可能包含数据库生成的字段)
    class URLMap(URLMapBase):
        id: int
        created_at: datetime
        updated_at: datetime
    
        # 这个配置让 Pydantic 模型可以从 ORM 对象 (数据库记录) 创建
        class Config:
            orm_mode = True
            #  Pydantic V2 中,是 from_attributes = True
            # from pydantic_settings import BaseSettings
            # model_config = SettingsConfigDict(from_attributes=True)
    

    注意:Pydantic V2 中 orm_mode 已改为 from_attributes。根据你的 Pydantic 版本调整。

  2. 创建获取数据库会话的依赖项:在 database.py 中定义一个函数,用于生成数据库会话,并确保在使用后关闭。

    # database.py
    # ... 其他代码 ...
    
    # 依赖项:获取数据库会话
    def get_db():
        db = SessionLocal()
        try:
            yield db # 使用 yield 将会话提供给路径操作函数
        finally:
            db.close() # 确保在请求结束后关闭会话
  3. 修改 main.py,添加路由并使用依赖注入

    # main.py
    from fastapi import FastAPI, Depends, HTTPException, status
    from fastapi.responses import RedirectResponse
    from sqlalchemy.orm import Session
    import models, schemas, crud # 导入相关模块
    from database import engine, SessionLocal, get_db # 导入 get_db
    
    models.Base.metadata.create_all(bind=engine)
    
    app = FastAPI()
    
    # 创建短网址的端点
    @app.post("/shorten", response_model=schemas.URLMap)
    def create_short_url(url_in: schemas.URLCreate, db: Session = Depends(get_db)):
        # 检查长 URL 是否已存在
        db_url = crud.get_url_map_by_long_url(db, str(url_in.long_url))
        if db_url:
            return db_url # 如果已存在,直接返回现有的记录
        # 创建新的 URL 映射
        new_url_map = crud.create_url_map(db=db, long_url=str(url_in.long_url))
        return new_url_map
    
    # 重定向端点
    @app.get("/{short_code}")
    def redirect_to_long_url(short_code: str, db: Session = Depends(get_db)):
        db_url = crud.get_url_map_by_short_code(db, short_code)
        if db_url is None:
            # 如果短码不存在,返回 404 Not Found
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Short URL not found")
    
        # 执行重定向
        return RedirectResponse(url=str(db_url.long_url), status_code=status.HTTP_301_MOVED_PERMANENTLY)
    • db: Session = Depends(get_db): 这就是依赖注入。FastAPI 看到这个,会自动调用 get_db 函数,获取一个数据库会话 db,并将其传递给 create_short_urlredirect_to_long_url 函数。
    • response_model=schemas.URLMap: 指定了 /shorten 端点成功响应的数据结构。
    • RedirectResponse: 用于返回 HTTP 重定向。
    • HTTPException: 用于返回标准的 HTTP 错误响应。

我的小练习: 运行 uvicorn main:app --reload,使用 /docs 交互式文档测试 /shorten 端点,创建一个短网址。然后直接在浏览器地址栏输入生成的短网址(如 http://127.0.0.1:8000/xxxxxx),看是否能成功重定向到原始的长 URL。

(感受:依赖注入 Depends(get_db) 的写法很优雅!它把获取和关闭数据库会话的逻辑从我的 API 处理函数中解耦了。使用 Pydantic 模型 (schemas.py) 来定义 API 的输入 (URLCreate) 和输出 (URLMap),使得代码结构非常清晰,FastAPI 还能自动校验和生成文档,这太棒了。RedirectResponseHTTPException 处理重定向和错误也很直接。)

17. 部署我的 FastAPI 应用 (基础篇)

我的 URL 短网址应用现在可以在本地运行了,但最终目标是让别人也能访问。这就需要把它部署到服务器上。

AI 告诉我,部署一个 Python Web 应用(尤其是像 FastAPI 这样的 ASGI 应用)通常涉及几个组件协同工作。以下是一个基础的、手动的部署流程(先不考虑 Docker):

假设我有一台 Linux 服务器(比如 Ubuntu),并且可以通过 SSH 访问。

步骤 1:服务器基础设置

  1. 安装 Python 和 Pip:确保服务器上有合适版本的 Python 3 和 pip3。如果用的是 Ubuntu,通常可以用 sudo apt update && sudo apt install python3 python3-pip python3-venv 来安装。
  2. 创建部署用户 (可选但推荐):创建一个专门用于运行应用的用户,而不是直接用 root。
  3. 防火墙设置:确保服务器防火墙允许 HTTP (80) 和 HTTPS (443) 端口的入站连接。

步骤 2:获取应用代码

  1. 安装 Gitsudo apt install git
  2. 克隆代码库:通过 SSH 登录服务器,在你选择的目录下(比如 /var/www/ 或用户主目录)克隆我的项目代码:
    git clone <你的项目 Git 仓库地> fastapi-project
    cd fastapi-project

步骤 3:设置 Python 虚拟环境和依赖

  1. 创建虚拟环境:在项目目录下创建虚拟环境:
    python3 -m venv .venv
  2. 激活虚拟环境
    source .venv/bin/activate
  3. 安装依赖:根据 requirements.txt 安装所有必需的包:
    pip install -r requirements.txt
    注意:这时 pip 会安装 FastAPI, Uvicorn, SQLAlchemy, aiosqlite 等。

步骤 4:运行 ASGI 服务器 (Gunicorn + Uvicorn)

直接用 uvicorn main:app 运行只适合开发。在生产环境中,通常需要一个更强大的 应用服务器 (Application Server) 来管理 Uvicorn 进程。Gunicorn 是一个常用的选择,它可以管理多个 Uvicorn worker 进程来处理并发请求。

  1. 安装 Gunicorn:在虚拟环境里安装 Gunicorn。

    pip install gunicorn

    (别忘了更新 requirements.txt)

  2. 使用 Gunicorn 运行 Uvicorn worker

    gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app -b 0.0.0.0:8000
    • -w 4: 启动 4 个 worker 进程 (数量通常根据服务器 CPU 核心数调整)。
    • -k uvicorn.workers.UvicornWorker: 指定 Gunicorn 使用 Uvicorn 来处理 ASGI 应用。
    • main:app: 和之前一样,指向我的 FastAPI 应用实例。
    • -b 0.0.0.0:8000: 绑定服务器的 IP 地址和端口。0.0.0.0 表示监听所有网络接口的 8000 端口。

    现在,我的应用应该可以通过服务器的 IP 地址和 8000 端口访问了 (比如 http://<服务器IP>:8000)。

步骤 5:使用进程管理器 (Systemd)

直接在终端运行 Gunicorn,如果我关闭了 SSH 连接,或者服务器重启,应用就停了。我们需要一个进程管理器来确保应用在后台持续运行,并且能在出错或服务器重启后自动恢复。

在现代 Linux 系统中,Systemd 是标准的进程管理器。

  1. 创建 Systemd service 文件:在 /etc/systemd/system/ 目录下创建一个名为 fastapi-app.service (或其他你喜欢的名字) 的文件,内容类似:

    [Unit]
    Description=Gunicorn instance to serve FastAPI URL Shortener
    After=network.target
    
    [Service]
    User=<你的部署用户名> # 替换成你创建的部署用户
    Group=www-data # 或你的部署用户所属的组
    WorkingDirectory=/path/to/fastapi-project # 替换成你的项目绝对路径
    Environment="PATH=/path/to/fastapi-project/.venv/bin" # 确保使用虚拟环境的路径
    ExecStart=/path/to/fastapi-project/.venv/bin/gunicorn --workers 4 --worker-class uvicorn.workers.UvicornWorker main:app --bind unix:/tmp/fastapi-app.sock
    # 注意:这里改为绑定到 Unix socket 文件,而不是 IP:Port,后面 Nginx 会用到
    
    [Install]
    WantedBy=multi-user.target
    • 你需要将 <你的部署用户名>/path/to/fastapi-project 替换成实际的值。
    • ExecStart 里,我们让 Gunicorn 绑定到一个 Unix socket 文件 (/tmp/fastapi-app.sock),而不是 TCP 端口。这样做通常性能更好,也更安全,因为只有本地进程(比如 Nginx)能访问它。
  2. 启用并启动服务

    sudo systemctl enable fastapi-app.service # 设置开机自启
    sudo systemctl start fastapi-app.service  # 启动服务
    sudo systemctl status fastapi-app.service # 查看服务状态

    现在 Gunicorn 和我的 FastAPI 应用应该在后台稳定运行了。

步骤 6:设置反向代理 (Nginx)

虽然 Gunicorn 已经在运行了,但直接把它暴露给公网不是最佳实践。通常会在前面放一个 Web 服务器 作为 反向代理,比如 Nginx 或 Apache。

Nginx 的好处:

  • 处理静态文件 (虽然我们这个简单应用可能没有)。
  • 负载均衡 (如果未来扩展到多台服务器)。
  • HTTPS/SSL 加密 (非常重要!)。
  • 安全防护 (限制请求速率、过滤恶意请求等)。
  • 将公网的 80/443 端口请求转发给后端的 Gunicorn (通过 Unix socket)
  1. 安装 Nginxsudo apt install nginx

  2. 配置 Nginx:在 Nginx 的配置目录 (通常是 /etc/nginx/sites-available/) 创建一个新的配置文件,比如 fastapi_app,内容类似:

    server {
        listen 80; # 监听 HTTP 80 端口
        server_name your_domain.com www.your_domain.com; # 替换成你的域名,或者服务器 IP
    
        location / {
            proxy_pass http://unix:/tmp/fastapi-app.sock; # 将所有请求转发给 Gunicorn  socket
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    
        # 如果有静态文件,可以在这里配置
        # location /static {
        #     alias /path/to/fastapi-project/static;
        # }
    }
  3. 启用配置并重启 Nginx

    sudo ln -s /etc/nginx/sites-available/fastapi_app /etc/nginx/sites-enabled/ # 创建软链接启用配置
    sudo nginx -t # 测试 Nginx 配置是否正确
    sudo systemctl restart nginx # 重启 Nginx

现在,我应该可以通过我的域名或服务器 IP 地址直接访问我的 FastAPI 应用了 (Nginx 会把请求转发给后台的 Gunicorn/Uvicorn/FastAPI)。

我的小练习: (这个比较复杂,暂时不做) 梳理一遍这个部署流程,理解每个组件(Git, venv, pip, Gunicorn, Uvicorn, Systemd, Nginx)的作用和它们之间的关系。

(感受:部署比我想象的要复杂!涉及好多组件。Gunicorn 管理 Uvicorn 进程,Systemd 管理 Gunicorn 进程,Nginx 做反向代理处理外部请求并转发给 Gunicorn。虽然步骤多,但每个组件职责清晰。这和部署 Node.js 应用(比如用 PM2 管理进程,用 Nginx 反代)的思路有很多相似之处,只是具体工具不同。先理解这个流程,以后再考虑用 Docker 简化它。)

18. 最终目标达成与后续

经过这一系列的步骤,我已经成功地:

  1. 掌握了 Python 的基础语法和常用数据结构。
  2. 学会了使用虚拟环境 (venv) 和包管理器 (pip)。
  3. 理解了 Python 的类、类型提示和异步编程 (async/await)。
  4. 上手了 FastAPI 框架,包括路由、Pydantic 模型和依赖注入。
  5. 实现了 URL 短网址应用的核心数据库交互 (SQLAlchemy) 和 API 逻辑。
  6. 了解了将 FastAPI 应用部署到服务器的基本流程。

虽然我的短网址应用还很简单(比如短码生成策略可以优化、可以增加点击统计等),并且部署流程也可以用 Docker 进一步简化,但达到这个程度,我已经算完成了最初设定的目标!

接下来我可以:

  • 完善这个短网址应用,增加更多功能。
  • 学习如何用 Docker 打包和部署这个应用。
  • 深入学习 FastAPI 的高级特性,比如 WebSocket、后台任务、认证授权等。
  • 探索其他 Python Web 框架或库。

我的 Python + FastAPI 之旅,才刚刚开始!

19. 用 Docker 打包我的 FastAPI 应用

之前我学习了手动部署的流程,虽然能跑起来,但步骤繁琐,而且每次换环境都要重复配置,感觉很麻烦。AI 告诉我,这正是 Docker 发挥作用的地方!

Docker 可以把我的应用代码、Python 解释器、所有依赖项(就像 requirements.txt 里的那些)以及运行所需的配置,一起打包到一个叫做 镜像 (Image) 的标准单元里。然后,我可以在任何支持 Docker 的机器上,用这个镜像运行一个 容器 (Container),容器就是一个隔离的、包含了应用运行所需一切的环境。这就像是把我的整个开发环境(或者说,一个精简版的运行环境)打包带走。

这和前端开发中,有时会用 Docker 来确保 Node.js 版本一致、或者打包一个 Next.js 应用进行部署的思路很像,都是为了环境一致性和简化部署。

任务 17:编写 Dockerfile

Dockerfile 就是一个文本文件,里面包含了一系列指令,告诉 Docker 如何构建我的应用镜像。

我在我的 fastapi-project 根目录下创建一个名为 Dockerfile (没有扩展名) 的文件,内容如下:

# Dockerfile

# 1. 选择一个基础镜像
# Python 官方提供了包含 Python 环境的镜像
# 选择一个具体的版本,比如 3.11,并使用 slim 版本以减小体积
FROM python:3.11-slim

# 2. 设置工作目录
# 在容器内创建一个目录,后续命令都在这里执行
WORKDIR /app

# 3. 复制依赖文件
#  requirements.txt 复制到容器的工作目录
COPY requirements.txt requirements.txt

# 4. 安装依赖
# 在容器内运行 pip install,安装所有依赖项
# --no-cache-dir 可以减少镜像体积
RUN pip install --no-cache-dir -r requirements.txt

# 5. 复制应用代码
# 将项目目录下的所有文件复制到容器的工作目录 /app
COPY . .

# 6. 暴露端口 (可选,但推荐)
# 告诉 Docker 容器将会监听哪个端口 (和 Uvicorn/Gunicorn 运行的端口一致)
EXPOSE 8000

# 7. 定义容器启动时运行的命令
# 使用 Uvicorn 启动 FastAPI 应用
# 使用 0.0.0.0 监听所有接口,端口 8000
# 注意:这里用 uvicorn 直接启动,适合简单场景或结合其他工具 (如 K8s)
# 如果需要 Gunicorn 管理多进程,CMD 可以是 gunicorn 命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Dockerfile 指令解释:

  • FROM: 指定基础镜像。
  • WORKDIR: 设置容器内的工作目录。
  • COPY: 将本地文件复制到容器内。
  • RUN: 在构建镜像过程中执行命令(比如安装依赖)。
  • EXPOSE: 声明容器运行时监听的端口(信息性,实际端口映射在 docker run 时指定)。
  • CMD: 指定容器启动时默认执行的命令。

任务 18:构建和运行 Docker 镜像

  1. 确保 Docker 已安装并运行:我需要先在我的 Mac 上安装 Docker Desktop
  2. 构建镜像:在终端里,确保我在 fastapi-project 目录下 (包含 Dockerfile 的地方),然后运行:
    docker build -t fastapi-shortener .
    • docker build: 构建命令。
    • -t fastapi-shortener: 给镜像打上标签 (tag),方便引用,格式是 repository:tag (这里省略了 tag,默认为 latest)。
    • .: 表示 Dockerfile 所在的当前目录。
      Docker 会按照 Dockerfile 里的步骤执行,下载基础镜像、复制文件、安装依赖等。第一次构建可能需要点时间。
  3. 运行容器:镜像构建成功后,用它来启动一个容器:
    docker run -d -p 8000:8000 --name shortener-app fastapi-shortener
    • docker run: 运行容器命令。
    • -d: 在后台 (detached mode) 运行容器。
    • -p 8000:8000: 将宿主机的 8000 端口映射到容器的 8000 端口 (格式是 hostPort:containerPort)。这样我就可以通过 http://localhost:8000 访问容器里的应用了。
    • --name shortener-app: 给运行的容器起个名字,方便管理。
    • fastapi-shortener: 使用哪个镜像来创建容器。
  4. 测试:现在我应该能像之前一样,通过 http://localhost:8000http://localhost:8000/docs 来访问我的 FastAPI 应用了,只不过这次它运行在 Docker 容器里!
  5. 管理容器
    • 查看运行中的容器:docker ps
    • 停止容器:docker stop shortener-app
    • 启动已停止的容器:docker start shortener-app
    • 查看容器日志:docker logs shortener-app
    • 删除容器 (需先停止):docker rm shortener-app
    • 查看本地镜像:docker images
    • 删除镜像:docker rmi fastapi-shortener

我的小练习: 成功构建并运行我的短网址应用的 Docker 容器,并通过浏览器访问 /docs 确认它正常工作。

(感受:Dockerfile 的语法挺直观的,有点像写部署脚本。把环境和依赖都打包在一起确实解决了环境不一致的问题。docker builddocker run 的流程跟我之前用 Docker 构建 Node.js 应用很像。虽然 Docker 内部还有很多概念(网络、卷等),但基础的打包和运行流程掌握起来不难。这对于部署来说,比之前的手动流程规范和方便多了!)

20. 添加一个极简前端页面

虽然 FastAPI 主要用于构建 API,但它也能提供静态文件服务。为了让我的短网址应用更完整一点,AI 建议我可以加一个最简单的前端页面,用来输入长 URL 并显示生成的短 URL。

任务 19:用 FastAPI 托管静态文件并实现前端交互

  1. 创建静态文件目录:在 fastapi-project 根目录下创建一个名为 static 的文件夹。

  2. 创建 index.html:在 static 文件夹里创建 index.html 文件。

    <!-- static/index.html -->
    <!DOCTYPE html>
    <html lang="zh-CN">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>URL 缩短器</title>
        <style>
          body {
            font-family: sans-serif;
            padding: 20px;
          }
          input[type='url'] {
            width: 80%;
            padding: 8px;
            margin-right: 5px;
          }
          button {
            padding: 8px 15px;
          }
          #result {
            margin-top: 15px;
            font-weight: bold;
          }
          #error {
            margin-top: 15px;
            color: red;
          }
        </style>
      </head>
      <body>
        <h1>URL 缩短器</h1>
        <form id="shorten-form">
          <input
            type="url"
            id="long-url"
            placeholder="输入要缩短的长网址"
            required
          />
          <button type="submit">缩短</button>
        </form>
        <div id="result"></div>
        <div id="error"></div>
    
        <script>
          const form = document.getElementById('shorten-form');
          const longUrlInput = document.getElementById('long-url');
          const resultDiv = document.getElementById('result');
          const errorDiv = document.getElementById('error');
    
          form.addEventListener('submit', async (event) => {
            event.preventDefault(); // 阻止表单默认提交行为
            resultDiv.textContent = ''; // 清空上次结果
            errorDiv.textContent = ''; // 清空上次错误
    
            const longUrl = longUrlInput.value;
    
            try {
              // 调用后端的 /shorten API
              const response = await fetch('/shorten', {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json',
                },
                body: JSON.stringify({ long_url: longUrl }),
              });
    
              if (!response.ok) {
                // 如果 API 返回错误状态码
                const errorData = await response.json();
                throw new Error(
                  errorData.detail || `HTTP error! status: ${response.status}`
                );
              }
    
              const data = await response.json();
              const shortUrl = `${window.location.origin}/${data.short_code}`;
              resultDiv.innerHTML = `短网址: <a href="${shortUrl}" target="_blank">${shortUrl}</a>`;
              longUrlInput.value = ''; // 清空输入框
            } catch (error) {
              console.error('Error:', error);
              errorDiv.textContent = `出错了: ${error.message}`;
            }
          });
        </script>
      </body>
    </html>

    这个 HTML 包含一个表单和一个简单的 JavaScript 片段,用于:

    • 监听表单提交事件。
    • 阻止默认提交。
    • 使用 fetch API 向后端的 /shorten 发送 POST 请求。
    • 处理响应,显示生成的短 URL 或错误信息。
  3. 修改 main.py 以提供静态文件服务:我需要告诉 FastAPI 如何找到并提供 static 目录下的文件。

    # main.py
    from fastapi import FastAPI, Depends, HTTPException, status, Request
    from fastapi.responses import RedirectResponse, HTMLResponse # 导入 HTMLResponse
    from fastapi.staticfiles import StaticFiles # 导入 StaticFiles
    from fastapi.templating import Jinja2Templates # 如果需要模板引擎
    from sqlalchemy.orm import Session
    import models, schemas, crud
    from database import engine, SessionLocal, get_db
    
    models.Base.metadata.create_all(bind=engine)
    
    app = FastAPI()
    
    # 挂载静态文件目录
    # "/static"  URL 路径, directory="static" 是本地目录名
    app.mount("/static", StaticFiles(directory="static"), name="static")
    
    # (可选) 如果你想让 index.html 在根路径 `/` 提供服务
    # 可以创建一个模板实例
    # templates = Jinja2Templates(directory="static")
    # @app.get("/", response_class=HTMLResponse)
    # async def read_index(request: Request):
    #     return templates.TemplateResponse("index.html", {"request": request})
    # 或者更简单地,为根路径创建一个路由直接返回 index.html
    @app.get("/", response_class=HTMLResponse)
    async def read_index():
        with open("static/index.html") as f:
            return HTMLResponse(content=f.read(), status_code=200)
    
    # --- 其他 API 路由保持不变 ---
    @app.post("/shorten", response_model=schemas.URLMap)
    def create_short_url(url_in: schemas.URLCreate, db: Session = Depends(get_db)):
        # ... (省略之前的代码)
        db_url = crud.get_url_map_by_long_url(db, str(url_in.long_url))
        if db_url:
            return db_url
        new_url_map = crud.create_url_map(db=db, long_url=str(url_in.long_url))
        return new_url_map
    
    @app.get("/{short_code}")
    def redirect_to_long_url(short_code: str, db: Session = Depends(get_db)):
        # ... (省略之前的代码)
        db_url = crud.get_url_map_by_short_code(db, short_code)
        if db_url is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Short URL not found")
        return RedirectResponse(url=str(db_url.long_url), status_code=status.HTTP_301_MOVED_PERMANENTLY)
    
    # ... (省略 __main__ 部分) ...
    • app.mount("/static", StaticFiles(directory="static"), name="static"): 这行代码告诉 FastAPI,所有以 /static 开头的 URL 请求,都应该去本地的 static 目录查找文件。
    • 我还添加了一个根路径 / 的 GET 路由,它直接读取并返回 static/index.html 的内容。
  4. 测试:运行 uvicorn main:app --reload。现在访问 http://127.0.0.1:8000/ 应该就能看到我创建的 URL 缩短器页面了。尝试输入一个 URL 并点击"缩短",看看是否能正常工作。

我的小练习: 稍微美化一下 index.html 的样式,或者在显示结果时增加一个"复制"按钮。

(感受:用 FastAPI 提供静态文件服务比我想象的简单,app.mount 就搞定了。前端的 JS 代码和在 Node.js 应用里写前端交互逻辑基本一样,都是用 fetch 调用 API。这种方式适合给 API 配一个简单的管理界面或测试页面。当然,对于复杂的单页应用 (SPA),我还是会选择用 React/Vue/Next.js 分开构建,然后让 FastAPI 纯粹提供 API。)

21. 生产环境注意事项:我的应用还缺啥?

我的应用现在能在本地跑,也能打包进 Docker,甚至还有个简单的前端界面。但 AI 提醒我,要把它真正放到生产环境稳定运行,还有很多事情要做。这就像我完成了一个前端组件,但还需要考虑测试、性能、安全性、部署流程等才能合并到主分支一样。

AI 帮我梳理了一个生产准备清单:

  1. 配置管理

    • 问题:我的数据库 URL (SQLALCHEMY_DATABASE_URL) 现在是硬编码在 database.py 里的,这在生产环境肯定不行,比如生产数据库的地址和密码不能直接写在代码里。
    • 解决方案:像 Node.js 应用常用 .env 文件和 dotenv 库一样,Python 也有类似机制。推荐使用 环境变量 来传递配置。Pydantic 的 Settings Management 功能可以很方便地从环境变量或 .env 文件加载配置。
    • 下一步:学习使用 Pydantic Settings 来管理数据库 URL 和可能的密钥。
  2. HTTPS/SSL

    • 问题:我的应用现在是通过 HTTP 运行的,这在生产环境是不安全的,数据传输都是明文。
    • 解决方案:必须启用 HTTPS。这通常不是在 FastAPI/Uvicorn/Gunicorn 层面直接做,而是在 反向代理层(如 Nginx) 配置 SSL 证书(可以使用 Let's Encrypt 获取免费证书)。Nginx 负责处理 HTTPS 加密/解密,然后通过安全的内部连接(如 Unix socket)与后端应用通信。
    • 下一步:学习如何在 Nginx 中配置 SSL 证书。
  3. 日志记录 (Logging)

    • 问题:现在我的应用几乎没有日志,出了问题很难排查。
    • 解决方案:Python 内置了强大的 logging 模块。我需要配置它来记录应用的运行信息、API 请求、错误等,并将日志输出到文件或集中的日志系统。
    • 下一步:学习配置 Python 的 logging 模块,并集成到 FastAPI 中记录请求和异常。
  4. 错误监控与告警

    • 问题:应用在生产环境出错时,我需要能及时知道并获取详细信息。
    • 解决方案:集成第三方错误监控服务,比如 Sentry。它可以在应用抛出未捕获的异常时自动上报错误详情。
    • 下一步:了解如何在 FastAPI 应用中集成 Sentry。
  5. 数据库迁移 (Migrations)

    • 问题:如果以后我需要修改数据库模型(比如给 URLMap 加个 clicks 计数列),直接修改 models.py 并不能自动更新现有的数据库表结构。
    • 解决方案:使用数据库迁移工具。对于 SQLAlchemy,最常用的是 Alembic。它可以根据模型的变更自动生成迁移脚本,并安全地应用到数据库。
    • 下一步:学习使用 Alembic 来管理数据库 Schema 的变更。
  6. 更健壮的测试

    • 问题:我现在还没有为我的应用编写任何测试。
    • 解决方案:编写单元测试(测试单个函数或类)、集成测试(测试组件间的交互,比如 API 调用数据库)和端到端测试。FastAPI 对测试有很好的支持,可以方便地模拟请求。
    • 下一步:学习使用 pytest 和 FastAPI 的 TestClient 来编写测试用例。
  7. 安全性加固

    • 问题:虽然 FastAPI 和 Pydantic 已经提供了一些基础校验,但生产环境需要更多考虑。
    • 解决方案
      • CORS (跨源资源共享):如果我的前端和后端部署在不同域名下,需要在 FastAPI 中配置 CORS 中间件。
      • 速率限制:防止恶意用户频繁请求 API。
      • 安全头部:添加一些 HTTP 安全头部(通常在 Nginx 配置)。
      • 依赖项安全扫描:定期检查项目依赖是否存在已知的安全漏洞 (类似 npm audit)。
    • 下一步:了解 FastAPI 的 CORS 中间件和其他安全相关的实践。
  8. 性能与扩展性

    • 问题:目前应用很简单,但流量增大后可能会遇到瓶颈。
    • 解决方案
      • 数据库优化:确保为常用查询字段添加了索引 (我们之前在模型里加了 index=True)。
      • 缓存:对于不经常变化的数据(比如短码对应的长 URL),可以添加缓存 (如 Redis) 来减少数据库查询。
      • 调整 Gunicorn workers:根据服务器资源调整 worker 数量。
      • (更进一步) 水平扩展:运行多个应用实例,并通过负载均衡器分发流量。
    • 下一步:暂时不需要深入,但要了解这些优化方向。
  9. CI/CD (持续集成/持续部署)

    • 问题:现在每次更新代码都需要手动登录服务器、拉代码、重启服务,效率低且易出错。
    • 解决方案:搭建 CI/CD 流水线(比如用 GitHub Actions, GitLab CI, Jenkins)。当代码推送到 Git 仓库时,自动运行测试、构建 Docker 镜像、并将新版本部署到服务器。
    • 下一步:了解 CI/CD 的基本概念和工具。

(感受:哇,生产环境要考虑的事情真不少!配置、安全、日志、监控、测试、部署自动化... 这清单让我意识到写代码只是第一步。不过很多概念(像环境变量、HTTPS、日志、测试、CI/CD)和我在前端/Node.js 开发中接触到的思路是相通的,只是具体工具和实现方式不同。感觉 Python 生态在这些方面应该也有成熟的解决方案。)

22. Python Web 框架对比:FastAPI vs. Django vs. Flask

既然我选择了 FastAPI,AI 也建议我了解一下 Python 世界里另外两个非常流行的 Web 框架:Django 和 Flask,这样能更好地理解 FastAPI 的定位和优缺点。

特性/方面FlaskDjangoFastAPI
类型微框架 (Microframework)全栈框架 ("Batteries-included")微框架 (API 重点) / 异步框架
核心理念保持核心简单,高度可扩展,灵活约定优于配置,快速开发,功能全面高性能 (异步),类型提示,自动文档,开发者体验
自带功能路由、请求响应处理、模板 (Jinja2)ORM、Admin 后台、认证、表单、模板、缓存等路由、异步、数据校验 (Pydantic)、依赖注入、自动文档
异步支持原生不支持 (需扩展或 ASGI 适配器)近期版本开始加强异步支持 (ORM, Views 等)核心特性 (基于 Starlette 和 asyncio)
数据校验依赖扩展 (如 Flask-WTF, Marshmallow)自带表单和模型校验核心特性 (深度集成 Pydantic)
数据库 ORM依赖扩展 (如 Flask-SQLAlchemy)自带强大 ORM依赖扩展 (常用 SQLAlchemy)
模板引擎Jinja2 (默认)自带模板引擎 (DTL),也支持 Jinja2不直接绑定,可集成 Jinja2 等 (但更侧重 API)
Admin 后台无 (需扩展)自带,非常强大无 (可自己构建或用第三方库)
学习曲线较低 (核心简单)较高 (功能多,概念多)中等 (需要理解类型提示、异步、依赖注入)
灵活性非常高较低 (更遵循框架约定)较高 (比 Django 灵活)
适合场景简单应用、API、微服务、需要高度定制的项目大型复杂 Web 应用、内容管理系统、需要快速原型开发API、微服务、需要高性能/高并发的应用、类型安全是重点
类比 Node.js类似 Express.js (轻量、灵活)类似 NestJS (框架感强,功能全) 或 Ruby on Rails现代化、专注于 API、性能好,可能没有特别直接的类比

为什么 FastAPI 适合我的 URL 短网址应用?

  • API 优先:我的目标主要是构建 API (/shorten, /{short_code} 重定向),FastAPI 天生就是为此设计的。
  • 性能:虽然短网址应用初期性能要求不高,但 FastAPI 基于 ASGI 的异步特性让它有很好的性能潜力。
  • 类型提示和 Pydantic:这极大地简化了数据校验和 API 文档,提高了开发效率和代码健壮性,对于习惯了 TypeScript 的我来说很友好。
  • 自动文档:Swagger UI / ReDoc 对于 API 开发和调试太有用了。
  • 学习曲线适中:相对于 Django 的庞大体系,FastAPI 更容易上手,同时又比 Flask 提供了更多现代化的内置功能(如依赖注入)。

(感受:这个对比很清晰。Flask 看起来更自由,适合从零搭起;Django 功能强大,适合快速构建大型应用但可能有点"重";FastAPI 则是在性能、现代特性和开发效率之间找到了一个很好的平衡点,特别适合构建 API。看来我选择 FastAPI 作为入门和完成这个项目的框架是明智的。)

23. 学习总结与展望

这次 AI 辅助的 Python 学习旅程真是收获满满!从最初只会前端的我,到现在能够:

  • 熟练使用 Python 基础语法。
  • 理解并运用虚拟环境和包管理。
  • 掌握 Python 的类、类型提示和异步编程。
  • 使用 FastAPI 框架构建 API,并集成 Pydantic 和 SQLAlchemy。
  • 了解了基本的部署流程和生产环境注意事项。
  • 甚至用 Docker 对应用进行了打包!

虽然我的 URL 短网址应用还很简单,但这个过程让我对 Python 后端开发有了全面的认识和实践经验。我明白了 Python 不仅仅是脚本语言,它在 Web 开发、数据处理等方面同样强大且生态完善。

接下来,我会继续完善我的短网址应用,实践生产环境清单里的各项要求,特别是配置管理、日志、测试和数据库迁移。然后,我可能会尝试用 Docker Compose 把应用和可能的数据库(比如换成 PostgreSQL)一起编排部署。

这段学习经历也让我体会到,不同语言和技术栈之间,很多核心思想是相通的(比如模块化、依赖管理、异步处理、MVC/MVVM 模式、部署流程等)。拥有前端背景,学习 Python 后端感觉顺畅了不少。

感谢 AI 的一路指导!Python 的世界,我来了!

24. 免费部署选择:让我的 Demo 上线

本地跑起来很酷,Docker 打包也搞定了,但我的最终目标是让这个短网址 Demo 能在网上被访问到。手动部署到 VPS 流程我大致了解了,但感觉还是有点麻烦,而且还得花钱买服务器。

我就问 AI,有没有免费的、适合部署这种简单 Demo 的平台?最好能让我省点事。

AI 给我分析了几个流行的平台,并结合我目前用 SQLite 的情况给出了建议:

  1. Vercel: 我比较熟悉,前端项目经常用。理论上它支持 Python Serverless Functions,可以跑 FastAPI。但 关键问题 在于它的函数环境是无状态且文件系统短暂的。这意味着我的 shortener.db SQLite 文件无法持久保存,每次请求都可能面对一个丢失数据的环境。结论:不改数据库的话,Vercel 不行。 要用 Vercel,我必须换成它支持的云数据库(比如 Vercel Postgres 或 Neon),并把连接信息配置成环境变量。

  2. PythonAnywhere: 这个平台 AI 重点推荐给我,因为它专门为 Python 设计,而且它的免费套餐提供持久化文件系统!这意味着我可以直接部署我现有的、使用 SQLite 的应用,无需修改数据库代码。这对于快速验证 Demo 来说,简直是完美匹配。缺点是免费版有限制,部署流程可能和 Vercel 不太一样。

  3. Render: 它提供免费的 Web Service (跑 FastAPI) 和免费的 PostgreSQL 数据库。部署流程可以通过连接 GitHub 实现自动化,比较现代。缺点是我必须把数据库从 SQLite 换成 PostgreSQL,需要修改代码并学习新的数据库配置。免费实例在不活动时会休眠。

  4. Fly.io: 也是一个不错的选择,提供免费的小型 Postgres 集群,并且对 Docker 支持很好(我正好写了 Dockerfile)。缺点同样是必须换数据库,且主要通过命令行操作,学习曲线可能比 Render 稍高一点点。

  5. Railway: 和 Render 类似,提供免费的使用额度可以跑应用和数据库 (如 PostgreSQL),通过 Git 部署也很方便。缺点还是必须换数据库,并且要注意免费额度。

我的选择思考:

  • 目标:最快、最简单部署现有 Demo -> PythonAnywhere 无疑是首选,因为它兼容 SQLite,我不用改代码就能上线看效果。
  • 目标:顺便学习更通用的云部署和数据库 -> RenderRailway 不错,它们强迫我切换到 PostgreSQL(这是生产环境更常用的数据库),并且部署流程更现代化。
  • 目标:利用 Docker 部署经验 -> Fly.io 挺合适,同样需要换数据库,但能更好地利用我的 Dockerfile。

考虑到我只是想快速部署这个 Demo,而且暂时不想花时间去改数据库和学习 PostgreSQL,我决定先研究一下 PythonAnywhere 的部署方法。等以后应用复杂了,或者想学习云数据库了,再考虑 Render 或 Fly.io。

25. 下一步行动

说明:最后我没有部署在 PythonAnywhere 上!!!!!

清晰了!我的下一个具体的行动步骤是:

  1. 注册 PythonAnywhere 账号。
  2. 学习如何在 PythonAnywhere 上传我的 fastapi-project 代码。
  3. 配置 PythonAnywhere 来运行我的 FastAPI 应用(可能需要设置 WSGI/ASGI 入口,类似 main:app)。
  4. 确保 SQLite 数据库文件 shortener.db 能够被应用正确读写。
  5. 获取我的 *.pythonanywhere.com 域名,并测试应用。

搞定部署后,我的这个学习项目就算真正落地了!

26.一些在过程中碰到的问题

问题 1:Python 生成随机字符串的两种语法有什么区别?

# 语法 1: 使用循环和 random.choice
def generate_short_code_v1(length: int = 6) -> str:
  characters = string.ascii_letters + string.digits
  # 每次循环随机选择一个字符,然后用 join 连接起来
  return ''.join(random.choice(characters) for _ in range(length))

# 语法 2: 使用 random.choices
def generate_short_code_v2(length: int = 6) -> str:
  characters = string.ascii_letters + string.digits
  # 直接从 characters 中随机选择 length 个字符 (允许重复),返回一个列表
  # 然后用 join 连接起来
  return ''.join(random.choices(characters, k=length))

这两种语法都能达到生成指定长度随机字符串的目的,主要区别在于实现方式和表达的简洁性:

  1. random.choice(sequence): 这个函数从给定的序列(字符串、列表等)中随机选择一个元素并返回。语法 1 使用了一个生成器表达式 (random.choice(characters) for _ in range(length)),它会循环 length 次,每次调用 random.choice 得到一个随机字符,最后 ''.join() 将这些逐个生成的字符连接成一个字符串。
  2. random.choices(population, k=n): 这个函数(注意是 choices,复数)直接从给定的 population(序列)中一次性随机选择 k 个元素(默认允许重复选择),并返回一个包含这 k 个元素的列表。语法 2 调用它得到一个包含 length 个随机字符的列表,然后 ''.join() 将这个列表连接成字符串。

哪个更好?

  • 简洁性: 语法 2 (random.choices) 通常被认为更简洁、更 Pythonic,因为它直接表达了"随机选择 k 个元素"的意图,而不是通过循环逐个构建。
  • 效率: 对于生成较长的字符串,random.choices 内部实现可能比显式 Python 循环调用 random.choice 更优化一些,但对于短字符串(比如短码),性能差异通常可以忽略不计。

类比 Node.js: 这有点像你需要生成一个包含 6 个随机数字的数组:

  • 语法 1 类似于写一个 for 循环,每次循环 Math.random()push 到数组。
  • 语法 2 更像是(虽然 JS 没有直接对应的内置函数)调用一个假想的 generateRandomItems(sourceArray, count) 函数,它直接返回所需数量的随机项组成的数组。

结论:两者功能相同,但使用 random.choices 是更推荐、更简洁的方式。


问题 2:Python 类 __init__ 方法的 self 参数必须传且必须在第一个位置吗?

class Shortener:
  # 这个 self 是必须的吗?必须叫 self 吗?必须放第一位吗?
  def __init__(self):
    self.db_path = "shortener.db"
    self.engine = create_engine(f"sqlite:///{self.db_path}")
    self.session = sessionmaker(bind=self.engine)()

是的,对于实例方法 (instance methods)(包括特殊方法如 __init__),情况如下:

  1. 必须有代表实例本身的参数:当你调用一个对象的方法时(例如 my_shortener = Shortener(); my_shortener.some_method()),Python 解释器会自动将这个对象(my_shortener)作为第一个参数传递给方法。因此,方法的定义必须包含一个参数来接收这个实例引用。
  2. 必须是第一个参数:这个代表实例本身的参数必须是方法定义的第一个参数
  3. 命名约定是 self: 这个参数的名字按约定俗成应该是 self。虽然你可以用其他名字(比如 this,如果你非常想念 JS 的话),但这强烈不推荐,会违反 Python 社区的广泛约定,让其他 Python 开发者(以及未来的你)阅读代码时感到困惑。

类比 Node.js/JavaScript:

  • __init__ 方法非常类似于 JS 类中的 constructor() 方法,都是在创建类的新实例时自动调用的初始化方法。
  • self 参数扮演的角色完全等同于 JS 中的 this 关键字。JS 中的 this 是隐式可用的,你不需要在方法参数列表里声明它。而在 Python 中,你需要显式地在实例方法的参数列表里声明第一个参数(通常命名为 self)来接收这个实例引用。当你需要访问实例的属性或调用实例的其他方法时,都需要通过 self.attributeself.other_method() 来进行,就像在 JS 中使用 this.propertythis.method() 一样。

总结: 在 Python 类中定义实例方法(如 __init__, greet 等)时,第一个参数必须存在,用来接收实例本身,并且强烈建议将其命名为 self


问题 3:Python 的依赖注入 (Dependency Injection) 是什么概念?FastAPI 是怎么实现的?

依赖注入 (Dependency Injection, DI) 是一种设计模式,其核心思想是将一个对象(或函数)所依赖的其他对象(它的"依赖项")从外部传递("注入")给它,而不是让它自己在内部创建这些依赖项

为什么需要 DI?(好处)

  • 解耦 (Decoupling):你的组件(比如 API 路由处理函数)不需要关心它的依赖项(比如数据库会话)是如何创建和管理的。它只需要声明它需要什么,DI 容器(在 FastAPI 中就是 FastAPI 框架本身)会负责提供。这降低了组件之间的耦合度。
  • 可测试性 (Testability):在测试时,你可以轻松地用模拟 (Mock) 的依赖项替换掉真实的依赖项。比如,测试 API 逻辑时,你可以注入一个模拟的数据库会话,而不需要连接真实的数据库。
  • 可重用性 (Reusability):依赖项的创建逻辑(比如获取数据库连接、获取当前用户)可以被多个地方重用。
  • 代码更清晰 (Clean Code):业务逻辑代码可以更专注于核心任务,不必混杂依赖项的创建和管理代码。

类比 Node.js/前端:

  • 在某些 Node.js 框架(如 NestJS)或前端框架(如 Angular)中,DI 是一个核心概念,有专门的容器和装饰器(如 @Injectable(), @Inject()) 来实现。
  • 即使在没有显式 DI 容器的 Express.js 或 React 中,有时也会应用 DI 的思想。比如,你会把数据库连接实例或者配置对象作为参数传递给你的服务层函数或 React 组件的 props,而不是让它们直接去 require('./db')import config from './config'

FastAPI 如何实现 DI?

FastAPI 的 DI 系统非常优雅且易于使用,主要通过 Depends 函数实现:

  1. 依赖项是一个可调用对象: 一个"依赖项"可以是任何可调用对象 (callable),最常见的是一个函数(比如我们写的 get_db() 函数),也可以是一个类。

  2. 在路径操作函数中声明依赖: 在你的 API 路由处理函数(路径操作函数)的参数中,使用 Depends() 来声明依赖。

    from fastapi import Depends, FastAPI
    from sqlalchemy.orm import Session
    from .database import get_db # 假设 get_db 是获取数据库会话的函数
    
    app = FastAPI()
    
    @app.get("/items/")
    # db: Session = Depends(get_db) 就是声明依赖
    # FastAPI 会调用 get_db(),并将返回值赋给 db 参数
    async def read_items(db: Session = Depends(get_db)):
        # 在函数体内可以直接使用注入的 db 会话
        items = db.query(Item).all()
        return items
  3. FastAPI 自动处理: 当请求到达这个路径时,FastAPI 会:

    • 识别出 Depends(get_db)
    • 调用 get_db() 函数。
    • get_db() 函数通常使用 yield 来提供依赖项(比如数据库会话 db)。
    • FastAPI 将 yield 出来的值(db 会话)注入read_items 函数的 db 参数中。
    • 执行 read_items 函数的逻辑。
    • read_items 函数执行完毕(无论成功还是出错),FastAPI 会继续执行 get_db() 函数中 yield 之后的代码(通常是 finally 块里的 db.close()),确保资源被正确清理。

FastAPI DI 的强大之处:

  • 支持嵌套依赖: 依赖项本身也可以声明自己的依赖项。
  • 自动参数注入: 依赖项函数如果需要参数(比如请求对象、其他依赖),FastAPI 也会自动注入。
  • 类型提示集成: 可以结合类型提示,使依赖项的类型清晰可见。
  • 代码简洁: 相比其他框架复杂的 DI 配置,Depends 非常直观。

问题 4:Python 的包 (Package) 和模块 (Module) 都是什么概念?怎么管理?

在 Python 中,模块和包是组织代码的基本方式:

  1. 模块 (Module):

    • 概念: 最简单的模块就是一个包含 Python 代码的 .py 文件。文件名就是模块名(不含 .py 后缀)。
    • 作用: 将相关的代码(函数、类、变量)组织在一个文件内,方便复用和管理命名空间。
    • 使用: 使用 import module_namefrom module_name import name 来导入和使用模块中的内容。
    • 类比 Node.js: 非常类似于 Node.js 中的一个 .js 文件模块。
  2. 包 (Package):

    • 概念: 一个包含多个模块(和其他子包)的目录。这个目录必须包含一个特殊的 __init__.py 文件(即使它是空的),Python 才会将其识别为一个包。
    • 作用: 用于组织更大规模的项目,将相关模块按功能或层次结构分组。
    • 使用: 可以导入整个包 (import package_name),或者导入包中的特定模块 (import package_name.module_namefrom package_name import module_name),或者导入模块中的具体内容 (from package_name.module_name import name)。
    • 类比 Node.js:
      • 概念上有点像 Node.js 中一个包含 index.js 或在 package.json 中指定了 main 入口的目录模块。
      • 但更广泛地说,Node.js 的"包"通常指一个可以通过 npm 安装和管理的单元,由 package.json 描述,可以包含代码、文档、测试等,并发布到 npm registry。Python 的"包"最初更侧重于代码的组织结构,但现在通过 PyPI (Python Package Index) 和打包工具 (如 setuptools, Poetry),也可以实现类似 Node.js 包的发布和分发。

管理方式:

  • 代码组织: 通过创建目录和 __init__.py 文件来组织包和子包。
  • 导入: 使用 import 语句来访问包和模块中的代码。Python 会根据其 模块搜索路径 (Python Path) 来查找需要导入的包和模块。这个路径包括:
    1. 当前脚本所在的目录。
    2. 环境变量 PYTHONPATH 中指定的目录。
    3. Python 安装时设置的标准库路径和 site-packages 目录(第三方库安装的地方)。
  • 第三方包管理:
    • 查找: PyPI (Python Package Index) 是官方的第三方包仓库 (类似 npm registry)。
    • 安装/管理:
      • 标准工具: pip (安装/卸载) + venv (虚拟环境) + requirements.txt (记录依赖快照)。
      • 现代工具: PoetryPipenv (自动化虚拟环境管理、依赖声明、版本锁定、打包发布)。这些工具提供了更接近 npm/yarn 的体验。

总结: Python 的模块是单个文件,包是包含 __init__.py 的目录,用于组织模块。通过 import 语句和 Python Path 来管理代码间的引用。第三方包通过 PyPI 查找,并推荐使用 Poetry 或 Pipenv 进行安装和管理。


问题 5:Python 3 有哪些内置模块?常用的有哪些?

这里需要区分两个概念:内置函数/类型 (Built-ins)标准库模块 (Standard Library Modules)

  1. 内置函数/类型 (Built-ins):
  • 概念: 这些是直接编译到 Python 解释器内部的,无需 import 即可直接使用。它们是 Python 语言核心的一部分。
  • 常用示例:
    • 函数: print(), len(), type(), id(), input(), range(), open(), abs(), max(), min(), sum(), sorted(), reversed(), isinstance(), issubclass(), dir(), help() 等。
    • 类型: int, float, str, bool, list, dict, tuple, set, bytes, NoneType (对应 None) 等。
    • 异常: Exception, ValueError, TypeError, IndexError, KeyError, FileNotFoundError 等。
  • 类比 Node.js/JavaScript: 类似于 JS 语言本身提供的全局对象(Math, JSON, Date)、全局函数(parseInt, parseFloat, isNaN)、原始类型(string, number, boolean, null, undefined, symbol, bigint)和内置对象(Array, Object, Function, Promise, Error 等)。
  1. 标准库模块 (Standard Library Modules):
  • 概念: 这些模块是随 Python 安装一起提供的,包含了大量用于完成各种常见任务的功能。但与内置函数不同,你需要使用 import 语句来加载和使用它们。标准库非常丰富,是 Python 强大的原因之一。
  • 常用示例 (挑选一些你可能感兴趣或常用的):
    • os: 提供了访问操作系统功能的接口,如文件路径操作 (os.path 类似 Node.js 的 path)、环境变量 (os.environ)、文件系统操作 (os.listdir, os.remove) 等。(类似 Node.js 的 os, fs, path 模块的部分功能)
    • sys: 访问与 Python 解释器紧密相关的变量和函数,如命令行参数 (sys.argv)、退出解释器 (sys.exit())、模块搜索路径 (sys.path) 等。
    • json: 用于编码和解码 JSON 数据。(类似 JS 的全局 JSON 对象)
    • datetime: 处理日期和时间的类 (date, time, datetime, timedelta)。(功能比 JS 的 Date 强大,更像 Moment.js 或 date-fns)
    • math: 提供基本的数学函数 (math.sqrt, math.sin, math.log 等)。(类似 JS 的 Math 对象)
    • random: 生成伪随机数、进行随机选择等。
    • re: 正则表达式操作。(类似 JS 的 RegExp 对象和字符串的正则方法)
    • collections: 提供了一些有用的数据结构补充,如 defaultdict (带默认值的字典), Counter (计数器), deque (双端队列), namedtuple (带字段名的元组)。
    • typing: 提供对类型提示的支持(我们在 FastAPI 中大量使用)。
    • asyncio: Python 的异步编程框架(async/await 的基础)。
    • http.client, urllib.request, urllib.parse: 用于进行 HTTP 请求和 URL 处理的基础库(但通常大家更喜欢用第三方库 requests)。
    • logging: 用于记录日志的标准库。
    • subprocess: 用于创建和管理子进程。
    • shutil: 提供高级文件操作(如复制、移动、删除文件和目录)。
    • pathlib: 面向对象的文件系统路径操作(比 os.path 更现代、更易用)。
    • csv: 读写 CSV 文件。
    • sqlite3: 直接支持 SQLite 数据库。
    • venv: 创建虚拟环境的工具。
    • unittest, pytest (虽然 pytest 是第三方,但非常常用): 用于编写和运行测试。

总结: Python 有很多无需导入即可使用的内置函数和类型,构成了语言的核心。此外,还有一个极其丰富的标准库,包含了各种模块,你需要 import 它们来使用其功能,涵盖了从文件操作、网络通信到日期时间处理等方方面面。熟悉常用的标准库模块对于高效 Python 开发非常重要。