← 返回模块
3.1.1.4beta 可读 · 未来付费校验通过内容版本 2026-05-26

模块、异常与标准库入门

3.1.1 · Python 语言基础 · 编程

周五下午——日内流改完后第三周。风险口要你每天产出的同一个 P&L 数字,但要求改成「他们的收盘脚本能 import 进去用的函数」。所以那个一直被你贴到每个文件里的 pnl(p0, p1, ...) 得从「笔记本里的片段」升级成「另一个组按名导入的模块」。雪上加霜的是上午一条沪深300 ETF 的 tick 飞来一个 price = 0,你的除法直接抛 ZeroDivisionError,整批跑挂了。下周一交给风控的报告要好看一点,下次得让脚本把坏数据这一行记下来、然后继续跑。这一课覆盖把片段变成正经程序的三件事:导入模型、异常机制,以及每天都会拿来用的几个 stdlib 模块。

导入模型

每个 .py 文件就是一个模块。一个进程第一次 import pnl_utils 时,Python 在 sys.path 上找到 pnl_utils.py,在全新的命名空间里从头到尾跑一遍,把该命名空间作为模块对象绑在导入方的名字 pnl_utils 上;后续再 import 同名模块直接返回缓存,不会重跑。

三种导入写法都常见:

import math               # binds `math`; use as math.sqrt(2)
from math import sqrt     # binds `sqrt` directly; use as sqrt(2)
import math as m          # alias; use as m.sqrt(2)

短脚本里默认 import math / from math import sqrt;别名留给名字太长的库。两文件项目:

# pnl_utils.py
def pnl(p0, p1, shares=100, fee=0.0):
    """股票化 P&L:价差乘股数,再扣手续费。"""
    return (p1 - p0) * shares - fee

# run.py
from pnl_utils import pnl
print(pnl(100.0, 101.5))

if __name__ == "__main__": 守卫把「文件作为主脚本执行时跑」和「文件被 import 时跑」分开。Python 把主脚本的 __name__ 设为字符串 "__main__";被 import 时则是该模块自己的名字。于是:

# pnl_utils.py — add at the bottom
if __name__ == "__main__":
    # smoke test that runs only when you do `python pnl_utils.py`
    assert pnl(100.0, 101.5) == 150.0
    print("ok")

包(package)就是一个含有(可以为空的)__init__.py 文件的目录,加上更多模块。多级点分导入、命名空间包(namespace package)、可编辑安装(editable install)这些深层包布局留给 3.1.3。

异常胜过哨兵返回值

Python 处理错误的惯用风格是「先做、出问题再说」(ask forgiveness, not permission):让操作自然抛出,捕获自己能处理的,其余让它继续往上传。异常层级根在 BaseException,几乎所有你会去 except 的都是 Exception 的子类。本周会撞上的几个:ValueError(类型对、值不对——int("abc"))、TypeError(类型不对——len(5))、KeyError(dict 键缺失)、IndexError(list 下标越界)、ZeroDivisionError(早上那个 feed 问题)、FileNotFoundError(迟到的 CSV)。

完整四段式的执行顺序很精确:

def validated(price):
    if price <= 0:
        raise ValueError("价格必须为正")
    return price

try:
    p = validated(-1.0)
except ValueError as exc:
    print(exc)              # "价格必须为正"
else:
    # runs only if try-block did NOT raise
    print(f"ok: {p}")
finally:
    # always runs, raise or no raise
    print("done")

try: 包住可能抛错的操作;except <Type> as exc: 捕获该类型异常并把异常对象绑到 excelse: 仅在 try 块未抛错时执行;finally: 无论抛没抛都跑——即便 try 里走 return。要捕获到能恢复的最窄类型;裸 except: 几乎都是 bug,因为它会吞 KeyboardInterrupt 也会掩盖真正的错。

在按行 for 循环里,把循环体外套一层 try / exceptcontinue 跳过坏行——脚本现在能把 ZeroDivisionError 那一行记下来后继续,而不是一头栽倒在地。

上下文管理器与文件 I/O

手动开关文件看着也行——直到 close() 之前抛了个异常、文件句柄漏掉。with 保证收尾。典型写法:

with open("ticks.csv") as f:
    text = f.read()
# file is closed here, even if read() raised

