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

这篇文档记录了我从 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 小应用了!

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