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

这篇文档记录了我从 0 开始,在 AI 的帮助下学习 Python 并上线我的短网址应用的全过程。

应用在线地址: https://fastapi.zilin.im/
应用源代码 Github: https://github.com/hifizz/shorturl-fastapi

为什么选择短网址这个项目来练习?

  1. 短网址应用是一个典型的 Web 应用,适合用来学习 Python 后端开发。
  2. 这个项目代码量小,却功能完整,适合用来实践生产环境清单里的各项要求。
  3. 小项目容易有成就感,适合用来练习。

由这段 Prompt 开始了我的 Python 学习之旅 👇🏻

你是一个高级 Python 开发工程师。现在教我如何一步一步学习 Python。

我是一个前端开发者,非常熟悉 Nodejs  Vue.js React.js,Next.js ;我的操作系统是 macOS;请结合我的背景继续给我小任务帮我一步一步学习 python,然后输出到这个文档中。
注意:请以我在 AI 的帮助下学习 Python 的视角来编写这边文档。

---从这里开始就是AI在我的指挥下生成的文档了👇🏻---

1. 在 macOS 上安装 Python 环境

我的 Mac 上虽然自带了 Python,但版本比较旧。AI 建议我安装最新版本,这样能用到最新的功能和优化。最方便的方法是用 Homebrew 这个包管理器。

第一步:安装 Homebrew (如果我还没装的话)

AI 告诉我,如果我之前没用过 Homebrew,需要先在终端里运行这个命令来安装:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

安装过程需要我输入密码确认一下,花了几分钟。

第二步:用 Homebrew 安装 Python

装好 Homebrew 后,就可以用它来安装 Python 了。在终端里输入:

brew install python

Homebrew 自动帮我搞定了最新稳定版的下载和安装。

第三步:验证安装

为了确认 Python 装好了,AI 让我重新打开一个终端窗口,然后输入:

python3 --version

看到输出 Python 3.x.x 这样的版本号,就表示成功了!AI 特别提醒我要用 python3 而不是 python,因为 python 可能还是指向系统自带的旧版本。

2. 我的第一个 Python 程序:"Hello, World!"

按照传统,我得先让程序说 "Hello, World!"。

第一步:准备编辑器

写代码得有个地方。虽然 Mac 自带的文本编辑也能用,但 AI 推荐我用 VS Code,功能更专业。我就先用自带的试试,回头再装 VS Code。

第二步:创建 .py 文件

我新建了一个文件,输入了下面这行 Python 代码:

print("Hello, World!")

AI 解释说,这行代码就是调用 print 函数,把括号里的文字打印出来。

我把文件保存为 hello.py,放到了桌面上。AI 强调后缀名必须是 .py

第三步:运行程序

我再次打开终端,先用 cd Desktop 命令切换到桌面目录。然后,用 python3 来运行我的文件:

python3 hello.py

终端里果然出现了:

Hello, World!

成功!我的第一个 Python 程序跑起来了!

3. 结合我的前端背景:Python 初体验

"Hello, World!" 成功后,我开始尝试一些更具体的语法,AI 帮我对比了 Python 和我熟悉的 JavaScript。

任务 1:变量和数据类型 - 和 JS 挺像

我发现 Python 定义变量比 JS 还简单,不用 letconst,直接写变量名赋值就行。

# 我定义的几个变量
my_name = "前端小明" # 字符串 (string)
my_age = 28        # 整数 (integer)
my_height = 175.5  # 浮点数 (float)
is_learning_python = True # 布尔值 (boolean)

# AI 让我用 type() 看看类型
print(type(my_name))           # <class 'str'>
print(type(my_age))            # <class 'int'>
print(type(my_height))         # <class 'float'>
print(type(is_learning_python)) # <class 'bool'>

# JS 的数组 (Array) -> Python 的列表 (List)
my_skills = ["JavaScript", "React", "Node.js", "CSS"]
print(my_skills)               # ['JavaScript', 'React', 'Node.js', 'CSS']
print(type(my_skills))         # <class 'list'>

# JS 的对象 (Object) -> Python 的字典 (Dictionary)
my_profile = {
    "name": my_name,
    "age": my_age,
    "skills": my_skills
}
print(my_profile)              # {'name': '前端小明', 'age': 28, 'skills': [...]}
print(type(my_profile))        # <class 'dict'>

我的小练习: 我创建了一个表示项目配置的字典,像这样:

project_config = {
    "projectName": "MyAwesomeApp",
    "version": "1.0.0",
    "dependencies": ["react", "next", "tailwindcss"]
}
print(project_config)

(感受:基本数据类型和 JS 差别不大,列表和字典的概念也很容易对应。)

任务 2:定义函数 - def vs function

Python 用 def 定义函数,后面跟函数名、参数和冒号,然后是缩进的代码块。AI 提醒我缩进在 Python 里是语法的一部分,不像 JS 主要靠花括号 {}

# 定义一个简单的打招呼函数
def greet(name):
    # 这个 f"..."  f-string,类似 JS 的模板字符串
    greeting = f"你好, {name}! 很高兴你开始学习 Python。"
    print(greeting)

# 调用函数
greet(my_name)
greet("世界")

# 定义一个带返回值的函数
def calculate_list_length(items):
    return len(items) # len() 是内置函数,获取长度

skills_count = calculate_list_length(my_skills)
print(f"我有 {skills_count} 项主要技能。")

我的小练习: 我写了一个计算列表长度的函数,用 len() 内置函数很方便。

(感受:函数定义语法清晰,f-string 很实用。缩进规则需要特别注意。)

任务 3:条件判断 - if/elif/else

Python 的条件语句是 ifelif (注意不是 else if)、else,后面跟冒号和缩进代码块。逻辑运算符 andornot 感觉比 JS 的 &&||! 更接近自然语言。

experience_years = 1

if experience_years < 1:
    print("Python 新手")
elif experience_years >= 1 and experience_years < 3:
    print("Python 初学者")
else:
    print("Python 进阶中")

# 检查列表是否包含某元素,用 'in' 操作符
if "Python" in my_skills:
    print("技能栈里已经有 Python 了!")
