Python 学习路线:从语法基础到可维护的小项目
本文目录
如果已经会写 JavaScript,学 Python 不必再从“什么是变量”开始背一遍。更有效的顺序是:掌握 Python 自己的数据模型,学会把代码拆成函数和模块,再用文件、异常和测试做出一个小工具;只有确实要写异步网络程序时,再进入 asyncio。
本文是一条原创练习路线,目标是让读者能读懂、运行、测试并逐步维护一个 Python 小项目,不是把第三方教程翻译或搬进本站。示例面向 Python 3.10 及以上版本;实际项目应遵从 pyproject.toml、部署环境和 CI 声明的版本范围。
先选一条目标路线
| 目标 | 先学什么 | 做到什么再进入下一步 |
|---|---|---|
| 写本地脚本 | 语法、列表/字典、pathlib、JSON、异常 | 能处理缺文件、坏输入和重复运行 |
| 做 HTTP API | 上述基础、HTTP、模块边界、测试、类型标注 | 能解释状态码、输入校验、权限和错误响应 |
| 做数据处理 | 基础容器、迭代、文件格式、虚拟环境 | 能从小样本复现、验证结果并记录数据来源 |
| 写异步服务 | 先理解普通函数、异常和 I/O,再学 asyncio | 能解释取消、超时、并发上限和资源关闭 |
先选一个近期能完成的小目标,不要同时安装多个 Web 框架、异步运行时、ORM 和任务队列。项目已有的技术栈应优先于“热门工具清单”。
0. 建立可复现的运行环境
先确认你真正运行的是哪个解释器,再创建项目虚拟环境。macOS/Linux 常用 python3,Windows 上可以使用 Python Launcher 的 py:
python3 --version
python3 -c "import sys; print(sys.executable)"
python3 -m venv .venv
source .venv/bin/activate
python -m pip --version
Windows PowerShell:
py -3 --version
py -3 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -c "import sys; print(sys.executable)"
python -m pip --version
激活脚本只是调整当前 shell 的命令搜索路径。判断是否使用了项目环境,应检查 sys.executable 和 python -m pip --version 的路径。不要把 .venv 提交到 Git,也不要把它复制到另一台电脑;在目标机器用项目声明的 Python 重新创建即可。环境与可测试服务的公开入门见 Python 后端开发入门。
1. 先掌握 Python 自己的表达方式
Python 用缩进表达代码块;值仍然有类型,但变量不需要先声明类型。条件判断中,空列表、空字符串和 0 都是假值;检查是否缺少值时应写 is None,而不是把 is 当作一般相等比较:
title = " Python 工程入门 "
items = []
optional_value = None
if items:
print("列表非空")
else:
print("列表为空")
if optional_value is None:
print("还没有提供可选值")
normalized_title = " ".join(title.split())
print(normalized_title)
JavaScript 的 === 和 Python 的 == 都用于比较值;Python 的 is 判断是不是同一个对象,最常见的正确用途是 value is None。列表和字典是可变对象,赋值通常不会复制内容:
source = ["Python"]
alias = source
alias.append("测试")
assert source == ["Python", "测试"]
如果要得到独立的浅拷贝,可使用 source.copy();如果内部还包含可变对象,浅拷贝不会递归复制所有层级。开始时先用简单结构,只有确实需要时再引入 copy.deepcopy。
2. 熟悉列表、字典、循环与迭代
Python 列表常用 append(),长度通过 len(values) 获取;字典使用 data[key] 取必需字段,.get() 读取可选字段。列表推导式适合简短的转换,不要把多层循环、异常处理和副作用全部压成一行。
records = [
{"title": "浏览器通信", "published": True},
{"title": "异步请求", "published": False},
]
published_titles = [record["title"] for record in records if record["published"]]
for title in published_titles:
print(title)
边界需要说清楚:record["title"] 缺键会抛 KeyError;record.get("title") 缺键会返回 None,但它不会验证值是不是字符串。如果字段来自文件、网络或用户输入,读取后仍要做类型和范围检查。更多 JavaScript 到 Python 的容器对照见列表、字典与数据访问。
3. 把函数、模块和异常作为工程边界
函数先表达一个可命名的动作,再明确输入、输出和失败条件。不要用可变对象作默认参数:默认值在函数定义时创建,后续调用可能共享同一个对象。
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
一个 .py 文件就是一个模块。项目长大后按职责拆文件,并在入口文件中用 if __name__ == "__main__": 区分“作为脚本运行”和“被其他模块导入”。异常处理只捕获当前层能处理的错误:把不可恢复的输入错误转换成清晰消息,底层意外错误则保留上下文,不要用空的 except: 悄悄继续。
def parse_view_count(raw: str) -> int:
try:
count = int(raw)
except ValueError as error:
raise ValueError("阅读量必须是整数") from error
if count < 0:
raise ValueError("阅读量不能为负数")
return count
raise ... from error 保留原始异常链,排查时既能看到业务提示,也能定位转换失败的根因。不要把同一错误在每一层都重复包装成含糊的“操作失败”。
4. 用 pathlib、JSON 和测试做一个小练习
把下面代码保存为 mini_catalog.py。它对标题做规范化,并把阅读量限制为非负整数:
from __future__ import annotations
def normalize_title(value: str) -> str:
if not isinstance(value, str):
raise TypeError("标题必须是字符串")
result = " ".join(value.split())
if not result:
raise ValueError("标题不能为空")
return result
def parse_view_count(value: str | int) -> int:
if isinstance(value, bool) or not isinstance(value, (str, int)):
raise TypeError("阅读量必须是整数或数字字符串")
try:
result = int(value)
except ValueError as error:
raise ValueError("阅读量必须是整数") from error
if result < 0:
raise ValueError("阅读量不能为负数")
return result
把下面测试保存为 tests/test_mini_catalog.py。测试的是可观察的边界,不是私有实现细节:
import unittest
from mini_catalog import normalize_title, parse_view_count
class CatalogInputTests(unittest.TestCase):
def test_title_whitespace_is_collapsed(self):
self.assertEqual(normalize_title(" Python 工程 "), "Python 工程")
def test_empty_title_is_rejected(self):
with self.assertRaises(ValueError):
normalize_title(" ")
def test_non_string_title_is_rejected(self):
with self.assertRaises(TypeError):
normalize_title(123)
def test_view_count_accepts_digit_string(self):
self.assertEqual(parse_view_count("12"), 12)
def test_negative_count_is_rejected(self):
with self.assertRaisesRegex(ValueError, "不能为负数"):
parse_view_count(-1)
def test_boolean_is_not_silently_accepted_as_integer(self):
with self.assertRaises(TypeError):
parse_view_count(True)
def test_float_is_not_silently_truncated(self):
with self.assertRaises(TypeError):
parse_view_count(1.5)
if __name__ == "__main__":
unittest.main()
在项目根目录运行:
python -m unittest discover -s tests -v
这组练习刻意区分 TypeError 和 ValueError,并覆盖空白、错误类型、合法数字和负数。接下来可以把记录保存为 JSON:使用 pathlib.Path 表达路径,用 with 关闭文件,用 json.load/json.dump 读写,并明确 UTF-8 编码;不要把读取文件、校验、转换和打印全部塞进一个函数。JSON、函数拆分与测试的公开综合示例见 Python 后端开发入门。
迭代器和生成器决定数据是否按需读取;上下文管理器决定文件等资源何时释放。开始处理流式输入或多份文件时,可参考 Python 官方的迭代器类型说明与contextlib 文档,重点练习延迟执行和异常路径上的资源清理。
要把本地数据写入数据库,可从 Python 官方 sqlite3 文档开始:使用参数绑定而不是拼接 SQL,在明确的事务边界内处理一组写入,并结合目标 SQLite 版本核对并发和锁行为。
准备访问外部 API 时,可先看 Python 官方 urllib.request 文档,再为连接和读取分别设置合理的超时、限制响应大小,并只对明确幂等的请求做有预算的重试;超时后的 POST 不能盲目重放。
5. 类型标注是协作提示,不是运行时校验
name: str、-> int 等标注能帮助读者、IDE 和静态类型检查器理解接口;Python 解释器默认不会因为类型标注自动拒绝不符合类型的值。外部输入仍要在运行时验证,不能用一个 str 标注替代 API 参数校验。
先标注稳定的公共函数和数据结构,再在团队确定后选一个类型检查器并写入项目配置。不要为了标注每个局部变量而让代码失去可读性。涉及复杂泛型、协议、运行时反射或新版 typing 功能时,查对应 Python 版本的官方文档,避免把某个新版本的语法带进旧运行环境。
6. 什么时候进入 asyncio 和 Web 后端
asyncio 面向异步 I/O 协作。async def 定义协程,只有在 await 等交还控制权的位置,事件循环才有机会调度其他协程;把长时间 CPU 计算或同步阻塞调用直接放进协程,仍会卡住事件循环。异步不是“代码里加上 async 就变快”,也不是所有脚本的默认起点。
进入异步服务后,下一步不要只练 asyncio.gather():任务的所有权、取消后的清理、期限传递和队列背压同样重要。请结合 Python 官方 asyncio 任务与 TaskGroup 文档验证当前版本行为,并为取消与超时路径补上可重复的测试。
准备做 API 时,建议按这个顺序扩展:
- HTTP 方法、状态码、请求体、响应头和超时。
- 输入 schema、字段校验、错误格式和分页契约。
- 身份认证与对象级授权;前端隐藏按钮不是权限控制。
- 数据库连接生命周期、事务、唯一约束和迁移。
- 日志、健康检查、配置与密钥隔离、可观测性和可回滚部署。
- 并发限制、客户端断开、幂等、重试和资源释放。
先完成一个普通同步请求的完整闭环,再根据项目框架的官方文档学习异步处理。跨层异常要保留原因,数据库状态变更要考虑事务;不要让 API 直接暴露原始 SQL、堆栈或用户提交的敏感字段。
一条四周练习安排
| 周次 | 练习 | 验收结果 |
|---|---|---|
| 第 1 周 | 基础语法、字符串、列表、字典、循环与函数 | 不看答案写出小型数据清洗函数 |
| 第 2 周 | 模块、路径、JSON、异常与单元测试 | 在新建虚拟环境中运行一个 CLI 工具和测试 |
| 第 3 周 | 类型标注、依赖声明、日志与配置边界 | 能解释依赖放在运行组还是开发组,且不把密钥提交到仓库 |
| 第 4 周 | HTTP/API、数据库事务或 asyncio(三选一) | 有请求/错误/权限/清理测试,不只是“服务能启动” |
每周都保留一项可运行产物。遇到新工具时先回答“它解决哪个具体问题、失败时怎么诊断、如何锁定版本”,再决定是否加入项目。
常见误区检查表
- 裸
pip和运行程序的python可能不属于同一个解释器;优先使用python -m pip。 is不是普通的相等运算;值比较用==,单例缺失值用is None。list/dict是可变对象;不要把它们当作共享默认参数。.get()只处理缺键,不会验证数据类型、范围或业务含义。except Exception: pass会隐藏故障;只捕获能恢复的具体错误。- 类型标注不代替运行时输入校验;异步语法不代替超时、取消和背压设计。
.venv是本机可重建环境,不是跨电脑的依赖交付物;提交项目配置和经过审阅的依赖声明。
字典练习:读、增、改、删与成员判断
本地 Python 学习材料中的 lesson4.py 目前只有练习要求,没有已完成的答案。下面是为这篇学习路线新写的参考解答,不是原文件代码,也不表示这段练习曾在真实项目中使用。姓名、年龄和经验值都是虚构示例,公开示例不要填入不打算公开的个人信息。
profile = {
"name": "Example Reader",
"age": 30,
"job": "Frontend Engineer",
"skills": ["JavaScript", "TypeScript", "Python"],
}
print(profile["name"])
print(profile["job"])
profile["years_of_experience"] = 3
profile["job"] = "AI Engineer"
del profile["age"]
print(list(profile.keys()))
print(list(profile.values()))
print("skills" in profile)
print(profile.get("email"))
for key, value in profile.items():
print(f"{key}: {value}")
profile["job"] = ... 会新增或覆盖该键;del profile["age"] 删除已存在的键,如果键不存在会抛出 KeyError。确实允许字段缺失时,可以用 profile.pop("age", None),但不要用默认值掩盖本来必须存在的数据。profile.get("email") 适合读取可选字段,返回 None 不代表已经完成类型校验。
"skills" in profile 检查的是字典键,不是键对应的值;与 JavaScript 的 in 不同,它不会沿原型链查找。keys()、values() 和 items() 返回可迭代的动态视图,例子里的 list(...) 是为了把当时的结果变成普通列表。日常遍历键和值时,直接使用 for key, value in profile.items() 更清楚。
学习资源
| 资源 | 适合怎么用 | 阅读时留意 |
|---|---|---|
| 廖雪峰 Python 教程 | 中文连续学习语法、函数、模块和常见库 | 作为讲解路径,不把旧安装步骤或历史版本判断当作当前环境要求 |
| Python 官方教程 | 对照语言语法、数据结构、模块、异常和标准库 | 选择与项目支持版本匹配的文档版本 |
Python 虚拟环境 venv 与 Packaging User Guide 教程 | 管理隔离环境、依赖和项目打包 | 按项目已有工具选一种流程,不要叠加多个锁文件体系 |
Python unittest | 学习测试夹具、断言、发现和清理 | 框架只是执行器,测试仍需覆盖业务边界 |
Python typing | 查类型注解和版本差异 | 运行时默认不强制执行标注 |
Python asyncio | 学协程、任务、取消和异步 I/O | 先区分 I/O 并发与 CPU 并行 |
以上中文教程和官方文档均保留为外链;本文仅提供原创路线、示例和选择建议,不镜像第三方教程。所有版本相关行为以项目目标解释器对应的官方文档为准。路线上的其他 Python 专题见后端目录。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。