本页目录

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

用现有 JavaScript 经验理解 Python 的运行环境、数据类型、文件处理、异常与类型提示,完成可执行的数据校验脚本。

L2 · 能交付约 18 分钟阅读含示例、练习与验收
本页内容

用途、程度与前置#

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

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

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

Python 解释器类似执行 JavaScript 的运行时,但不同 Python 项目可能依赖不同包版本。venv 创建项目独立环境,作用接近把依赖环境隔离在项目中;它不是虚拟机,也不会自动解决依赖锁定或操作系统库依赖。运行时版本仍需明确记录。venv 文档

建议显式调用 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 教程

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

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

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

模块名也可能冲突。将自己的文件命名为 json.pycsv.pyrequests.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 任务与取消

集合操作的返回值也属于 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 把所有问题统一变成“数据格式不对”。

参考实现

下面代码用于替换 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,它不会被这个边界吞掉,而会保留堆栈,提醒开发者这是代码缺陷。业务边界不是把错误藏起来,而是对已知失败提供稳定合同,对未知缺陷保留足够诊断信息。

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

练习、提示与参考解答#

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

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

参考答案

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

可验证验收与自测#

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

  1. 类型提示会拒绝错误 JSON 吗? 不会,外部输入仍需要运行时校验。
  2. 为什么使用整数分? 明确金额单位并避免直接用二进制浮点累计十进制金额。
  3. 为什么不要把错误都转换为空列表? 这会把输入损坏伪装成正常无数据,让下游形成错误结论。
原有课程整理于 2026-09-10;Node / Electron 扩充于 2026-09-11。示例环境与验证范围以正文为准。
原创中文学习手册,阅读结构参考 Vue 文档;非 Vue 官方教材。
下载本章 Markdown

支持中文和英文全文搜索 · ↑ ↓ 选择 · Enter 打开 · Esc 关闭