else:
    print("技能栈里还没有 Python,得加上!")
    # 列表可以用 .append() 方法添加元素
    my_skills.append("Python")
    print("已添加 Python 到技能列表:", my_skills)

我的小练习: 我检查了技能列表里有没有 "Python",并用 .append() 方法加了进去。

(感受:elif 需要适应一下,in 操作符用来检查成员资格很方便。)

任务 4:循环遍历 - for 循环的 Python 风格

Python 的 for 循环主要用来遍历序列(如列表、字符串)或字典。感觉和 JS 的 for...offor...in 有点像,但用法更统一。

# 遍历我的技能列表
print("\n我的技能栈:")
for skill in my_skills:
    print(f"- {skill}")

# 遍历字典的键和值
print("\n我的档案详情:")
# .items() 方法可以同时获取键和值
for key, value in my_profile.items():
    print(f"{key} -> {value}")

# 遍历项目配置字典
print("\n项目配置:")
for config_key, config_value in project_config.items():
    print(f"{config_key}: {config_value}")

我的小练习: 我用 .items() 遍历了之前创建的项目配置字典。

(感受:for...in 遍历各种数据结构都很方便,特别是 .items() 处理字典,比 JS 遍历对象属性简洁。)

4. 我的下一步计划

通过 AI 的引导和这些小练习,我对 Python 的基础语法有了初步认识。和 JavaScript 相比,有很多相似之处,但也有 Python 独特的语法和风格,比如强制缩进、elif、列表和字典的方法等。

