part1(本文): 在 AI 的帮助下,只花了 5 个小时就入门并上线了我的 Python 短网址应用(01)
part2: 在 AI 的帮助下,只花了 5 个小时就入门并上线了我的 Python 短网址应用(02)
这篇文档记录了我从 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
}
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 小应用了!