with EXPR as NAME: 调用 EXPR.__enter__() 完成开场,把结果绑到 NAME,跑完块体后调用 __exit__() 收场——抛错与否都执行。文件离开 with 块时一定被关闭,即使中途抛错。自己写上下文管理器属于 3.1.2 的范围;现在你要养成的是「每次碰文件就 with open(...)」的反射动作。

每周都会用到的四个 stdlib 模块

math 装着内置之外的小型数值例程。math.sqrt(2) 求平方根;math.log(x)math.exp(x)math.pimath.e 凑齐工具箱。一次性运算用它;需要向量化数组时再上 NumPy(subject 3.2)。

datetime 管日期和时间戳。date(2025, 9, 30) 是一个日期,datetime(2025, 9, 30, 15, 0) 加上时刻,timedelta(days=8) 是一段间隔。算术写法是显然的:date(2025, 10, 8) - date(2025, 9, 30) 等于 timedelta(days=8)——正是国庆假期前后的日历天数。要按 SSE 实际开市日计算,得用 3.6 的交易日历;这里日历天数够用。

collections 补上几个本该内置的数据结构。Counter(seq) 返回一个 dict-like,把 seq 里每个元素映射到其出现次数——替掉 for x in seq: c[x] = c.get(x, 0) + 1 的样板代码。defaultdict(list) 是一个 dict,缺失键会用你传入的工厂自动创建——替掉 setdefault 的样板代码。

pathlib 给你 Path——一个知道自己是文件系统路径的对象。Path("data/ticks.csv").exists()Path(...).read_text()Path(...).with_suffix(".json")——字符串从路径处理代码里消失,同一份代码在不同 OS 上都跑。

收尾综合:四块拼成一个脚本

风险口要的收盘脚本现在长成一个文件:import 进来 pnl、逐行校验、用 with 打开输入、用 stdlib 工具汇总:

from pathlib import Path
from collections import Counter
from pnl_utils import pnl


def validated_price(price):
    if price <= 0:
        raise ValueError("价格必须为正")
    return price


path = Path("ticks.csv")
totals_by_code = Counter()
skipped = 0
with open(path) as f:
    rows = f.read().splitlines()

for row in rows[1:]:                                # skip header
    code, p0_str, p1_str = row.split(",")
    try:
        p0 = validated_price(float(p0_str))
        p1 = validated_price(float(p1_str))
    except ValueError as exc:
        skipped += 1
        continue
    totals_by_code[code] += pnl(p0, p1, shares=100)

print(totals_by_code.most_common(3), f"skipped={skipped}")

四块逐一就位:from pnl_utils import pnl(导入)、raise ValueError + try / except ValueError(异常机制)、with open(path) as f:(上下文管理器)、Path + Counter(stdlib)。skipped 计数就是风控想看到的那一条审计行。

练习

Exercise

写一个函数 safe_divide(a, b),正常情况下返回 a / b,但当 b == 0 时不抛错、改返回字符串 "undefined"。用 try / except 实现,不要预先检查 b。验证 safe_divide(10, 2) 返回 5.0,safe_divide(7, 0) 返回 "undefined"。

提示
形态:try: return a / b except ZeroDivisionError: return "undefined"。捕获​​具体​​类型 ZeroDivisionError,不要用裸 except:——后者会吞掉所有无关错误。
提示
两个验证都做:safe_divide(10, 2) 应返回浮点 5.0,不是整数 5safe_divide(7, 0) 进入 except 分支、返回字面字符串 "undefined"。返回类型要严格对上。

衔接下一课

至此你能把脚本拆到多个文件里、按名导入函数、捕获能恢复的失败让其余继续上传、确保打开的文件被关好、以及从四个 stdlib 模块里挑出几样短小好用的操作。这一课收完整个语言基础模块。下一门子主题 3.1.2(Idioms & Tooling)会叠上生产环境 Python 的惯例:PEP 8 排版、typing 类型注解、dataclasses、装饰器、pyproject.toml 打包、pytest 测试,以及 ruff / black / mypy 这套 lint / 格式化工具链。下个子主题开始前的过渡阅读:Python 官方教程第 6、8、10 章;《流畅的 Python》第 2 版第 18 章对 with 和上下文管理器有更深一层的剖析。