接下来,我准备在 AI 的帮助下继续探索:

  • 模块和包管理:Python 是怎么组织代码和管理依赖的?(AI 提到了 importpip
  • 面向对象编程:Python 的类和对象怎么用?和 JS 的原型链、Class 有什么不同?
  • Web 开发框架:了解一下 Django 和 Flask 这两个流行的 Python 后端框架。
  • 文件操作:学习如何读写文件。

感觉 Python 的生态也很庞大,有很多可以学习的地方。继续加油!

5. 模块、包和脚本入口:组织代码与执行

搞定了基础语法,我开始好奇 Python 是怎么组织代码的。在 Node.js 里,我们用 require 或者 import 来引入其他文件或者库。Python 也有类似的概念。

任务 5:模块导入 - import 的世界

Python 用 import 关键字来导入其他 Python 文件(称为模块)。这和 JS 的 ES6 import 很像。

第一步:创建我自己的模块

我在同一个目录下(比如桌面)创建了另一个 Python 文件,命名为 my_utils.py。内容如下:

# my_utils.py

def shout(text):
    return text.upper() + "!!!"

PI = 3.14159

这个文件定义了一个函数 shout 和一个常量 PI

第二步:在主文件里导入并使用

然后,我在之前的 hello.py (或者新建一个文件 main.py) 里导入并使用 my_utils

# main.py

# 导入整个模块
import my_utils

message = "python 很有趣"
shouted_message = my_utils.shout(message)
print(shouted_message) # 输出: PYTHON 很有趣!!!
print(f"圆周率大约是: {my_utils.PI}")

# 也可以只导入特定的函数或变量
from my_utils import shout

another_message = "只导入 shout 函数"
print(shout(another_message)) # 输出: 只导入 SHOUT 函数!!!

# 甚至可以给导入的东西起个别名
import my_utils as utils

print(f"用别名访问 PI: {utils.PI}")

我的小练习: 创建一个新的工具模块 string_tools.py,在里面定义一个函数 count_vowels(s) 用于计算字符串中元音字母(a, e, i, o, u, 大小写都算)的数量。然后在主脚本中导入并测试它。

(感受:import 的用法和 JS 类似,很容易理解。可以用 import module_name 或者 from module_name import something,还可以用 as 起别名,很灵活。)

任务 6:包管理器 - pip 对标 npm

Node.js 有 npmyarn 来管理第三方库。Python 的标准包管理器是 pip。Homebrew 安装 Python 时通常会自动安装 pip

我可以检查一下 pip 是否可用:

# 在终端里运行
pip3 --version

python3 一样,AI 建议我使用 pip3

安装一个包试试

就像用 npm install <package>,我可以用 pip3 install <package> 来安装 Python 包。我们来安装一个非常有名的 HTTP 请求库 requests

# 在终端里运行
pip3 install requests

AI 提醒我,像这样直接运行 pip3 install requests,默认情况下是把 requests 这个包安装到了 全局 的 Python 环境里。这有点像用 npm install -g <package> 把一个工具装到系统全局。

全局安装的问题:

这样做对于一些系统级的 Python 工具可能没问题,但对于项目开发来说,通常不是个好主意。为什么呢?

  1. 依赖冲突:想象一下,我的项目 A 需要 requests 版本 2.25,但另一个项目 B 需要版本 2.28。如果都装在全局,它们就会打架,一个项目能跑,另一个可能就报错了。这和 Node.js 里不同项目可能依赖不同版本的库,但可以通过本地 node_modules 解决是类似的。
  2. 项目隔离性差:全局安装让我很难清晰地知道某个特定项目到底依赖了哪些包和版本。当我需要把项目分享给别人,或者部署到服务器时,就很难确保环境一致性,容易出现"在我电脑上明明好好的"这种情况。

推荐的做法:虚拟环境 (Virtual Environments)

AI 告诉我,Python 开发的最佳实践是为每个项目创建一个 虚拟环境。虚拟环境就像是为项目创建了一个独立的、干净的 Python "沙箱"。在这个沙箱里,我可以安装项目所需的特定版本的包,它们只对这个项目可见,不会影响全局环境或其他项目。

这和 Node.js 每个项目都有自己的 node_modules 文件夹非常相似,依赖关系都记录在 package.json 里。Python 虚拟环境通常配合一个 requirements.txt 文件(类似 package.json)来管理依赖。

简单来说:

  • 全局安装 (pip3 install <package>):方便尝试,但容易造成混乱和冲突,不推荐用于项目开发。
  • 虚拟环境 + 项目安装:为每个项目创建隔离环境,清晰管理依赖,是专业开发的首选。

(我们稍后会学习如何创建和使用虚拟环境,现在先知道有这个概念就好。)

使用安装的包

安装后,就可以在 Python 代码里 import 并使用了:

import requests

try:
    # 尝试访问一个公开的 API
    response = requests.get('https://httpbin.org/get')
    response.raise_for_status() # 如果请求出错 (比如 404, 500), 这会抛出异常

    # 解析返回的 JSON 数据 (requests 会自动处理)
    data = response.json()
    print("请求成功! 响应数据中的 origin IP:", data['origin'])

except requests.exceptions.RequestException as e:
    print(f"请求失败: {e}")

更多 pip 与 npm 的对比操作:

除了安装,其他常用的操作和 npm 也有对应:

  • 移除模块

    • Python: pip uninstall <package>
    • Node.js: npm uninstall <package>npm remove <package>
    • 注意:和安装一样,推荐在激活的虚拟环境里操作。
  • 克隆仓库后安装依赖

    • Python: 当我从 GitHub 克隆一个 Python 项目后,如果它包含 requirements.txt 文件,我需要先创建并激活该项目的虚拟环境,然后运行:
      pip install -r requirements.txt
      
    • Node.js: 克隆后,通常是运行 npm installyarn install,它会读取 package.json 并下载依赖到 node_modules
    • 核心思路一致:都是通过一个清单文件 (requirements.txt vs package.json) 来恢复项目所需的依赖。
  • 删除所有依赖

    • Python: Python/pip 没有一个直接的命令像 rm -rf node_modules 那样"清空"所有依赖。因为依赖是安装到虚拟环境里的特定位置。最彻底的方法是:
      1. 退出虚拟环境 (deactivate)。
      2. 删除整个虚拟环境文件夹 (比如 rm -rf .venv)。
      3. 如果需要,可以重新创建虚拟环境 (python3 -m venv .venv) 并根据 requirements.txt 重新安装。
    • Node.js: 通常就是删除 node_modules 文件夹和可能的 package-lock.jsonyarn.lock
    • 关键区别:Python 更侧重于管理整个隔离的"环境",而不是仅仅一个依赖文件夹。
  • 构建最终应用

    • Python: 这个概念和前端的"构建"不太一样。Python 通常是解释型语言,不需要像 Webpack/Vite 那样把代码打包成最终的 JS bundle。
      • 对于 Web 应用(比如 FastAPI),所谓的"构建"更多是指 **部署"过程,可能包括:将代码和 requirements.txt 复制到服务器、创建虚拟环境、安装依赖、设置 Web 服务器(如 Gunicorn + Nginx)来运行 Uvicorn 和 FastAPI 应用、配置数据库等。
      • 对于一些需要分发的独立应用或库,可能会用到打包工具(如 PyInstaller、setuptools)来创建可执行文件或发布包,但这更接近库发布,而非前端的 build 过程。
    • Node.js: 前端通常是 npm run build,生成优化后的静态文件(HTML/CSS/JS)用于部署。后端 Node.js 应用的部署也可能涉及依赖安装和进程管理(如 PM2),但没有前端那种明确的"构建"步骤。
    • 总结:Python 应用(尤其是后端)的"构建"更偏向于环境准备和部署流程,而不是代码转换和打包。

(感受:pip 的基本用法和 npm 非常相似...)

7. 虚拟环境:告别全局混乱,拥抱项目隔离

之前 AI 提到,直接用 pip3 install 把包安装到全局不是个好习惯,容易导致项目间的依赖冲突。它推荐使用 虚拟环境。这和 Node.js 每个项目有自己的 node_modules 文件夹,依赖记录在 package.json 的思路非常相似。

Python 内置了一个创建虚拟环境的工具叫 venv

任务 8:创建并激活我的第一个虚拟环境

  1. 选择项目目录:首先,我得为我的新项目(比如就叫 fastapi-project)创建一个文件夹,并在终端里 cd 进去。
    mkdir fastapi-project
    cd fastapi-project
    
  2. 创建虚拟环境:在这个项目文件夹里,运行以下命令来创建一个名为 .venv 的虚拟环境。(.venv 是一个常用的约定名称,前面的点表示它是隐藏文件夹)
    python3 -m venv .venv
    
    这个命令会创建一个 .venv 文件夹,里面包含了独立的 Python 解释器副本和 pip 等工具。
  3. 激活虚拟环境:创建好后,需要激活它。激活后,我的终端提示符前面通常会显示虚拟环境的名字(比如 (.venv)),并且后续运行 pythonpip 命令时,使用的都是这个虚拟环境里的版本。
    • 在 macOS 或 Linux 上:
      source .venv/bin/activate
      
    • (如果用 Windows,命令是 .\.venv\Scripts\activate)
  4. 验证:激活后,我可以运行 which pythonwhich pip,会看到路径指向了 .venv 文件夹内部,而不是全局路径。
  5. 退出虚拟环境:当我完成工作,想退出虚拟环境时,只需在终端运行:
    deactivate
    

管理依赖:requirements.txt (Python 版 package.json)

在激活的虚拟环境里用 pip install <package> 安装的包,只会安装到这个环境里。为了记录项目依赖,方便别人或者部署时重建环境,通常会用一个 requirements.txt 文件。

  • 生成 requirements.txt:安装完所有依赖后,可以运行:
    pip freeze > requirements.txt
    
    这会把当前环境里所有已安装的包及其版本号写入 requirements.txt
  • 根据文件安装依赖:别人拿到我的项目后,只需创建并激活虚拟环境,然后运行:
    pip install -r requirements.txt
    
    pip 就会自动安装文件里列出的所有依赖。

我的小练习: 为我的 fastapi-project 创建并激活虚拟环境,然后尝试在虚拟环境里安装 requests 包,并生成 requirements.txt 文件看看。

(感受:虚拟环境的概念和 Node.js 的项目依赖管理思路一致,只是工具和文件名不同 (venv vs node_modules, requirements.txt vs package.json)。这下我可以放心地为不同项目安装不同版本的库了!)

问题 1:我执行完 pip install requests 需要手动写入 requirements.txt 文件吗?

回答 1:

不需要手动写入,但需要手动执行命令来生成或更新它。

pip install <package> 命令只负责将包安装到你的 Python 环境(理想情况是虚拟环境)中。它 不会 自动更新 requirements.txt

你需要在安装、卸载或更新依赖后,手动执行 pip freeze > requirements.txt 这个命令。这个命令会检查当前环境中所有已安装的包及其精确版本,并将这个列表输出(覆盖)到 requirements.txt 文件中。


问题 2:手动执行 pip freeze 感觉有点麻烦,没有什么更加智能的方案吗?业界最通用的方案是什么?

回答 2:

是的,有更智能、更通用的方案,并且是业界推荐的最佳实践。

虽然 pip + venv + requirements.txt 是 Python 内置的标准方式,但手动管理虚拟环境和手动更新 requirements.txt 确实不够方便。

业界更现代、更通用的解决方案是使用 依赖管理工具,最主流的是:

  1. Poetry: 使用 pyproject.toml 定义依赖,生成 poetry.lock 锁定精确版本。通过 poetry add <package> 添加依赖时会自动更新这两个文件。使用 poetry install 安装依赖。
  2. Pipenv: 使用 Pipfile 定义依赖,生成 Pipfile.lock 锁定精确版本。通过 pipenv install <package> 添加依赖时会自动更新这两个文件。使用 pipenv install 安装依赖。

这些工具自动化了虚拟环境管理和依赖文件的更新,提供了类似 npm/yarn 的体验,并具有更强大的依赖解析能力。对于新项目或严肃的项目开发,强烈推荐使用 Poetry 或 Pipenv


问题 3:为什么 npm install 能自动更新 package.json,而 Python 的 pip install 需要手动执行 pip freeze > requirements.txt 呢?

回答 3:

这主要是历史原因和设计哲学的差异:

  1. npm 的设计: npm 从一开始就是围绕 package.json 这个 项目清单文件 设计的。package.json 是项目的核心描述,包含元数据、依赖声明(你希望使用的包和版本范围)等。因此,npm install <package> 的核心职责之一就是更新这个项目清单,记录你的依赖"意图"。
  2. pip 的设计: pip 最初主要被设计为一个 包安装器。它的核心职责是根据指令(安装特定包或列表中的包)执行安装。它本身不强制依赖一个中心化的项目描述文件。
  3. requirements.txt 的起源: requirements.txt 文件(尤其是通过 pip freeze 生成的)最初更多地是作为一个 环境快照 或"安装脚本"的约定。pip freeze 的目的是"记录下当前环境里所有安装的包及其精确版本",以便能在别处精确复现这个环境。它记录的是安装后的 结果状态,而不是项目的依赖"意图"。

简单说: npm install 设计为"安装并记录意图",而 pip install 设计为"只管安装",记录环境状态是 pip freeze 这个独立命令的事。


问题 4:requirements.txt 文件是类似于 package.json 还是 package-lock.json / yarn.lock

回答 4:

通过 pip freeze > requirements.txt 生成的 requirements.txt 文件,其功能和内容更类似于 package-lock.jsonyarn.lock

  • package-lock.json / yarn.lock: 记录整个项目所有依赖(包括直接和间接依赖)的精确版本号和依赖树,用于保证环境的可复现性
  • requirements.txt (来自 pip freeze): 同样列出了当前环境中所有安装的包(直接和间接依赖)的精确版本号 (==),其主要目的也是通过 pip install -r requirements.txt精确复现环境

那么,Python 中类比 package.json(表达依赖意图、允许版本范围)的文件是什么呢?

在使用现代依赖管理工具时:

  • 对于 Poetry,是 pyproject.toml 文件中的 [tool.poetry.dependencies] 部分。
  • 对于 Pipenv,是 Pipfile 文件。

这些现代工具通过引入这两个文件(一个声明意图,一个锁定精确版本),提供了与 npm + package.json + package-lock.json 类似的工作流和优势。

8. Python 类与对象:不仅仅是数据容器

虽然 Python 可以用字典来表示结构化数据,但定义 类 (Class) 是更强大的方式,尤其是在构建复杂应用时。这和 JS 里用 Class 或者构造函数来创建对象类似。

任务 9:定义和使用我的第一个 Python 类

FastAPI 经常使用类来定义数据模型,所以先来了解基础。

class User:
    # __init__ 方法类似于 JS 类里的 constructor
    # 它在创建类的新实例 (对象) 时自动调用
    # 'self' 参数代表实例本身,类似于 JS 里的 'this'
    def __init__(self, username, email, age):
        print(f"正在创建一个新用户: {username}")
        # 这些是实例变量 (Instance Variables)
        self.username = username
        self.email = email
        self.age = age
        self.is_active = True # 可以有默认值

    # 这是类的一个方法 (Method)
    def greet(self):
        print(f"你好,我是 {self.username}!")

    def get_profile_info(self):
        return f"用户名: {self.username}, 邮箱: {self.email}, 年龄: {self.age}, 状态: {'活跃' if self.is_active else '不活跃'}"

# 创建类的实例 (对象)
user1 = User("Alice", "alice@example.com", 30)
user2 = User("Bob", "bob@test.com", 25)

# 访问实例变量
print(user1.username)
print(user2.email)

# 调用实例方法
user1.greet()
user2.greet()

print(user1.get_profile_info())

# 修改实例变量
user2.is_active = False
print(user2.get_profile_info())

我的小练习: 定义一个 Product 类,包含 name (字符串)、price (浮点数)、stock (整数) 三个实例变量,并添加一个 get_description 方法返回产品描述字符串。

(感受:Python 类的语法比 JS 的 Class 更简洁一点。__init__self 的用法需要习惯一下,但整体概念和 JS 面向对象差不多。这对于定义像 API 请求/响应体这样的数据结构很有用。)

9. 类型提示:让代码更清晰健壮 (FastAPI 的好朋友)

Python 是动态类型语言,通常不需要像 TypeScript 那样显式声明类型。但从 Python 3.5 开始,引入了 类型提示 (Type Hinting),允许我们为变量、函数参数和返回值添加类型标注。

不会改变 Python 的动态类型本质(解释器通常会忽略它们),但带来了很多好处:

  1. 提高代码可读性:一眼就能看出变量应该是什么类型,函数期望什么参数,返回什么结果。
  2. 编辑器和工具支持:像 VS Code 这样的编辑器可以利用类型提示提供更好的自动补全、错误检查 (配合 MyPy 等工具)。
  3. FastAPI 大量使用:FastAPI 利用类型提示来实现自动的数据校验、序列化和 API 文档生成!这是学习 FastAPI 必须掌握的。

任务 10:给我的代码加上类型提示

我们把之前的函数和类加上类型提示。

# 给函数加上类型提示
def greet(name: str) -> None: # -> None 表示这个函数不返回任何有意义的值
    greeting = f"你好, {name}! 很高兴你开始学习 Python。"
    print(greeting)

def calculate_list_length(items: list) -> int:
    return len(items)

# 给变量加上类型提示
my_name: str = "前端小明"
my_skills: list[str] = ["JavaScript", "React"] # 可以指定列表里元素的类型
project_config: dict[str, any] = { # 可以指定字典键和值的类型, any 表示任意类型
    "projectName": "MyAwesomeApp",
    "version": "1.0.0",
    "dependencies": ["react", "next"]
}

# 给类加上类型提示
from typing import List, Dict, Any # 复杂类型可能需要从 typing 模块导入

class User:
    # 类变量的类型提示 (写在 __init__ 外面)
    username: str
    email: str
    age: int
    is_active: bool

    def __init__(self, username: str, email: str, age: int) -> None:
        print(f"正在创建一个新用户: {username}")
        self.username = username
        self.email = email
        self.age = age
        self.is_active = True

    def greet(self) -> None:
        print(f"你好,我是 {self.username}!")

    def get_profile_info(self) -> str:
        status = '活跃' if self.is_active else '不活跃'
        return f"用户名: {self.username}, 邮箱: {self.email}, 年龄: {self.age}, 状态: {status}"

# 使用时保持不变
user1 = User("Alice", "alice@example.com", 30)
print(user1.get_profile_info())

我的小练习: 给你的 Product 类和 string_tools.py 里的 count_vowels 函数加上类型提示。

(感受:类型提示的语法 :-> 很直观,有点像 TypeScript。虽然 Python 不强制执行,但写上后代码确实清晰多了,而且知道 FastAPI 会用到它,这让我更有动力去使用。)

10. Async/Await:Python 的异步编程

JavaScript 里处理异步操作(比如网络请求、定时器)常用 async/await。Python 也有几乎一样的机制!这对于需要处理并发请求的 Web 服务器(比如 FastAPI 应用)至关重要。

Python 的异步基于 asyncio 库。

任务 11:编写和运行我的第一个 Python 异步函数

import asyncio
import time

# 使用 'async def' 定义一个异步函数
async def fetch_data(url: str) -> dict:
    print(f"开始获取数据从 {url}...")
    # 'await asyncio.sleep(2)' 模拟一个耗时 2 秒的网络请求
    # 在等待期间,事件循环可以去执行其他任务
    await asyncio.sleep(2)
    print(f"...数据获取完成从 {url}")
    # 假设返回了一些数据
    return {"url": url, "data": f"一些来自 {url} 的数据"}

# 定义另一个异步函数,调用 fetch_data
async def main():
    start_time = time.time()
    print("开始执行 main 函数")

    # 使用 asyncio.gather() 可以并发运行多个异步任务
    # 这类似于 JS 里的 Promise.all()
    task1 = fetch_data("url_1")
    task2 = fetch_data("url_2")

    results = await asyncio.gather(task1, task2)

    print("\n所有任务完成!")
    for result in results:
        print(f"获取到的结果: {result}")

    end_time = time.time()
    print(f"\n总耗时: {end_time - start_time:.2f} 秒") # 注意时间,应该接近 2 秒,而不是 4 

# 运行异步代码
# 如果你在 Jupyter Notebook  IPython 环境,可以直接 await main()
# 在普通的 .py 脚本里,需要用 asyncio.run() 来启动事件循环并运行顶层异步函数
if __name__ == "__main__":
    asyncio.run(main())

我的小练习: 修改 main 函数,让它按顺序(而不是并发)获取两次数据(即 await fetch_data("url_1") 然后 await fetch_data("url_2")),观察总耗时有什么变化。

(感受:async defawait 的用法和 JavaScript 几乎一模一样!asyncio.sleep 就像 setTimeout 的异步版本。asyncio.gather 对应 Promise.all,也很容易理解。这让我对接 FastAPI 的异步特性信心大增。)

问题 1:我想知道和 Nodejs 对比,python3 的异步如何捕获 & 处理错误?

好的,我们来对比一下 Python 3 的 asyncio 和 Node.js 在异步错误处理上的异同。

核心相似点:async/awaittry...except/catch

最核心也是最常用的方式,两者都非常相似:都推荐使用 async/await 语法,并通过各自语言的异常处理机制来捕获错误。

  • Python (asyncio): 使用 try...except 块。
  • Node.js (modern): 使用 try...catch 块。

当你 await 一个可能抛出异常的异步函数(在 Python 中称为协程 coroutine)时,如果该函数内部发生了错误并且没有被内部捕获,异常会像同步代码一样向上冒泡,可以被外层的 try...except (Python) 或 try...catch (Node.js) 捕获。

Python asyncio 示例:

import asyncio
import random

async def potentially_failing_task(task_id):
    print(f"任务 {task_id}: 开始")
    await asyncio.sleep(random.uniform(0.1, 0.5))
    if random.random() < 0.3: # 模拟 30% 的失败率
        print(f"任务 {task_id}: 失败!")
        raise ValueError(f"任务 {task_id} 遇到错误")
    print(f"任务 {task_id}: 成功")
    return f"结果来自任务 {task_id}"

async def main():
    try:
        print("尝试执行单个可能失败的任务...")
        result = await potentially_failing_task(1)
        print(f"任务成功,结果: {result}")
    except ValueError as e:
        print(f"捕获到任务错误: {e}")
    except Exception as e:
        print(f"捕获到其他类型的任务错误: {e}")

    print("\n尝试执行另一个任务...")
    # 如果这里失败,异常会传递给 asyncio.run,如果没有更高层级的捕获,程序会终止并打印 Traceback
    await potentially_failing_task(2)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except Exception as e:
         #  asyncio.run 外层也可以捕获 main() 中未被捕获的异常
         print(f"在 asyncio.run 外层捕获到未处理的异常: {e}")

# 可能的输出 1:
# 尝试执行单个可能失败的任务...
# 任务 1: 开始
# 任务 1: 成功
# 任务成功,结果: 结果来自任务 1
#
# 尝试执行另一个任务...
# 任务 2: 开始
# 任务 2: 失败!
#  asyncio.run 外层捕获到未处理的异常: 任务 2 遇到错误

# 可能的输出 2:
# 尝试执行单个可能失败的任务...
# 任务 1: 开始
# 任务 1: 失败!
# 捕获到任务错误: 任务 1 遇到错误
#
# 尝试执行另一个任务...
# 任务 2: 开始
# 任务 2: 成功

处理并发任务中的错误 (asyncio.gather vs. Promise.all/Promise.allSettled)

当需要同时运行多个异步任务并等待它们全部完成时,错误处理方式有些差异:

  • Python (asyncio.gather):

    • 默认行为 (fail-fast): 如果 gather 中的任何一个任务抛出异常,gather立即 将第一个遇到的异常向上抛出,并且 不会等待 其他任务完成(正在运行的任务可能会继续运行一会儿,但 gather 本身不等它们了)。这类似于 Node.js 的 Promise.all 的默认行为。
    • 捕获所有结果/异常 (return_exceptions=True): 如果你希望 gather 等待所有任务结束,无论成功还是失败,并将异常作为结果返回而不是直接抛出,可以设置 return_exceptions=True。这样,gather 的返回结果列表中,成功任务对应的是返回值,失败任务对应的则是该任务抛出的异常对象。这非常类似于 Node.js 的 Promise.allSettled
  • Node.js:

    • Promise.all([...]): 行为类似于 asyncio.gather 的默认行为。只要有一个 Promise rejected,Promise.all 就会立即 reject,并带有第一个拒绝的原因。
    • Promise.allSettled([...]): 行为类似于 asyncio.gather(*tasks, return_exceptions=True)。它总是会等待所有 Promise 完成(无论是 resolved 还是 rejected),然后返回一个包含每个 Promise 状态和结果/原因的对象数组。

Python asyncio.gather 错误处理示例:

import asyncio
import random

async def potentially_failing_task(task_id):
    print(f"任务 {task_id}: 开始")
    await asyncio.sleep(random.uniform(0.1, 1)) # 增加随机性
    if random.random() < 0.5: # 模拟 50% 的失败率
        print(f"任务 {task_id}: 失败!")
        raise ValueError(f"任务 {task_id} 遇到错误")
    print(f"任务 {task_id}: 成功")
    return f"结果来自任务 {task_id}"

async def main_gather_default():
    print("--- gather 默认行为 (fail-fast) ---")
    tasks = [potentially_failing_task(i) for i in range(1, 4)]
    try:
        results = await asyncio.gather(*tasks) # 注意这里的 * 解包
        print("所有任务成功完成!")
        for result in results:
            print(f"结果: {result}")
    except Exception as e:
        print(f"捕获到 gather 中的错误: {e}")
        # 注意:此时其他任务可能仍在后台运行,但 gather 已返回

async def main_gather_return_exceptions():
    print("\n--- gather return_exceptions=True ---")
    tasks = [potentially_failing_task(i) for i in range(4, 7)]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    print("gather 完成,检查结果:")
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            print(f"任务 {i+4} 失败: {result}")
        else:
            print(f"任务 {i+4} 成功: {result}")

if __name__ == "__main__":
    asyncio.run(main_gather_default())
    print("-" * 20)
    # 等待一小会儿,让可能仍在后台运行的任务有机会结束(仅为演示)
    asyncio.run(asyncio.sleep(1.5))
    print("-" * 20)
    asyncio.run(main_gather_return_exceptions())


# 可能的输出:
# --- gather 默认行为 (fail-fast) ---
# 任务 1: 开始
# 任务 2: 开始
# 任务 3: 开始
# 任务 1: 成功
# 任务 3: 失败!
# 捕获到 gather 中的错误: 任务 3 遇到错误
# 任务 2: 成功  <-- 注意这个可能在捕获错误之后才打印,说明它仍在运行
# --------------------
# (等待 1.5 秒)
# --------------------
#
# --- gather return_exceptions=True ---
# 任务 4: 开始
# 任务 5: 开始
# 任务 6: 开始
# 任务 5: 失败!
# 任务 4: 成功
# 任务 6: 成功
# gather 完成,检查结果:
# 任务 4 成功: 结果来自任务 4
# 任务 5 失败: 任务 5 遇到错误
# 任务 6 成功: 结果来自任务 6

与 Node.js 回调 (Callback) 模式对比

Python 的 asyncio 基本不使用 Node.js 早期常见的那种错误优先的回调函数风格 (callback(err, data))。现代 Python 异步编程几乎完全围绕 async/awaittry/except 构建,或者使用 Future 对象(类似于 Promise,但 async/await 更常用)。

总结:

  • 对于单个异步操作的错误处理,Python asynciotry...except await ... 与 Node.js 的 try...catch await ... 几乎完全相同,是推荐的方式。
  • 对于并发任务,asyncio.gather 默认行为像 Promise.all (fail-fast),而 asyncio.gather(..., return_exceptions=True) 提供了类似 Promise.allSettled 的能力,可以收集所有任务的结果或异常。
  • Python asyncio 不依赖 Node.js 早期那种错误优先的回调模式。

总的来说,如果你熟悉 Node.js 的 async/awaitPromise.all/Promise.allSettled,你会发现 Python asyncio 的错误处理模式非常相似且易于理解。

问题 2: 我是前端开发者,刚接触 python3,如何知道我可以怎么找 asyncio 的包,在哪里找它的 API 文档呢?不要迎合我,给我业界最专业的最常用的方式。

好的,作为一名前端开发者,你可能习惯于通过 npm 或 yarn 来管理包,并通过在线文档(如 MDN 或特定库的网站)查找 API。Python 生态系统有类似但略有不同的方式。

对于 asyncio 这个具体的库,它比较特殊,因为它是 Python 的 标准库 的一部分。这意味着你不需要额外安装它,它随着 Python 的安装就自带了。

查找 asyncio 或任何 Python 包(无论是标准库还是第三方库)的 API 文档,最专业且常用的方式主要有以下几种:

  1. 官方 Python 文档:

    • 对于标准库(如 asyncio, os, json, datetime 等),最权威、最全面的信息来源是 Python 官方文档网站。你可以直接访问 docs.python.org,选择你使用的 Python 版本,然后在库索引 (Library Reference) 中找到 asyncio
    • 这是查找标准库 API 最推荐的方式,信息准确且更新及时。
  2. 使用内置 help() 函数:

    • 在 Python 交互式解释器 (直接在终端输入 pythonipython 进入) 中,你可以导入模块,然后使用 help() 函数查看其文档字符串 (docstrings)。
    • 示例:
      import asyncio
      help(asyncio) # 查看整个 asyncio 模块的文档
      help(asyncio.gather) # 查看特定函数 asyncio.gather 的文档
      
    • 这种方式适合快速查看某个函数或类的基本用法和参数,无需离开终端。ipython 在这方面体验通常更好,有语法高亮和更好的分页显示。
  3. 使用 dir() 函数探索:

    • 同样在 Python 解释器中,dir() 函数可以列出一个模块或对象包含的所有属性和方法名。这有助于你了解一个模块大致提供了哪些功能。
    • 示例:
      import asyncio
      dir(asyncio)
      
    • 结合 help() 使用 dir() 是探索不熟悉的库的常用技巧。
  4. IDE/代码编辑器的集成功能:

    • 现代的代码编辑器(如 VS Code, PyCharm)通常集成了强大的 Python 支持。当你导入一个模块并使用其函数或类时:
      • 代码提示/自动补全: 会显示可用的方法和属性。
      • 悬停提示 (Hover): 将鼠标悬停在函数或类名上,通常会显示其简要文档 (docstring)。
      • 跳转到定义 (Go to Definition): 可以直接跳转到源代码或类型定义文件,查看其实现和详细文档。
    • 这是日常开发中非常高频的使用方式,效率很高。

总结:

  • 首选: 查阅 Python 官方文档 (docs.python.org) 获取最权威、最全面的信息,特别是对于标准库 asyncio
  • 快速查看/探索:Python 解释器 中使用 help()dir()
  • 日常开发: 依赖 IDE/编辑器 的代码提示、悬停文档和跳转定义功能。

对于第三方库(例如 requests, numpy, pandas 等),除了上述方法(特别是 IDE 集成和 help()),它们通常也有自己独立的文档网站,通过搜索引擎(如 Google 搜索 "requests python documentation")很容易找到。

11. FastAPI 初体验:搭建我的第一个 API

终于到了激动人心的时刻!我们已经掌握了虚拟环境、类、类型提示和异步编程的基础,可以开始尝试 FastAPI 了。

FastAPI 是一个现代、快速(高性能)的 Python Web 框架,用于构建 API。它的很多设计理念(比如基于 ASGI 的异步特性、依赖注入)可能让我想起 Node.js 的一些框架(如 Express/Koa/NestJS)。

任务 12:创建并运行一个最简单的 FastAPI 应用

  1. 确保在虚拟环境里:首先,确认我已经激活了之前为 fastapi-project 创建的 .venv 环境。

    source .venv/bin/activate
    
  2. 安装 FastAPI 和 Uvicorn:FastAPI 需要一个 ASGI 服务器来运行,Uvicorn 是官方推荐的。用 pip 安装它们。

    pip install fastapi "uvicorn[standard]"
    

    ("uvicorn[standard]" 会安装 Uvicorn 以及一些推荐的依赖,提供更好的性能和特性)。安装后,记得更新 requirements.txt (pip freeze > requirements.txt)。

  3. 创建主应用文件 main.py:在 fastapi-project 目录下创建 main.py 文件,内容如下:

    # main.py
    from fastapi import FastAPI
    
    # 创建 FastAPI 应用实例
    # 这有点像 const app = express();
    app = FastAPI()
    
    # 定义一个路径操作 (Route)
    # @app.get("/") 是一个装饰器 (Decorator),它告诉 FastAPI
    # 下面的函数 `read_root` 将处理对路径 "/"  GET 请求
    @app.get("/")
    async def read_root(): # 注意这里用了 async def
        # 返回的内容可以是字典、列表、字符串、数字等
        # FastAPI 会自动将其转换为 JSON 响应
        return {"message": "Hello, FastAPI World!"}
    
    # 再定义一个带路径参数的 GET 请求
    @app.get("/items/{item_id}")
    async def read_item(item_id: int): # FastAPI 利用类型提示进行路径参数校验
        return {"item_id": item_id, "description": f"这是第 {item_id} 号物品"}
    
    # 在这里加上 __main__ 判断,虽然 uvicorn 通常不会直接运行这个
    if __name__ == "__main__":
        print("要运行 FastAPI 应用, 请使用 uvicorn 命令:")
        print("uvicorn main:app --reload")
    
  4. 运行应用:在终端(确保处于 fastapi-project 目录且虚拟环境已激活),运行 Uvicorn 服务器:

    uvicorn main:app --reload
    
    • main: 指的是 main.py 文件(Python 模块)。
    • app: 指的是在 main.py 文件中创建的 FastAPI() 实例对象。
    • --reload: 这个参数让服务器在代码更改后自动重启,非常适合开发环境(类似 nodemon)。
  5. 访问 API:终端会显示类似 Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 的信息。现在我可以在浏览器里打开:

    • http://127.0.0.1:8000/ -> 会看到 {"message":"Hello, FastAPI World!"}
    • http://127.0.0.1:8000/items/5 -> 会看到 {"item_id":5,"description":"这是第 5 号物品"}
    • http://127.0.0.1:8000/items/abc -> 会看到错误信息,因为路径参数 item_id 被类型提示为 int,FastAPI 自动校验了。
    • 自动交互式文档:FastAPI 还有一个超赞的功能,它会自动生成 API 文档!打开 http://127.0.0.1:8000/docs 可以看到 Swagger UI 界面,在这里可以直接测试 API。

我的小练习: 添加一个新的 GET 路径 /users/me,让它返回一个包含我的名字和技能列表的字典。

(感受:FastAPI 的启动和基本路由定义非常简洁!装饰器 @app.get 的用法很新颖,但容易理解。基于类型提示的自动数据校验和自动生成的交互式文档简直是神器,比以前用 Node.js 配 Swagger 要方便太多了!异步函数的支持也很自然。)

12. Pydantic 模型:定义 API 的数据结构

前面我们用字典返回 JSON,但对于复杂的请求体或响应体,直接用字典不够健壮,也难以清晰地定义数据结构。FastAPI 推荐(并深度集成)使用 Pydantic 这个库来定义数据模型。

Pydantic 模型是用 Python 类和类型提示来定义的,它能提供:

  1. 数据校验:自动检查传入的数据是否符合模型定义的类型和约束。
  2. 数据转换:如果可能,会自动进行类型转换(比如字符串转数字)。
  3. 编辑器支持:获得类属性的自动补全。
  4. 文档生成:FastAPI 会根据 Pydantic 模型自动生成请求体和响应体的 Schema 文档。

这非常像在 TypeScript 里定义接口 (Interface) 或类型 (Type) 来约束数据结构,或者使用 Zod 等库进行运行时校验。

任务 13:使用 Pydantic 定义请求体和响应体

  1. 安装 Pydantic:虽然 FastAPI 通常会附带安装 Pydantic,但明确安装一下或确保版本没问题总是好的。

    pip install pydantic
    

    (记得更新 requirements.txt)

  2. 定义 Pydantic 模型:在 main.py 里定义一个表示"物品"的模型。

    # main.py
    from fastapi import FastAPI
    from pydantic import BaseModel # 导入 BaseModel
    from typing import Optional # 用于可选字段
    
    app = FastAPI()
    
    # 定义 Item 模型
    class Item(BaseModel):
        name: str
        description: Optional[str] = None # 可选字段,默认值为 None
        price: float
        tax: Optional[float] = None
    
    @app.get("/")
    async def read_root():
        return {"message": "Hello, FastAPI World!"}
    
    # 使用 Path 参数
    @app.get("/items/{item_id}")
    async def read_item(item_id: int):
        return {"item_id": item_id, "description": f"这是第 {item_id} 号物品"}
    
    # 使用 POST 请求和请求体 (Request Body)
    # FastAPI 会自动将请求体 JSON 数据解析并校验为 Item 对象
    @app.post("/items/")
    async def create_item(item: Item): # 参数类型是 Item 模型
        print("接收到的物品:", item)
        item_dict = item.dict() # Pydantic 模型可以转为字典
        if item.tax:
            price_with_tax = item.price + item.tax
            item_dict.update({"price_with_tax": price_with_tax})
        return item_dict
    
    # 使用 Pydantic 模型作为响应模型 (Response Model)
    # 可以在路径操作装饰器中用 response_model 参数指定
    # FastAPI 会确保返回的数据符合 Item 模型的结构
    # (即使你返回了多余的字段,也会被过滤掉)
    @app.get("/items/default/", response_model=Item)
    async def get_default_item():
        # 假设我们从数据库或其他地方获取了数据
        default_item_data = {
            "name": "默认物品",
            "description": "这是一个默认物品的描述",
            "price": 9.99,
            "tax": 0.99,
            "extra_field": "这个字段不会包含在响应里"
        }
        return default_item_data
    
  3. 测试

    • 运行 uvicorn main:app --reload
    • 打开 http://127.0.0.1:8000/docs
    • 找到 /items/ 的 POST 请求,点击 "Try it out"。
    • 在 Request body 里输入 JSON 数据,比如:
      {
        "name": "我的新物品",
        "price": 19.99
      }
      
      点击 "Execute",可以看到 FastAPI 成功接收并处理了数据,返回了包含 price_with_tax (如果提供了 tax) 的字典。
    • 尝试发送缺少 nameprice 的请求,会看到 FastAPI 自动返回 422 Unprocessable Entity 错误,并指出哪个字段有问题。
    • 访问 /items/default/ GET 请求,查看返回的 JSON 数据是否只包含了 Item 模型定义的字段。

我的小练习: 定义一个 UserCreate Pydantic 模型,包含 username (str) 和 email (str) 字段。然后创建一个 POST /users/ 路径操作,接收 UserCreate 模型作为请求体,并简单地返回接收到的用户信息。

(感受:Pydantic 和 FastAPI 的结合太强大了!用 Class 和类型提示来定义数据结构,就能自动获得请求校验、响应过滤和 API 文档,这开发体验比 Node.js 手动处理校验或者配置各种库要流畅得多。Optional 类型处理可选字段也很方便。)

Pydantic 基本操作

api 文档
使用 Pydantic,可以定义一个模型类,该类需要继承 pydantic 中的 BaseModel 类,模型类描述了数据的结构和类型,并指定验证规则。

然后,可以使用这个模型类来验证输入的数据是否符合预期,并以类型安全的方式访问和操作数据。

from pydantic import BaseModel, ValidationError

class User(BaseModel):
    name: str
    age: int
    email: str

try:
    user = User(name="Alice", age="30", email="alice@example.com")
except ValidationError as e:
    print(e.json())

如果创建实例的数据不符合类型注解的要求,则会报 ValidationError 错误。

13. 继续深入 FastAPI 和下一步

掌握了 FastAPI 的基础、Pydantic 模型和异步编程后,我已经具备了构建简单 API 的能力。

为了写真实的应用,AI 建议我接下来重点学习 FastAPI 的这些方面:

  • 请求参数详解:Query 参数、Path 参数、Body 参数的更多用法和校验。
  • 依赖注入 (Dependency Injection):FastAPI 的核心特性之一,用于代码复用、共享连接(如数据库连接)等。
  • 路由组织 (APIRouter):将大型应用的路由拆分到不同文件。
  • 错误处理:自定义异常处理器。
  • 中间件 (Middleware):处理请求日志、认证等。
  • 数据库交互:学习如何连接数据库(比如用 SQLAlchemy 或 ORM)并在 FastAPI 中使用。
  • 测试:为 FastAPI 应用编写测试。

看起来我的 Python 和 FastAPI 之旅还有很长的路要走,但现在我已经有了清晰的方向和动手实践的基础。是时候开始构思我的第一个 FastAPI 小应用了!

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 开发非常重要。