旁边那台的同事盯沪深300 ETF 的日内 tick 流。早上 10 点,她已经积了 8 万条 tick 行的 CSV;她的开盘脚本只是一段 Python:把每行读进来,过滤掉 volume <= 100 的,乘出 price * volume 作为成交金额(notional turnover),再按代码加总,打印前五大成交。今天早上她重写了内层循环,整段跑得快了 7 秒。诀窍不在算法——而在把二十行 for-with-append 删掉,换成两条列表推导式(list comprehension)加一次 dict.get(code, 0.0)。这就是本课要给你的工具:会按访问模式挑容器、不靠 Stack Overflow 就能切片、把开盘那段日内过滤写成一行可读表达式。
四种容器,四种用途
Python 内置四种容器,覆盖了一个单进程脚本里要用到的绝大多数场景。list 有序、可变、可重复(ordered, mutable, allows duplicates),是「我要一段逐步搭起来的序列」的默认选项。tuple 有序、不可变(ordered, immutable);用在固定记录(一个 (code, price, volume) 三元组)或需要可哈希(hashable)以便放进 set / dict 键时。dict 是哈希键映射(hash-keyed mapping),平均 O(1) 查找——表达「拿代码查 股票名称」或「按代码计数」就用它。set 是无序的唯一元素集合,平均 O(1) 成员查询,支持代数运算 |(并)、&(交)、-(差)、^(对称差)。
口诀:有序且要重复 → list;固定字段记录 → tuple;键到值 → dict;问「在不在」/「两边交集」 → set。
索引与切片
list、str、tuple 都支持按位置索引;负下标从右数,a[0] 是首项、a[-1] 是末项。切片 a[start:stop:step] 返回新对象,start 包含、stop 不包含,二者皆可缺省,step 为负则反向。四个值得背下来的惯用写法:
a = [10, 20, 30, 40, 50]
a[::-1] # [50, 40, 30, 20, 10]
a[1:-1:2] # [20, 40]
a[:3] # [10, 20, 30]
a[-1] # 50
切片总是新建对象——a[:] is a 是 False;这也是浅复制(shallow copy)的一行写法 b = a[:]。
list 的方法分两类,混淆会被 NoneType has no attribute ... 这条异常咬一次:原地修改(in-place mutation)的 lst.append(x)、lst.extend(other)、lst.pop(i)、lst.sort()——它们改 lst 并返回 None;不修改原对象的 sorted(lst)、lst + other、lst * 2——返回新列表。
字典与集合
dict 字面量长这样:{ "510300": "300ETF", "510500": "500ETF" }。键必须可哈希(hashable)——字符串、数字、可哈希元素组成的元组都行,list 不行。读取用 d[key](缺失抛 KeyError)或 d.get(key, default)(缺失返回默认值)。d[key] = d.get(key, 0) + amount 是按桶累加(bucket counter)的惯用写法;当桶里装的是列表则用 d.setdefault(key, []).append(item)。写到第一百次时你就会去查下一课的 collections.Counter。按键遍历 for k in d:,按 (键, 值) 遍历 for k, v in d.items()。
set 字面量长这样:{ "600519", "510300", "159919" }。s.add(x)、s.remove(x) 原地修改;x in s 平均 O(1)。代数写法很省事:held & watchlist 同时持有且关注的代码;held - covered 持有但尚未对冲的头寸。
遍历助手与解包
三种模式几乎出现在每个数据循环里。enumerate(seq) 产出 (下标, 元素) 对,省掉 i = 0; i += 1 的丑陋写法。zip(seq1, seq2) 同步遍历两条序列、在较短那条用尽时停下;「从价格序列算收益率」就是一行。元组解包(tuple unpacking)让你不靠下标拿到元素:
prices_t0 = [18.30, 1820.50, 3.45]
prices_t1 = [18.55, 1825.10, 3.46]
for code_index, (p0, p1) in enumerate(zip(prices_t0, prices_t1)):
print(code_index, (p1 - p0) / p0)
星号解包(star unpacking)偶尔好用:first, *rest = lst 把 first 绑到 lst[0],rest 绑到余下。
推导式
for-with-append 实在出现得太多,Python 给它配了专门的语法。列表推导式 [expr for x in it if cond] 是单个表达式,从一个可迭代对象构造新列表,过滤可选。同一计算两种写法:
# loop form, three lines
squares = []
for x in range(10):
if x % 2 == 0:
squares.append(x * x)
# comprehension, one line — same result
squares = [x * x for x in range(10) if x % 2 == 0]
两段都得到 [0, 4, 16, 36, 64]。推导式不仅更短,还更快(没有每次 append 的 LOAD_METHOD 与 CALL),也更难写错(少了一处可能忘掉的 append)。字典推导式与集合推导式同构:{k: v for ... } 和 {expr for ... }。
值得一记的还有生成器表达式(generator expression) (expr for x in it)。圆括号(或作为单一函数实参时省掉外层括号)让它惰性求值——只有外层迭代时才逐项算出。写成 sum(x*x for x in range(10_000_000)),它把一千万个平方一一流过 sum、不在内存里建任何列表;列表推导式版本要把一千万个 int 对象全部物化(CPython 上每个小整数对象约 28 字节,整体几百 MB 起步)。默认用列表推导式;序列大、且只消费一次时换成生成器表达式。
把开头那段日内过滤压缩成一行:
ticks = [
{"code": "600519", "price": 1820.50, "volume": 200},
{"code": "000001", "price": 12.30, "volume": 80},
{"code": "510300", "price": 4.05, "volume": 1200},
{"code": "600519", "price": 1820.30, "volume": 300},
{"code": "510300", "price": 4.06, "volume": 900},
]
notional = [t["price"] * t["volume"] for t in ticks if t["volume"] > 100]
# [364100.0, 4860.0, 546090.0, 3654.0]
二十行删到四行,懂语法的同事五秒就能审完——这是要追的标准。
练习
Exercise
Given prices = [100.0, 101.5, 99.0, 102.0, 103.5, 100.5], write a one-line list comprehension that produces the list of simple returns [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))]. Confirm the first entry rounds to 0.015 and the list has length 5.
提示
[(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))]。range 从 1 开始,因为下标 0 没有前一价;这也是结果长度等于 len(prices) - 1 的原因。提示
(101.5 - 100.0) / 100.0 = 0.015。长度:range(1, 6) 给出 5 个值 1, 2, 3, 4, 5,因此推导式产出 5 个收益。衔接下一课
你已能挑容器、做切片、并发遍历、把十行循环压成一行推导式。但一个真实程序的外壳还差几样:怎么把代码拆到两个文件、互相 import;怎么处理严格校验函数抛出的 ValueError;怎么用 with open(...) 读那个开盘脚本真正在打开的 CSV。下一课覆盖 Python 的导入模型、try / except / else / finally、with 上下文管理器(context manager),以及 math / datetime / collections / pathlib 四个 stdlib 模块——你每个工作日都会拿出来用的那几样。延伸阅读:廖雪峰列表 / 元组 / dict / set / 迭代 / 生成器各节,《流畅的 Python》第 2 版第 2、3 章。