# 给前端的 Python 入门：从数据脚本开始

## 用途、程度与前置

对偏前端的 AI 全栈开发者，Python 最先带来的价值是数据清洗、离线评测和运行模型生态里的示例。你不必先把 Node 后端全部重写。L2 的目标是读懂常见 Python 项目，维护小型数据脚本，知道环境和异常在哪里发生；复杂模型训练可以留到后面。

前置是成熟的 JavaScript 编程能力、终端与 JSON 知识。本章不重新讲变量是什么，而是强调迁移时最容易出错的差异。示例要求 Python 3.12 或更高版本，仅使用标准库，在 Windows、macOS、Linux 都可运行。版本通过 python --version 或 python3 --version 检查。

## 运行环境相当于你熟悉的哪一层

Python 解释器类似执行 JavaScript 的运行时，但不同 Python 项目可能依赖不同包版本。venv 创建项目独立环境，作用接近把依赖环境隔离在项目中；它不是虚拟机，也不会自动解决依赖锁定或操作系统库依赖。运行时版本仍需明确记录。[venv 文档](https://docs.python.org/3/library/venv.html)

建议显式调用 python -m pip，让 pip 和当前解释器一致。单独输入 pip 时可能装到另一个 Python。依赖清单负责声明，锁定机制负责复现，两者不要混为一谈。标准库示例不需要 pip install；先理解脚本，再按任务安装真正需要的库。

```bash
python -m venv .venv
```

Windows PowerShell 可直接使用 .\.venv\Scripts\python.exe 执行后续文件，无需调整激活脚本策略。macOS/Linux 可使用 .venv/bin/python。以下章节统一写 python，表示你已选择正确解释器，不要求全局环境必须恰好配置成这个名字。

## 用 JavaScript 经验逐项迁移

Python 使用缩进定义代码块，四个空格是常见规范，混用 Tab 会产生难排查错误。None 类似表达“无值”的 null；True、False 首字母大写。dict 对应按键取值的数据结构，list 对应可变序列，tuple 常用于固定记录。json.loads 把 JSON 文本转成这些 Python 对象。

“值相等”使用 ==，is 判断是否同一个对象，通常写 value is None；不要用 is 比较普通字符串或数字。空字符串、空列表、零都为假，因此 result or default 和 JavaScript 的 || 一样可能覆盖有效的零值，不能总当缺省值处理。字典取不存在的键会产生 KeyError，get 可提供默认值，但必要字段缺失时应明确报错。

Python 的 list、dict 是可变对象，函数参数传递的是对象引用这一层的关系。尤其不要写 def f(items=[]) 作为可变默认参数，因为这个列表会跨调用复用。用 None 作为默认值，在函数体里创建列表。推导式可以代替部分 map/filter，但复杂嵌套会降低可读性，不必把所有逻辑挤在一行。

类型提示如 list[dict] 帮助编辑器和检查工具理解程序，并不自动运行时验证。就像 TypeScript 类型在外部 JSON 面前不能保证真实内容，Python 仍需检查解析后的值。还有一个迁移陷阱：bool 是 int 的子类，因此 isinstance(True, int) 为真。金额等字段必须拒绝布尔值时，可以用 type(value) is int 做精确类型判断。

## 完整示例：校验订单数据并统计已支付金额

保存以下完整 JSON 文件为 orders.json，金额采用整数分，避免小数金额计算误差。

```json
[
  {"id": "o1", "status": "paid", "amount_cents": 1200},
  {"id": "o2", "status": "pending", "amount_cents": 800},
  {"id": "o3", "status": "paid", "amount_cents": 3050}
]
```

保存以下完整脚本为 summarize.py。

```python
# summarize.py
import argparse
import json
import sys
from pathlib import Path

def validate(raw: object) -> list[dict]:
    if not isinstance(raw, list):
        raise ValueError("根节点必须是数组")
    seen: set[str] = set()
    result: list[dict] = []
    for index, item in enumerate(raw, start=1):
        if not isinstance(item, dict):
            raise ValueError(f"第 {index} 条必须是对象")
        order_id = item.get("id")
        amount = item.get("amount_cents")
        status = item.get("status")
        if not isinstance(order_id, str) or not order_id.strip():
            raise ValueError(f"第 {index} 条 id 无效")
        if order_id in seen:
            raise ValueError(f"重复 id：{order_id}")
        if type(amount) is not int or amount < 0:
            raise ValueError(f"{order_id} 金额必须是非负整数分")
        if status not in {"paid", "pending", "cancelled"}:
            raise ValueError(f"{order_id} 状态不受支持")
        seen.add(order_id)
        result.append(item)
    return result

def main() -> int:
    parser = argparse.ArgumentParser(description="统计已支付订单")
    parser.add_argument("file", type=Path, help="UTF-8 JSON 文件路径")
    args = parser.parse_args()
    try:
        raw = json.loads(args.file.read_text(encoding="utf-8"))
        orders = validate(raw)
    except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as error:
        print(f"输入失败：{error}", file=sys.stderr)
        return 2
    paid = [item for item in orders if item["status"] == "paid"]
    total = sum(item["amount_cents"] for item in paid)
    print(json.dumps({
        "paid_count": len(paid),
        "total_cents": total
    }, ensure_ascii=False))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())
```

```bash
python summarize.py orders.json
python summarize.py --help
```

第一条命令预期输出 {"paid_count": 2, "total_cents": 4250}，退出码 0；第二条输出参数帮助。把金额改成 true 后，程序输出中文错误到标准错误，退出码为 2，不产生一份看似成功的统计结果。你可以用 PowerShell 的 $LASTEXITCODE 或 Bash 的 echo $? 查看前一条程序退出码。

逐段解析：Path 把文件路径封装成对象，read_text 显式指定 UTF-8，避免依赖机器默认编码；json.loads 只负责解析语法；validate 承担业务契约。seen 用集合检测重复主键；错误带行序号或业务 id，便于回到来源修复；推导式筛出 paid，再使用生成器表达式求和，避免额外建立金额数组。

main 返回整数，最外层 SystemExit 把它交给操作系统。if __name__ == "__main__" 让此文件被别的模块 import 时不会自动解析命令行，你可以复用 validate 写评测或测试。标准输出留给机器可消费 JSON，标准错误留给诊断，后续管道脚本就不会把错误文字误当数据。[Python 教程](https://docs.python.org/3/tutorial/)

## 文件、异常和模块的 API 契约

Path.read_text 返回 str，路径不存在、权限不足等情况会抛 OSError 的子类；json.loads 返回解析后的对象，非法 JSON 抛 JSONDecodeError；argparse 在参数错误时提供帮助并以非零状态结束。把这些错误分类能让你知道问题来自环境、文件语法还是业务数据。[pathlib](https://docs.python.org/3/library/pathlib.html)、[json](https://docs.python.org/3/library/json.html)

打开较大文件时使用 with open(...) 管理关闭，避免文件句柄泄漏。批量数据不一定适合一次 read_text 全部放入内存，可改为 JSONL 按行处理。网络请求需要明确超时，CPU 密集计算和异步 I/O 是不同问题；不要因为 Node 习惯使用 async，就把每个 Python 函数都改成 async。

模块名也可能冲突。将自己的文件命名为 json.py、csv.py 或 requests.py，可能遮蔽真正的模块，产生“部分初始化”或缺少属性错误。检查当前目录文件名、解释器路径和 import 的来源，通常比反复重装所有依赖更快。

## 常见错误与排障

明明安装包却 ModuleNotFoundError，先打印 sys.executable，确认执行脚本和安装依赖的解释器一致。Windows 中文文件报解码错误，确认实际文件编码并显式读取；不要无条件 errors="ignore"，它可能悄悄删除重要字符。数据校验失败时保留错误位置，不要 catch Exception 后返回空列表，把错误伪装成无数据。

程序总金额不对，先检查金额单位、布尔值、字符串数字与重复 id。隐式把所有输入转成 int 看似方便，却可能掩盖上游合同变化。边界需要显式规则：到底接受字符串 "1200" 还是拒绝，由数据合同决定并写进测试。

## 不要把 Python 逐词翻译成 JavaScript

熟悉 JavaScript 能让你快速读懂 Python，但两者在对象、调用和异步语义上有重要差异。迁移时应该问“这个表达式返回什么、何时执行、是否共享对象、失败如何传播”，而不是只寻找语法替换表。很多初学者的脚本能跑，却在空值、可变参数或并发时出现难以解释的行为。

Python 的名字绑定到对象，不是每个变量都保存独立副本。把一个列表赋给另一个名字，两者指向同一个列表；对列表 append 会被双方看到；把其中一个名字重新赋成新列表，只改变这个名字的绑定。浅复制只复制外层容器，内部嵌套对象仍可能共享，这与 JavaScript 展开语法处理嵌套对象很相似。

字典可以保存异构数据，但类型提示不会使它自动拥有 DTO 的运行时约束。面对外部输入，先验证再进入内部模型。对于稳定结构，可以用 dataclass 表达字段，减少散落字符串键；但 dataclass 本身也不自动校验 JSON 或强制注解类型。把静态辅助、对象构造与业务校验分清，才能避免一种工具承担它并不提供的保证。

Python 的整数支持任意精度，但不是无限资源；非常大的整数计算仍会耗时。float 通常是双精度二进制浮点，处理十进制金额时仍会遇到表示误差。Decimal 适合按十进制规则计算，但应从字符串构造并明确舍入规则，不能先用 float 产生误差再希望 Decimal 自动恢复原值。金额若合同允许，也可以使用整数分保持简单。

字符串不可变，切片返回新字符串。大量循环拼接可能产生重复分配，可以收集到列表后 join，或者使用文件流逐段写。Python 的 len(str) 统计代码点数量，不等于用户感知字符数，也不等于模型 token 数；含组合字符或复杂 emoji 的显示长度尤其不同。前端输入长度、后端存储限制和模型上下文预算需要分别定义。

## 函数参数、默认值与异常传播

函数定义中的默认表达式只在定义时求值一次，因此可变默认参数会跨调用共享。def collect(value, items=[]) 不是每次创建空列表，而是不断修改同一个列表。正确方式通常是 items=None，然后在函数体内决定创建新列表。需要明确共享缓存时也可以使用持久对象，但应该显式命名和管理生命周期。

位置参数、关键字参数与仅关键字参数可以让接口更清楚。比如 def read_data(path, *, encoding="utf-8") 要求 encoding 按名字传入，避免两个字符串位置写反。函数返回多个值时通常是一个 tuple，调用方可以解包；解包个数不匹配会报错，不会像某些宽松结构那样静默忽略。

Python 没有 JavaScript 那种自动把普通函数返回值包装为 Promise 的习惯。普通函数直接执行并返回，async def 调用产生协程对象，是否运行取决于 await 或任务调度。异常也沿实际调用链传播：捕获范围过大容易把程序缺陷误当输入错误，范围过小则会丢失必要上下文。优先在清楚的边界捕获可恢复异常。

异常链能保留底层原因。将解析错误包装成业务错误时，可以使用 raise ValueError("输入格式不合法") from error，既给用户稳定说明，也保留调试线索。不要在每层都打印同一个异常，最后可能出现多份重复日志。通常由负责边界响应的一层记录一次，其余层补充语义并传播。

## 迭代器与生成器让大文件处理保持可控

列表已经包含全部元素，迭代器按需提供下一项。生成器函数通过 yield 逐条产出，执行会停在 yield 位置，直到消费者继续请求下一条。它适合 JSONL、日志和批量任务，因为不必先把所有正文读进内存；但函数中的错误也可能在消费到某一条时才发生，而不是创建生成器时发生。

惰性不等于零成本。为了全局去重，你可能仍要保存所有已见 id；为了排序全部记录，也通常需要收集或使用外部排序。应该分别分析正文缓冲、去重集合和输出结果的内存，而不是一看到 yield 就声称算法恒定内存。

一次性迭代器消费后不会自动重来。先用 sum(1 for _ in rows) 统计，再遍历同一个 rows，可能发现没有任何记录。需要重复遍历时重新打开文件、重新创建生成器，或在数据规模允许时显式转换为列表。让 API 名称说明返回迭代器还是列表，可以减少调用者误用。

## 第二个完整例子：逐行读取并统计订单

把原来的 summarize.py 放在同一目录，下面文件保存为 stream_orders.py。它复用原章节已提供的 validate，说明模块可以承担明确的输入合同；不是复制一份稍有不同的校验规则。

```python
# stream_orders.py
import argparse
import json
from pathlib import Path
from summarize import validate

def iter_orders(path):
    seen = set()
    with path.open(encoding="utf-8") as source:
        for number, line in enumerate(source, start=1):
            if not line.strip():
                raise ValueError(f"第 {number} 行为空，违反本例 JSONL 合同")
            try:
                # 每行单独校验，但全文件重复 ID 需要额外检查
                order = validate([json.loads(line)])[0]
            except (ValueError, TypeError) as error:
                raise ValueError(f"第 {number} 行错误：{error}") from error
            if order["id"] in seen:
                raise ValueError(f"第 {number} 行出现跨行重复 ID")
            seen.add(order["id"])
            yield order

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("file", type=Path)
    args = parser.parse_args()
    count = total = 0
    for order in iter_orders(args.file):
        if order["status"] == "paid":
            count += 1
            total += order["amount_cents"]
    print(json.dumps({"paid_count": count, "total_cents": total}))

if __name__ == "__main__":
    main()
```

将以下内容保存为 orders.jsonl，每行都必须是一个完整 JSON 对象，而不是数组的一部分。

```jsonl
{"id":"o1","status":"paid","amount_cents":1200}
{"id":"o2","status":"pending","amount_cents":800}
{"id":"o3","status":"paid","amount_cents":3050}
```

执行 python stream_orders.py orders.jsonl，预期结果仍是两笔已支付、总额四千二百五十分。将最后一行 id 改为 o1，应该出现跨行重复错误。将文件末尾增加空行，按本例合同也会失败；真实项目可以允许空行，但规则应明确，而不是每个脚本行为不同。

with 管理文件关闭，即使消费途中抛异常，离开上下文也会释放文件。validate([one]) 检查单条业务结构，seen 负责跨行约束，二者职责不同。最后只在全部处理完成后输出成功 JSON，避免半途失败时标准输出已经出现一份误导下游的“最终报告”。

## 模块、包与执行入口的实际区别

import summarize 会执行模块顶层代码一次，所以把命令行解析放进 main 并用 __name__ 判断十分重要。没有这个入口保护，另一个脚本只想复用 validate，也会触发参数解析或读文件。模块顶层适合定义常量、函数和轻量初始化，不适合默认启动服务器、访问网络或批量修改文件。

项目变大后可以把模块放进包，并通过 python -m package.module 执行。模块搜索路径与当前执行方式有关，直接运行某个深层文件可能导致相对导入失败。与其不断修改 sys.path，更应该建立清楚的包结构与运行入口。对一个教学脚本不必过早引入复杂构建工具，但真实项目应有一致的安装和执行方式。

虚拟环境只隔离 Python 包，不自动锁定系统库和外部命令。图像处理、数据库驱动或机器学习框架可能依赖操作系统组件。复制 .venv 到另一台机器通常不是可靠交付方法，应从明确运行时和依赖清单重建。要记录已验证的解释器版本、平台和必要原生依赖，而不是只说“我电脑能跑”。

## 异步 Python 与前端异步的关键对照

在 JavaScript 中调用一个 async 函数通常立即开始执行到首次等待点；Python 调用 async def 只创建协程对象，必须 await 或交给 asyncio 调度才运行。asyncio.create_task 会安排协程并发执行，asyncio.gather 等待多个任务并按传入顺序收集结果。并发的含义是等待期间允许其他工作推进，不等于多个 CPU 核心同时运行 Python 代码。

网络与磁盘等待可能适合异步；纯计算循环不会因为加 async 就让事件循环自动切换。阻塞库在事件循环线程中运行仍会阻塞其他任务，需要合适异步库、线程或进程边界。不要把 requests 这样的同步调用直接放进 async 函数，然后误以为已经实现非阻塞。

取消也是协作过程。任务在等待点收到取消异常，清理逻辑通常放在 finally 中。不要无条件捕获所有异常并继续循环，否则可能吞掉取消信号，让用户点击取消后任务仍运行。异步任务还要有并发上限；一次性创建几万条外部调用会把连接、额度和内存全部挤满。

## 第三个完整例子：限制并发并观察执行顺序

保存 async_demo.py，Python 3.12，仅标准库。它用 sleep 模拟等待，不访问网络。预期最多两个任务同时处于工作区，结果数组仍按输入编号排列。

```python
# async_demo.py
import asyncio

async def main():
    semaphore = asyncio.Semaphore(2)
    running = 0
    peak = 0
    async def work(index):
        nonlocal running, peak
        async with semaphore:
            running += 1
            peak = max(peak, running)
            try:
                await asyncio.sleep(0.01 * (4 - index))
                return index * 10
            finally:
                running -= 1
    results = await asyncio.gather(*(work(index) for index in range(4)))
    assert peak <= 2
    print({"results": results, "peak": peak})

if __name__ == "__main__":
    asyncio.run(main())
```

执行 python async_demo.py，预期 results 为零、十、二十、三十，peak 为二。任务完成先后可能与编号不同，但 gather 的返回顺序依据传入位置。async with 即使发生异常也会释放信号量，finally 维护计数；生产中还需要对单任务超时和结果未知设计状态。[asyncio 任务与取消](https://docs.python.org/3/library/asyncio-task.html)

## 集合操作的返回值也属于 API 合同

Python 的 list.sort 在原列表上排序并返回 None，sorted(iterable) 返回新列表。前端开发者若写 rows = rows.sort()，会把 rows 变成 None，后续迭代才报错。list.append 也返回 None，不应把它的返回值当作新增后的数组。阅读 API 时要同时确认是否修改原对象和返回什么，不能仅凭函数名判断。

字典的 get 默认返回 None，因此无法区分“键不存在”和“键存在且值为 None”。确实需要区分时，使用 key in mapping，或者创建独立哨兵对象作为默认值。不要把所有缺失都压成一个假值；订单金额为零、布尔开关为 False 和字段未提供可能有不同业务含义。

集合 set 适合唯一性与成员检查，但不应把它的遍历顺序当稳定输出合同。生成报告、哈希或参数摘要时，需要对无序集合显式排序。字典会保留插入顺序，但两个语义相同、插入顺序不同的字典经普通 JSON 序列化可能得到不同字符串。要做内容指纹，应明确规范化规则，而不是把任何字符串差异都解释为业务变化。

## 上下文管理器为何比“最后记得关闭”可靠

文件、数据库连接和锁都具有获取与释放生命周期。with 让清理与代码块范围绑定，即使中间发生异常也会尝试执行退出逻辑。它类似前端在组件卸载时释放订阅，但作用范围是一个代码块，而不是某个 UI 生命周期。资源清理不是异常处理的替代品：关闭文件后，原异常仍可能继续传播。

不同对象的上下文管理含义并不完全相同。例如 SQLite 连接的 with 通常用于事务提交或回滚，并不自动关闭连接，所以本教材仍显式 close。不能看到 with 就推断所有资源都以同样方式释放；应阅读该对象的文档合同。文件 with 会关闭文件，事务 with 管提交边界，锁 with 管获取和释放，这些相似语法背后是不同资源语义。

## 综合练习：将数据错误与程序错误区分开

练习基于 stream_orders.py：对输入错误返回退出码二，对成功返回零，同时保留未预期程序错误的堆栈。不要使用 except Exception 把所有问题统一变成“数据格式不对”。

<details><summary>参考实现</summary>

下面代码用于替换 stream_orders.py 最后的入口部分。它依赖该文件已有的 main，因此属于明确的替换片段。保存修改后运行正常文件应成功，运行重复主键文件应以退出码二失败。

```python
if __name__ == "__main__":
    import sys
    try:
        main()
    except (OSError, UnicodeError, ValueError) as error:
        print(f"输入处理失败：{error}", file=sys.stderr)
        raise SystemExit(2)
```

若你故意把程序变量名写错导致 NameError，它不会被这个边界吞掉，而会保留堆栈，提醒开发者这是代码缺陷。业务边界不是把错误藏起来，而是对已知失败提供稳定合同，对未知缺陷保留足够诊断信息。

</details>

学习 Python 的下一步应由实际任务驱动。需要清洗文件，就深入编码、迭代器和数据合同；需要并行评测，就深入任务、限流和取消；需要运行模型，就先读对应库的输入输出与资源要求。不要为了“学完整语言”先把所有语法都背完，也不要因为脚本短就忽略可复现环境与错误路径。


## 练习、提示与参考解答

练习：增加按状态统计数量，空数组应返回零；重复订单和未知状态必须失败。再新增 --status 参数，让用户选择统计 paid 或 pending。

提示：argparse 的 choices 可以限定输入；业务数据自身的状态校验仍然必须保留。

<details><summary>参考答案</summary>

增加 parser.add_argument("--status", choices=["paid", "pending"], default="paid")，筛选条件改为 item["status"] == args.status，输出字段命名为 count 与 total_cents。空数组经过 validate 得到空列表，sum 返回 0；不要把空列表视为异常。重复 id 和未知状态在 validate 阶段失败，即使它们不会进入所选状态统计，也不能被悄悄跳过。

</details>

## 可验证验收与自测

验收正常三条记录、空数组、重复 id、布尔金额、缺失文件与非法 JSON 六组输入。每次明确输出、退出码和错误位置；在虚拟环境解释器中运行，确认无需第三方包。

1. **类型提示会拒绝错误 JSON 吗？** 不会，外部输入仍需要运行时校验。
2. **为什么使用整数分？** 明确金额单位并避免直接用二进制浮点累计十进制金额。
3. **为什么不要把错误都转换为空列表？** 这会把输入损坏伪装成正常无数据，让下游形成错误结论。