这篇文档记录了我从 0 开始,在 AI 的帮助下学习 Python 并上线我的短网址应用的全过程。
应用在线地址: https://fastapi.zilin.im/
应用源代码 Github: https://github.com/hifizz/shorturl-fastapi
为什么选择短网址这个项目来练习?
由这段 Prompt 开始了我的 Python 学习之旅 👇🏻
你是一个高级 Python 开发工程师。现在教我如何一步一步学习 Python。
我是一个前端开发者,非常熟悉 Nodejs 和 Vue.js React.js,Next.js ;我的操作系统是 macOS;请结合我的背景继续给我小任务帮我一步一步学习 python,然后输出到这个文档中。
注意:请以我在 AI 的帮助下学习 Python 的视角来编写这边文档。
---从这里开始就是AI在我的指挥下生成的文档了👇🏻---
我的 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
可能还是指向系统自带的旧版本。
按照传统,我得先让程序说 "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 程序跑起来了!
"Hello, World!" 成功后,我开始尝试一些更具体的语法,AI 帮我对比了 Python 和我熟悉的 JavaScript。
我发现 Python 定义变量比 JS 还简单,不用 let
或 const
,直接写变量名赋值就行。
# 我定义的几个变量
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 差别不大,列表和字典的概念也很容易对应。)
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 很实用。缩进规则需要特别注意。)
if/elif/else
Python 的条件语句是 if
、elif
(注意不是 else if
)、else
,后面跟冒号和缩进代码块。逻辑运算符 and
、or
、not
感觉比 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
操作符用来检查成员资格很方便。)
for
循环的 Python 风格Python 的 for
循环主要用来遍历序列(如列表、字符串)或字典。感觉和 JS 的 for...of
或 for...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 遍历对象属性简洁。)
通过 AI 的引导和这些小练习,我对 Python 的基础语法有了初步认识。和 JavaScript 相比,有很多相似之处,但也有 Python 独特的语法和风格,比如强制缩进、elif
、列表和字典的方法等。
接下来,我准备在 AI 的帮助下继续探索:
import
和 pip
)感觉 Python 的生态也很庞大,有很多可以学习的地方。继续加油!
搞定了基础语法,我开始好奇 Python 是怎么组织代码的。在 Node.js 里,我们用 require
或者 import
来引入其他文件或者库。Python 也有类似的概念。
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
起别名,很灵活。)
pip
对标 npm
Node.js 有 npm
或 yarn
来管理第三方库。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 工具可能没问题,但对于项目开发来说,通常不是个好主意。为什么呢?
requests
版本 2.25,但另一个项目 B 需要版本 2.28。如果都装在全局,它们就会打架,一个项目能跑,另一个可能就报错了。这和 Node.js 里不同项目可能依赖不同版本的库,但可以通过本地 node_modules
解决是类似的。推荐的做法:虚拟环境 (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 也有对应:
移除模块:
pip uninstall <package>
npm uninstall <package>
或 npm remove <package>
克隆仓库后安装依赖:
requirements.txt
文件,我需要先创建并激活该项目的虚拟环境,然后运行:
pip install -r requirements.txt
npm install
或 yarn install
,它会读取 package.json
并下载依赖到 node_modules
。requirements.txt
vs package.json
) 来恢复项目所需的依赖。删除所有依赖:
rm -rf node_modules
那样"清空"所有依赖。因为依赖是安装到虚拟环境里的特定位置。最彻底的方法是:
deactivate
)。rm -rf .venv
)。python3 -m venv .venv
) 并根据 requirements.txt
重新安装。node_modules
文件夹和可能的 package-lock.json
或 yarn.lock
。构建最终应用:
requirements.txt
复制到服务器、创建虚拟环境、安装依赖、设置 Web 服务器(如 Gunicorn + Nginx)来运行 Uvicorn 和 FastAPI 应用、配置数据库等。npm run build
,生成优化后的静态文件(HTML/CSS/JS)用于部署。后端 Node.js 应用的部署也可能涉及依赖安装和进程管理(如 PM2),但没有前端那种明确的"构建"步骤。(感受:pip 的基本用法和 npm 非常相似...)
之前 AI 提到,直接用 pip3 install
把包安装到全局不是个好习惯,容易导致项目间的依赖冲突。它推荐使用 虚拟环境。这和 Node.js 每个项目有自己的 node_modules
文件夹,依赖记录在 package.json
的思路非常相似。
Python 内置了一个创建虚拟环境的工具叫 venv
。
fastapi-project
)创建一个文件夹,并在终端里 cd
进去。
mkdir fastapi-project
cd fastapi-project
.venv
的虚拟环境。(.venv
是一个常用的约定名称,前面的点表示它是隐藏文件夹)
python3 -m venv .venv
这个命令会创建一个 .venv
文件夹,里面包含了独立的 Python 解释器副本和 pip
等工具。(.venv)
),并且后续运行 python
和 pip
命令时,使用的都是这个虚拟环境里的版本。
source .venv/bin/activate
.\.venv\Scripts\activate
)which python
或 which pip
,会看到路径指向了 .venv
文件夹内部,而不是全局路径。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
)。这下我可以放心地为不同项目安装不同版本的库了!)
pip install requests
需要手动写入 requirements.txt
文件吗?回答 1:
不需要手动写入,但需要手动执行命令来生成或更新它。
pip install <package>
命令只负责将包安装到你的 Python 环境(理想情况是虚拟环境)中。它 不会 自动更新 requirements.txt
。
你需要在安装、卸载或更新依赖后,手动执行 pip freeze > requirements.txt
这个命令。这个命令会检查当前环境中所有已安装的包及其精确版本,并将这个列表输出(覆盖)到 requirements.txt
文件中。
pip freeze
感觉有点麻烦,没有什么更加智能的方案吗?业界最通用的方案是什么?回答 2:
是的,有更智能、更通用的方案,并且是业界推荐的最佳实践。
虽然 pip
+ venv
+ requirements.txt
是 Python 内置的标准方式,但手动管理虚拟环境和手动更新 requirements.txt
确实不够方便。
业界更现代、更通用的解决方案是使用 依赖管理工具,最主流的是:
pyproject.toml
定义依赖,生成 poetry.lock
锁定精确版本。通过 poetry add <package>
添加依赖时会自动更新这两个文件。使用 poetry install
安装依赖。Pipfile
定义依赖,生成 Pipfile.lock
锁定精确版本。通过 pipenv install <package>
添加依赖时会自动更新这两个文件。使用 pipenv install
安装依赖。这些工具自动化了虚拟环境管理和依赖文件的更新,提供了类似 npm
/yarn
的体验,并具有更强大的依赖解析能力。对于新项目或严肃的项目开发,强烈推荐使用 Poetry 或 Pipenv。
npm install
能自动更新 package.json
,而 Python 的 pip install
需要手动执行 pip freeze > requirements.txt
呢?回答 3:
这主要是历史原因和设计哲学的差异:
npm
的设计: npm
从一开始就是围绕 package.json
这个 项目清单文件 设计的。package.json
是项目的核心描述,包含元数据、依赖声明(你希望使用的包和版本范围)等。因此,npm install <package>
的核心职责之一就是更新这个项目清单,记录你的依赖"意图"。pip
的设计: pip
最初主要被设计为一个 包安装器。它的核心职责是根据指令(安装特定包或列表中的包)执行安装。它本身不强制依赖一个中心化的项目描述文件。requirements.txt
的起源: requirements.txt
文件(尤其是通过 pip freeze
生成的)最初更多地是作为一个 环境快照 或"安装脚本"的约定。pip freeze
的目的是"记录下当前环境里所有安装的包及其精确版本",以便能在别处精确复现这个环境。它记录的是安装后的 结果状态,而不是项目的依赖"意图"。简单说: npm install
设计为"安装并记录意图",而 pip install
设计为"只管安装",记录环境状态是 pip freeze
这个独立命令的事。
requirements.txt
文件是类似于 package.json
还是 package-lock.json
/ yarn.lock
?回答 4:
通过 pip freeze > requirements.txt
生成的 requirements.txt
文件,其功能和内容更类似于 package-lock.json
或 yarn.lock
。
package-lock.json
/ yarn.lock
: 记录整个项目所有依赖(包括直接和间接依赖)的精确版本号和依赖树,用于保证环境的可复现性。requirements.txt
(来自 pip freeze
): 同样列出了当前环境中所有安装的包(直接和间接依赖)的精确版本号 (==
),其主要目的也是通过 pip install -r requirements.txt
来精确复现环境。那么,Python 中类比 package.json
(表达依赖意图、允许版本范围)的文件是什么呢?
在使用现代依赖管理工具时:
pyproject.toml
文件中的 [tool.poetry.dependencies]
部分。Pipfile
文件。这些现代工具通过引入这两个文件(一个声明意图,一个锁定精确版本),提供了与 npm
+ package.json
+ package-lock.json
类似的工作流和优势。
虽然 Python 可以用字典来表示结构化数据,但定义 类 (Class) 是更强大的方式,尤其是在构建复杂应用时。这和 JS 里用 Class 或者构造函数来创建对象类似。
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 请求/响应体这样的数据结构很有用。)
Python 是动态类型语言,通常不需要像 TypeScript 那样显式声明类型。但从 Python 3.5 开始,引入了 类型提示 (Type Hinting),允许我们为变量、函数参数和返回值添加类型标注。
这不会改变 Python 的动态类型本质(解释器通常会忽略它们),但带来了很多好处:
我们把之前的函数和类加上类型提示。
# 给函数加上类型提示
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 会用到它,这让我更有动力去使用。)
JavaScript 里处理异步操作(比如网络请求、定时器)常用 async/await
。Python 也有几乎一样的机制!这对于需要处理并发请求的 Web 服务器(比如 FastAPI 应用)至关重要。
Python 的异步基于 asyncio
库。
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 def
和 await
的用法和 JavaScript 几乎一模一样!asyncio.sleep
就像 setTimeout
的异步版本。asyncio.gather
对应 Promise.all
,也很容易理解。这让我对接 FastAPI 的异步特性信心大增。)
好的,我们来对比一下 Python 3 的 asyncio
和 Node.js 在异步错误处理上的异同。
核心相似点:async/await
和 try...except/catch
最核心也是最常用的方式,两者都非常相似:都推荐使用 async/await
语法,并通过各自语言的异常处理机制来捕获错误。
asyncio
): 使用 try...except
块。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
):
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/await
和 try/except
构建,或者使用 Future
对象(类似于 Promise,但 async/await
更常用)。
总结:
asyncio
的 try...except await ...
与 Node.js 的 try...catch await ...
几乎完全相同,是推荐的方式。asyncio.gather
默认行为像 Promise.all
(fail-fast),而 asyncio.gather(..., return_exceptions=True)
提供了类似 Promise.allSettled
的能力,可以收集所有任务的结果或异常。asyncio
不依赖 Node.js 早期那种错误优先的回调模式。总的来说,如果你熟悉 Node.js 的 async/await
和 Promise.all
/Promise.allSettled
,你会发现 Python asyncio
的错误处理模式非常相似且易于理解。
好的,作为一名前端开发者,你可能习惯于通过 npm 或 yarn 来管理包,并通过在线文档(如 MDN 或特定库的网站)查找 API。Python 生态系统有类似但略有不同的方式。
对于 asyncio
这个具体的库,它比较特殊,因为它是 Python 的 标准库 的一部分。这意味着你不需要额外安装它,它随着 Python 的安装就自带了。
查找 asyncio
或任何 Python 包(无论是标准库还是第三方库)的 API 文档,最专业且常用的方式主要有以下几种:
官方 Python 文档:
asyncio
, os
, json
, datetime
等),最权威、最全面的信息来源是 Python 官方文档网站。你可以直接访问 docs.python.org,选择你使用的 Python 版本,然后在库索引 (Library Reference) 中找到 asyncio
。使用内置 help()
函数:
python
或 ipython
进入) 中,你可以导入模块,然后使用 help()
函数查看其文档字符串 (docstrings)。import asyncio
help(asyncio) # 查看整个 asyncio 模块的文档
help(asyncio.gather) # 查看特定函数 asyncio.gather 的文档
ipython
在这方面体验通常更好,有语法高亮和更好的分页显示。使用 dir()
函数探索:
dir()
函数可以列出一个模块或对象包含的所有属性和方法名。这有助于你了解一个模块大致提供了哪些功能。import asyncio
dir(asyncio)
help()
使用 dir()
是探索不熟悉的库的常用技巧。IDE/代码编辑器的集成功能:
总结:
asyncio
。help()
和 dir()
。对于第三方库(例如 requests
, numpy
, pandas
等),除了上述方法(特别是 IDE 集成和 help()
),它们通常也有自己独立的文档网站,通过搜索引擎(如 Google 搜索 "requests python documentation")很容易找到。
终于到了激动人心的时刻!我们已经掌握了虚拟环境、类、类型提示和异步编程的基础,可以开始尝试 FastAPI 了。
FastAPI 是一个现代、快速(高性能)的 Python Web 框架,用于构建 API。它的很多设计理念(比如基于 ASGI 的异步特性、依赖注入)可能让我想起 Node.js 的一些框架(如 Express/Koa/NestJS)。
任务 12:创建并运行一个最简单的 FastAPI 应用
确保在虚拟环境里:首先,确认我已经激活了之前为 fastapi-project
创建的 .venv
环境。
source .venv/bin/activate
安装 FastAPI 和 Uvicorn:FastAPI 需要一个 ASGI 服务器来运行,Uvicorn 是官方推荐的。用 pip
安装它们。
pip install fastapi "uvicorn[standard]"
("uvicorn[standard]"
会安装 Uvicorn 以及一些推荐的依赖,提供更好的性能和特性)。安装后,记得更新 requirements.txt
(pip freeze > requirements.txt
)。
创建主应用文件 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")
运行应用:在终端(确保处于 fastapi-project
目录且虚拟环境已激活),运行 Uvicorn 服务器:
uvicorn main:app --reload
main
: 指的是 main.py
文件(Python 模块)。app
: 指的是在 main.py
文件中创建的 FastAPI()
实例对象。--reload
: 这个参数让服务器在代码更改后自动重启,非常适合开发环境(类似 nodemon
)。访问 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 自动校验了。http://127.0.0.1:8000/docs
可以看到 Swagger UI 界面,在这里可以直接测试 API。我的小练习: 添加一个新的 GET 路径 /users/me
,让它返回一个包含我的名字和技能列表的字典。
(感受:FastAPI 的启动和基本路由定义非常简洁!装饰器 @app.get
的用法很新颖,但容易理解。基于类型提示的自动数据校验和自动生成的交互式文档简直是神器,比以前用 Node.js 配 Swagger 要方便太多了!异步函数的支持也很自然。)
前面我们用字典返回 JSON,但对于复杂的请求体或响应体,直接用字典不够健壮,也难以清晰地定义数据结构。FastAPI 推荐(并深度集成)使用 Pydantic 这个库来定义数据模型。
Pydantic 模型是用 Python 类和类型提示来定义的,它能提供:
这非常像在 TypeScript 里定义接口 (Interface) 或类型 (Type) 来约束数据结构,或者使用 Zod 等库进行运行时校验。
任务 13:使用 Pydantic 定义请求体和响应体
安装 Pydantic:虽然 FastAPI 通常会附带安装 Pydantic,但明确安装一下或确保版本没问题总是好的。
pip install pydantic
(记得更新 requirements.txt
)
定义 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
测试:
uvicorn main:app --reload
。http://127.0.0.1:8000/docs
。/items/
的 POST 请求,点击 "Try it out"。{
"name": "我的新物品",
"price": 19.99
}
点击 "Execute",可以看到 FastAPI 成功接收并处理了数据,返回了包含 price_with_tax
(如果提供了 tax
) 的字典。name
或 price
的请求,会看到 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 类型处理可选字段也很方便。)
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 错误。
掌握了 FastAPI 的基础、Pydantic 模型和异步编程后,我已经具备了构建简单 API 的能力。
为了写真实的应用,AI 建议我接下来重点学习 FastAPI 的这些方面:
看起来我的 Python 和 FastAPI 之旅还有很长的路要走,但现在我已经有了清晰的方向和动手实践的基础。是时候开始构思我的第一个 FastAPI 小应用了!
我的目标是用 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,把关系数据库的表结构映射到对象上。
确保在虚拟环境里:我需要先激活我的 fastapi-project
的 .venv
环境。
source .venv/bin/activate
安装 SQLAlchemy:SQLAlchemy 需要单独安装。因为 FastAPI 是基于异步的,AI 建议我安装支持异步的版本(尽管基础 ORM 用法同步异步差别不大)。同时,由于我们选择了 SQLite,还需要安装对应的数据库驱动 aiosqlite
。
pip install sqlalchemy "aiosqlite"
(记得更新 requirements.txt
:pip freeze > requirements.txt
)
定义数据模型 (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}')>"
创建数据库和表 (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()
在主应用启动时创建表:修改 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_tables
或 models.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
语句了。)
在 pip install
命令中,引号(单引号 '
或双引号 "
)的主要作用是防止 shell(命令行解释器,如 Bash 或 Zsh)对包名或版本说明符进行特殊处理。
具体来说:
简单包名(如 sqlalchemy
):
sqlalchemy
,shell 会将其作为一个单独的参数传递给 pip
命令。"sqlalchemy"
)也可以,但通常没有必要。shell 会移除引号,然后将 sqlalchemy
传递给 pip
。包含特殊字符的包说明符(如 "aiosqlite"
或 "requests>=2.20"
或 "uvicorn[standard]"
):
"aiosqlite"
:这个包名本身很简单,不包含特殊字符,所以引号不是必需的。pip install sqlalchemy aiosqlite
和 pip install sqlalchemy "aiosqlite"
的效果是一样的。加上引号可能是个人习惯,或者是在一个包含其他需要引号的包的命令中为了保持一致性。>
, <
, =
, ~
):像 requests>=2.20
这样的写法包含了 >=
。在某些 shell 中,>
有重定向的特殊含义。为了确保 shell 不会尝试进行重定向,而是将整个 requests>=2.20
字符串传递给 pip
,需要加上引号,例如 pip install "requests>=2.20"
。[]
):像 uvicorn[standard]
这样的写法包含了方括号 []
。在 shell 中,方括号通常用于通配符(globbing)。为了防止 shell 尝试查找匹配 uvicorn[standard]
模式的文件名,需要加上引号,例如 pip install "uvicorn[standard]"
。&
, ;
等特殊字符,也需要引号。总结:
-
/_
的简单包名,引号不是必需的。>=
, ==
, <
等) 或 extras ([]
) 的包,强烈建议使用引号,以防止 shell 错误地解释这些特殊字符。pip install sqlalchemy "aiosqlite"
中,给 aiosqlite
加引号是可选的,但没有坏处。定义好了数据库模型,接下来就是实现短网址应用的核心功能:
我们需要在数据库会话 (Session) 中执行这些操作。
创建 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
理解数据库会话 (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
这套流程需要记住。)
现在有了数据库模型和 CRUD 操作函数,是时候把它们连接到 FastAPI 的 API 端点了。我们需要创建两个主要的端点:
POST /shorten
:接收长 URL,返回短 URL。GET /{short_code}
:接收短代码,重定向到原始长 URL。这里要用到 FastAPI 的一个核心特性:依赖注入 (Dependency Injection)。简单来说,就是让 FastAPI 自动为我们的路径操作函数提供所需的依赖项(比如数据库会话 Session
)。
定义 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 版本调整。
创建获取数据库会话的依赖项:在 database.py
中定义一个函数,用于生成数据库会话,并确保在使用后关闭。
# database.py
# ... 其他代码 ...
# 依赖项:获取数据库会话
def get_db():
db = SessionLocal()
try:
yield db # 使用 yield 将会话提供给路径操作函数
finally:
db.close() # 确保在请求结束后关闭会话
修改 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_url
或 redirect_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 还能自动校验和生成文档,这太棒了。RedirectResponse
和 HTTPException
处理重定向和错误也很直接。)
我的 URL 短网址应用现在可以在本地运行了,但最终目标是让别人也能访问。这就需要把它部署到服务器上。
AI 告诉我,部署一个 Python Web 应用(尤其是像 FastAPI 这样的 ASGI 应用)通常涉及几个组件协同工作。以下是一个基础的、手动的部署流程(先不考虑 Docker):
假设我有一台 Linux 服务器(比如 Ubuntu),并且可以通过 SSH 访问。
步骤 1:服务器基础设置
sudo apt update && sudo apt install python3 python3-pip python3-venv
来安装。步骤 2:获取应用代码
sudo apt install git
/var/www/
或用户主目录)克隆我的项目代码:
git clone <你的项目 Git 仓库地址> fastapi-project
cd fastapi-project
步骤 3:设置 Python 虚拟环境和依赖
python3 -m venv .venv
source .venv/bin/activate
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 进程来处理并发请求。
安装 Gunicorn:在虚拟环境里安装 Gunicorn。
pip install gunicorn
(别忘了更新 requirements.txt
)
使用 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 是标准的进程管理器。
创建 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)能访问它。启用并启动服务:
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 的好处:
安装 Nginx:sudo apt install nginx
配置 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;
# }
}
启用配置并重启 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 简化它。)
经过这一系列的步骤,我已经成功地:
venv
) 和包管理器 (pip
)。async/await
)。SQLAlchemy
) 和 API 逻辑。虽然我的短网址应用还很简单(比如短码生成策略可以优化、可以增加点击统计等),并且部署流程也可以用 Docker 进一步简化,但达到这个程度,我已经算完成了最初设定的目标!
接下来我可以:
我的 Python + FastAPI 之旅,才刚刚开始!
之前我学习了手动部署的流程,虽然能跑起来,但步骤繁琐,而且每次换环境都要重复配置,感觉很麻烦。AI 告诉我,这正是 Docker 发挥作用的地方!
Docker 可以把我的应用代码、Python 解释器、所有依赖项(就像 requirements.txt
里的那些)以及运行所需的配置,一起打包到一个叫做 镜像 (Image) 的标准单元里。然后,我可以在任何支持 Docker 的机器上,用这个镜像运行一个 容器 (Container),容器就是一个隔离的、包含了应用运行所需一切的环境。这就像是把我的整个开发环境(或者说,一个精简版的运行环境)打包带走。
这和前端开发中,有时会用 Docker 来确保 Node.js 版本一致、或者打包一个 Next.js 应用进行部署的思路很像,都是为了环境一致性和简化部署。
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
: 指定容器启动时默认执行的命令。fastapi-project
目录下 (包含 Dockerfile
的地方),然后运行:
docker build -t fastapi-shortener .
docker build
: 构建命令。-t fastapi-shortener
: 给镜像打上标签 (tag),方便引用,格式是 repository:tag
(这里省略了 tag,默认为 latest
)。.
: 表示 Dockerfile 所在的当前目录。Dockerfile
里的步骤执行,下载基础镜像、复制文件、安装依赖等。第一次构建可能需要点时间。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
: 使用哪个镜像来创建容器。http://localhost:8000
或 http://localhost:8000/docs
来访问我的 FastAPI 应用了,只不过这次它运行在 Docker 容器里!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 build
和 docker run
的流程跟我之前用 Docker 构建 Node.js 应用很像。虽然 Docker 内部还有很多概念(网络、卷等),但基础的打包和运行流程掌握起来不难。这对于部署来说,比之前的手动流程规范和方便多了!)
虽然 FastAPI 主要用于构建 API,但它也能提供静态文件服务。为了让我的短网址应用更完整一点,AI 建议我可以加一个最简单的前端页面,用来输入长 URL 并显示生成的短 URL。
创建静态文件目录:在 fastapi-project
根目录下创建一个名为 static
的文件夹。
创建 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 请求。修改 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
的内容。测试:运行 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。)
我的应用现在能在本地跑,也能打包进 Docker,甚至还有个简单的前端界面。但 AI 提醒我,要把它真正放到生产环境稳定运行,还有很多事情要做。这就像我完成了一个前端组件,但还需要考虑测试、性能、安全性、部署流程等才能合并到主分支一样。
AI 帮我梳理了一个生产准备清单:
配置管理:
SQLALCHEMY_DATABASE_URL
) 现在是硬编码在 database.py
里的,这在生产环境肯定不行,比如生产数据库的地址和密码不能直接写在代码里。.env
文件和 dotenv
库一样,Python 也有类似机制。推荐使用 环境变量 来传递配置。Pydantic 的 Settings Management 功能可以很方便地从环境变量或 .env
文件加载配置。HTTPS/SSL:
日志记录 (Logging):
logging
模块。我需要配置它来记录应用的运行信息、API 请求、错误等,并将日志输出到文件或集中的日志系统。logging
模块,并集成到 FastAPI 中记录请求和异常。错误监控与告警:
数据库迁移 (Migrations):
URLMap
加个 clicks
计数列),直接修改 models.py
并不能自动更新现有的数据库表结构。更健壮的测试:
pytest
和 FastAPI 的 TestClient
来编写测试用例。安全性加固:
npm audit
)。性能与扩展性:
index=True
)。CI/CD (持续集成/持续部署):
(感受:哇,生产环境要考虑的事情真不少!配置、安全、日志、监控、测试、部署自动化... 这清单让我意识到写代码只是第一步。不过很多概念(像环境变量、HTTPS、日志、测试、CI/CD)和我在前端/Node.js 开发中接触到的思路是相通的,只是具体工具和实现方式不同。感觉 Python 生态在这些方面应该也有成熟的解决方案。)
既然我选择了 FastAPI,AI 也建议我了解一下 Python 世界里另外两个非常流行的 Web 框架:Django 和 Flask,这样能更好地理解 FastAPI 的定位和优缺点。
特性/方面 | Flask | Django | FastAPI |
---|---|---|---|
类型 | 微框架 (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 短网址应用?
/shorten
, /{short_code}
重定向),FastAPI 天生就是为此设计的。(感受:这个对比很清晰。Flask 看起来更自由,适合从零搭起;Django 功能强大,适合快速构建大型应用但可能有点"重";FastAPI 则是在性能、现代特性和开发效率之间找到了一个很好的平衡点,特别适合构建 API。看来我选择 FastAPI 作为入门和完成这个项目的框架是明智的。)
这次 AI 辅助的 Python 学习旅程真是收获满满!从最初只会前端的我,到现在能够:
虽然我的 URL 短网址应用还很简单,但这个过程让我对 Python 后端开发有了全面的认识和实践经验。我明白了 Python 不仅仅是脚本语言,它在 Web 开发、数据处理等方面同样强大且生态完善。
接下来,我会继续完善我的短网址应用,实践生产环境清单里的各项要求,特别是配置管理、日志、测试和数据库迁移。然后,我可能会尝试用 Docker Compose 把应用和可能的数据库(比如换成 PostgreSQL)一起编排部署。
这段学习经历也让我体会到,不同语言和技术栈之间,很多核心思想是相通的(比如模块化、依赖管理、异步处理、MVC/MVVM 模式、部署流程等)。拥有前端背景,学习 Python 后端感觉顺畅了不少。
感谢 AI 的一路指导!Python 的世界,我来了!
本地跑起来很酷,Docker 打包也搞定了,但我的最终目标是让这个短网址 Demo 能在网上被访问到。手动部署到 VPS 流程我大致了解了,但感觉还是有点麻烦,而且还得花钱买服务器。
我就问 AI,有没有免费的、适合部署这种简单 Demo 的平台?最好能让我省点事。
AI 给我分析了几个流行的平台,并结合我目前用 SQLite 的情况给出了建议:
Vercel: 我比较熟悉,前端项目经常用。理论上它支持 Python Serverless Functions,可以跑 FastAPI。但 关键问题 在于它的函数环境是无状态且文件系统短暂的。这意味着我的 shortener.db
SQLite 文件无法持久保存,每次请求都可能面对一个丢失数据的环境。结论:不改数据库的话,Vercel 不行。 要用 Vercel,我必须换成它支持的云数据库(比如 Vercel Postgres 或 Neon),并把连接信息配置成环境变量。
PythonAnywhere: 这个平台 AI 重点推荐给我,因为它专门为 Python 设计,而且它的免费套餐提供持久化文件系统!这意味着我可以直接部署我现有的、使用 SQLite 的应用,无需修改数据库代码。这对于快速验证 Demo 来说,简直是完美匹配。缺点是免费版有限制,部署流程可能和 Vercel 不太一样。
Render: 它提供免费的 Web Service (跑 FastAPI) 和免费的 PostgreSQL 数据库。部署流程可以通过连接 GitHub 实现自动化,比较现代。缺点是我必须把数据库从 SQLite 换成 PostgreSQL,需要修改代码并学习新的数据库配置。免费实例在不活动时会休眠。
Fly.io: 也是一个不错的选择,提供免费的小型 Postgres 集群,并且对 Docker 支持很好(我正好写了 Dockerfile)。缺点同样是必须换数据库,且主要通过命令行操作,学习曲线可能比 Render 稍高一点点。
Railway: 和 Render 类似,提供免费的使用额度可以跑应用和数据库 (如 PostgreSQL),通过 Git 部署也很方便。缺点还是必须换数据库,并且要注意免费额度。
我的选择思考:
考虑到我只是想快速部署这个 Demo,而且暂时不想花时间去改数据库和学习 PostgreSQL,我决定先研究一下 PythonAnywhere 的部署方法。等以后应用复杂了,或者想学习云数据库了,再考虑 Render 或 Fly.io。
说明:最后我没有部署在 PythonAnywhere 上!!!!!
清晰了!我的下一个具体的行动步骤是:
fastapi-project
代码。main:app
)。shortener.db
能够被应用正确读写。*.pythonanywhere.com
域名,并测试应用。搞定部署后,我的这个学习项目就算真正落地了!
# 语法 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))
这两种语法都能达到生成指定长度随机字符串的目的,主要区别在于实现方式和表达的简洁性:
random.choice(sequence)
: 这个函数从给定的序列(字符串、列表等)中随机选择一个元素并返回。语法 1 使用了一个生成器表达式 (random.choice(characters) for _ in range(length))
,它会循环 length
次,每次调用 random.choice
得到一个随机字符,最后 ''.join()
将这些逐个生成的字符连接成一个字符串。random.choices(population, k=n)
: 这个函数(注意是 choices
,复数)直接从给定的 population
(序列)中一次性随机选择 k
个元素(默认允许重复选择),并返回一个包含这 k
个元素的列表。语法 2 调用它得到一个包含 length
个随机字符的列表,然后 ''.join()
将这个列表连接成字符串。哪个更好?
random.choices
) 通常被认为更简洁、更 Pythonic,因为它直接表达了"随机选择 k 个元素"的意图,而不是通过循环逐个构建。random.choices
内部实现可能比显式 Python 循环调用 random.choice
更优化一些,但对于短字符串(比如短码),性能差异通常可以忽略不计。类比 Node.js: 这有点像你需要生成一个包含 6 个随机数字的数组:
for
循环,每次循环 Math.random()
并 push
到数组。generateRandomItems(sourceArray, count)
函数,它直接返回所需数量的随机项组成的数组。结论:两者功能相同,但使用 random.choices
是更推荐、更简洁的方式。
__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__
),情况如下:
my_shortener = Shortener(); my_shortener.some_method()
),Python 解释器会自动将这个对象(my_shortener
)作为第一个参数传递给方法。因此,方法的定义必须包含一个参数来接收这个实例引用。self
: 这个参数的名字按约定俗成应该是 self
。虽然你可以用其他名字(比如 this
,如果你非常想念 JS 的话),但这强烈不推荐,会违反 Python 社区的广泛约定,让其他 Python 开发者(以及未来的你)阅读代码时感到困惑。类比 Node.js/JavaScript:
__init__
方法非常类似于 JS 类中的 constructor()
方法,都是在创建类的新实例时自动调用的初始化方法。self
参数扮演的角色完全等同于 JS 中的 this
关键字。JS 中的 this
是隐式可用的,你不需要在方法参数列表里声明它。而在 Python 中,你需要显式地在实例方法的参数列表里声明第一个参数(通常命名为 self
)来接收这个实例引用。当你需要访问实例的属性或调用实例的其他方法时,都需要通过 self.attribute
或 self.other_method()
来进行,就像在 JS 中使用 this.property
或 this.method()
一样。总结: 在 Python 类中定义实例方法(如 __init__
, greet
等)时,第一个参数必须存在,用来接收实例本身,并且强烈建议将其命名为 self
。
依赖注入 (Dependency Injection, DI) 是一种设计模式,其核心思想是将一个对象(或函数)所依赖的其他对象(它的"依赖项")从外部传递("注入")给它,而不是让它自己在内部创建这些依赖项。
为什么需要 DI?(好处)
类比 Node.js/前端:
@Injectable()
, @Inject()
) 来实现。require('./db')
或 import config from './config'
。FastAPI 如何实现 DI?
FastAPI 的 DI 系统非常优雅且易于使用,主要通过 Depends
函数实现:
依赖项是一个可调用对象: 一个"依赖项"可以是任何可调用对象 (callable),最常见的是一个函数(比如我们写的 get_db()
函数),也可以是一个类。
在路径操作函数中声明依赖: 在你的 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
FastAPI 自动处理: 当请求到达这个路径时,FastAPI 会:
Depends(get_db)
。get_db()
函数。get_db()
函数通常使用 yield
来提供依赖项(比如数据库会话 db
)。yield
出来的值(db
会话)注入到 read_items
函数的 db
参数中。read_items
函数的逻辑。read_items
函数执行完毕(无论成功还是出错),FastAPI 会继续执行 get_db()
函数中 yield
之后的代码(通常是 finally
块里的 db.close()
),确保资源被正确清理。FastAPI DI 的强大之处:
Depends
非常直观。在 Python 中,模块和包是组织代码的基本方式:
模块 (Module):
.py
文件。文件名就是模块名(不含 .py
后缀)。import module_name
或 from module_name import name
来导入和使用模块中的内容。.js
文件模块。包 (Package):
__init__.py
文件(即使它是空的),Python 才会将其识别为一个包。import package_name
),或者导入包中的特定模块 (import package_name.module_name
或 from package_name import module_name
),或者导入模块中的具体内容 (from package_name.module_name import name
)。index.js
或在 package.json
中指定了 main
入口的目录模块。npm
安装和管理的单元,由 package.json
描述,可以包含代码、文档、测试等,并发布到 npm registry。Python 的"包"最初更侧重于代码的组织结构,但现在通过 PyPI (Python Package Index) 和打包工具 (如 setuptools, Poetry),也可以实现类似 Node.js 包的发布和分发。管理方式:
__init__.py
文件来组织包和子包。import
语句来访问包和模块中的代码。Python 会根据其 模块搜索路径 (Python Path) 来查找需要导入的包和模块。这个路径包括:
PYTHONPATH
中指定的目录。pip
(安装/卸载) + venv
(虚拟环境) + requirements.txt
(记录依赖快照)。npm
/yarn
的体验。总结: Python 的模块是单个文件,包是包含 __init__.py
的目录,用于组织模块。通过 import
语句和 Python Path 来管理代码间的引用。第三方包通过 PyPI 查找,并推荐使用 Poetry 或 Pipenv 进行安装和管理。
这里需要区分两个概念:内置函数/类型 (Built-ins) 和 标准库模块 (Standard Library Modules)。
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
等。Math
, JSON
, Date
)、全局函数(parseInt
, parseFloat
, isNaN
)、原始类型(string
, number
, boolean
, null
, undefined
, symbol
, bigint
)和内置对象(Array
, Object
, Function
, Promise
, Error
等)。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 开发非常重要。