想象一下手工盯盘的一天:你在笔记本上抄了 5 只股票的早盘价,中午又抄了一遍午盘价,晚上想算算每只涨了多少、均价几何、谁涨谁跌。用计算器要做十几次除法,抄错一个数整个结果就废了。而这几行「记录 → 存起来 → 批量算 → 汇总报告」的动作,恰好就是编程最擅长的事——上一篇文章里我们已经把环境装好、让程序跑了起来,本篇就把「写程序的手」装上。
上一篇我们用 uv 建好了独立虚拟环境,还跑通了自检脚本和第一张折线图,但代码里的 list、f-string、for 循环都是照抄的。本篇就在这个基础上,把量化最常用的 Python 语法一次讲透:变量与四种容器、切片、循环与推导式、函数与 lambda、模块导入、异常处理和字符串格式化。场景就用开头那个:手工记录一天 5 只股票的价格,算均值、最大最小值和涨幅。学完你就能把这类手工表格活儿全部交给代码。
先说一句定心话:本篇不追求把 Python 讲完——那需要一整本书——只讲量化里出现频率最高的那一小块。后面每篇用到新语法都会随手补讲,你只要把本篇的例子亲手敲一遍,就够用了。
本篇目标
- 能用 list、dict、tuple、set 分别保存行情数据,并说清四者各自适合什么场合。
- 能用切片取出「前 N 只」「每隔一只」「倒序」等子序列,并解释「切片是视图」的含义。
- 能用 for 循环和列表推导式两种写法,批量计算一组股票的涨跌幅并得到相同结果。
- 能把重复的计算封装成函数,用 lambda 指定排序规则,用 try/except 兜住录入错误。
- 能用 f-string 输出一张对齐的收盘小结,包含均价、最高价、最低价和上涨家数。
核心概念
变量与四种容器:数据放哪儿
变量(Variable)就是给数据贴个名字标签,price = 10.5 之后,写 price 就是在用 10.5 这个数。量化数据很少是单个数,重点是四种容器(Container,装多个数据的结构):
- 列表 list:有序、可增删改,最常用。
prices = [10.2, 10.5, 10.3],取第一个写prices[0]——下标(Index,元素的位置编号)从 0 开始数。 - 字典 dict:键值对(Key-Value,用名字查数)。
price_book = {"DEMO01": 10.45},按代码查价格写price_book["DEMO01"],适合「股票代码 → 行情」这类映射。 - 元组 tuple:和 list 很像但不可修改,适合「日期、代码、价格」这种天然绑定的固定搭配,写法用圆括号。
- 集合 set:元素不重复、无顺序,最适合去重和做「两个名单的交集并集」,写法用花括号。
选择口诀:要顺序用 list,要按名字查用 dict,不许改用 tuple,要去重用 set。
切片:一口气取一段
切片(Slicing)的通用写法是 seq[start:stop:step]:从 start 取到 stop 前一个为止(包头不包尾),step 是步长。prices[1:4] 取下标 1、2、3 三个元素;prices[:2] 取前两个;prices[::2] 隔一个取一个;prices[::-1] 步长为负就是倒序。还有个关键性质:切片得到的是原数据的「视图」——对切片元素赋值,原列表也会跟着变。这个特性在 NumPy 里更重要,下一篇会专门展开。
循环与推导式:批量的活儿交给机器
把 5 只股票的涨幅挨个算一遍,人要按 5 次计算器,程序只要一个 for 循环(Loop,重复执行一段代码的结构)。而 Python 还提供了更紧凑的写法——推导式(Comprehension),一行顶一个循环加一个 append:
squares = [x ** 2 for x in range(5)] # [0, 1, 4, 9, 16], 一行生成整个列表
规则是:先写「每个元素变成什么」,再写「从哪里取」。逻辑简单用推导式,逻辑超过一行就用普通循环,别为了省行数把代码写成人天书。
函数与 lambda:把算法装进盒子
函数(Function)把一段计算打包命名,以后「喊名字」就能复用。定义用 def,输入是参数(Parameter),输出用 return 交回。写函数的最大好处是同一逻辑只写一遍:涨幅公式写一次,5 只股票、500 只股票都调用它。lambda 则是「一次性小函数」,lambda pair: pair[1] 意思是「给我一个 pair,我返回它的第 1 号元素」,最常见的用途是给 sorted 这类函数当临时的排序依据。
模块与包:站在标准库的肩膀上
模块(Module)就是一个 .py 文件,包(Package)是一组模块的文件夹。import 之后,别人写好的轮子随便用:from statistics import mean 就借来了标准库的均值函数。你以后还会不断 import numpy、import pandas——整个量化生态就是这样一层层借来的。
try/except:出错不崩盘
手工录价格难免手滑打个 7.8x,float("7.8x") 会直接报错终止程序。异常处理(Exception Handling)就是提前铺好缓冲垫:try 里放「可能出错的代码」,except 里放「出了错怎么办」。程序从「一碰就崩」变成「报个提醒继续干活」,这在后面写策略监控时是保命技能。
f-string:把数字拼成报告
f-string(格式化字符串字面量)在字符串前加 f,花括号里直接填变量和表达式:f"均价 {avg:.2f}" 中 .2f 表示保留两位小数,>+6.2f 表示带正负号、右对齐占 6 格。收盘小结这种对齐的报告全靠它。
划重点:容器选型先想「以后怎么查这份数」——按下标查用 list,按名字查用 dict。选对了容器,后面一半的代码会自动变简单。
完整代码
依赖安装:本篇只用 Python 标准库,无需额外安装(沿用第 1 篇的 quant 环境即可)。第一段代码负责「记录与统计」,保存为 record_prices.py 运行:
# record_prices.py —— 手工记录一天 5 只股票的价格并计算基础统计
stock_codes = ["DEMO01", "DEMO02", "DEMO03", "DEMO04", "DEMO05"] # 5 只虚拟股票
morning_prices = [10.20, 15.80, 7.40, 22.10, 31.50] # 上午记录的参考价
afternoon_prices = [10.45, 15.35, 7.75, 21.80, 32.60] # 下午记录的参考价
# ---- 列表 list: 有序可变, 下标从 0 开始 ----
n = len(stock_codes) # 元素个数
first_code = stock_codes[0] # 第一个元素
last_price = afternoon_prices[-1] # -1 表示最后一个
print(f"共记录 {n} 只, 第一只是 {first_code}, 最新价 {last_price}")
# ---- 切片: [起点:终点:步长], 包头不包尾 ----
print(f"前两只: {stock_codes[:2]}") # 等价于 [0:2]
print(f"每隔一只抽样: {stock_codes[::2]}") # 步长 2
print(f"倒序: {stock_codes[::-1]}") # 步长 -1
# ---- 字典 dict: 按代码查价格 ----
price_book = dict(zip(stock_codes, afternoon_prices)) # 代码 -> 价格
price_book["DEMO03"] = 7.80 # 修正一笔手工录入(只改字典, 原列表不受影响)
print(f"DEMO03 修正后: {price_book['DEMO03']}, 列表里仍是 {afternoon_prices[2]}")
# ---- 元组 tuple: 不可变的固定搭配 + 解包 ----
record = ("2026-09-28", "DEMO01", 10.45) # (日期, 代码, 价格)
date_str, code, price = record # 解包: 一次拆给三个变量
print(f"{date_str} {code} 收在 {price}")
# ---- 集合 set: 去重与交集 ----
watch_morning = {"DEMO01", "DEMO02", "DEMO03"} # 上午观察名单
watch_afternoon = {"DEMO03", "DEMO04", "DEMO05"} # 下午观察名单
print(f"两个时段都在观察: {watch_morning & watch_afternoon}") # 交集
# ---- 循环与推导式: 批量算涨幅 ----
changes = [] # 先用普通循环写一遍
for i in range(n): # i 依次取 0,1,2,3,4
chg = (afternoon_prices[i] / morning_prices[i] - 1) * 100 # 涨幅百分比
changes.append(round(chg, 2)) # 保留两位小数后追加
changes_lc = [round((a / m - 1) * 100, 2) # 推导式: 同样的逻辑压成一行
for a, m in zip(afternoon_prices, morning_prices)]
print(f"循环版涨幅: {changes}")
print(f"推导式涨幅: {changes_lc}")
assert changes == changes_lc # 两种写法必须一致, 不一致直接报错
# ---- 内置统计 ----
print(f"均价: {sum(afternoon_prices) / n:.2f}") # sum 求和再除以数量
print(f"最高: {max(afternoon_prices)}, 最低: {min(afternoon_prices)}")
第二段代码负责「封装与报告」,把重复逻辑收进函数,生成收盘小结,保存为 report_prices.py 运行(文件放在同一目录,直接沿用上一段定义的变量)。注意:为了方便单文件运行,下面开头三行把上一段的数据定义重复了一遍,实际连跑时可以删掉:
# report_prices.py —— 把统计逻辑封装成函数, 生成收盘小结报告
from statistics import mean # 模块导入: 标准库现成的均值函数
stock_codes = ["DEMO01", "DEMO02", "DEMO03", "DEMO04", "DEMO05"]
morning_prices = [10.20, 15.80, 7.40, 22.10, 31.50]
afternoon_prices = [10.45, 15.35, 7.75, 21.80, 32.60]
def pct_change(new: float, old: float) -> float:
"""涨跌幅(百分比) = (新价 / 旧价 - 1) * 100。"""
if old == 0: # 防御: 旧价为 0 无法相除
raise ZeroDivisionError("参考价为 0, 无法计算涨幅")
return (new / old - 1) * 100
def summarize(codes: list[str], prices: list[float]) -> dict:
"""汇总一组价格: 均值、最高价、最低价及其归属。"""
if len(codes) != len(prices): # 长度不一致说明录错了
raise ValueError("代码与价格数量不一致, 请检查录入")
return {
"均价": round(mean(prices), 2),
"最高": (codes[prices.index(max(prices))], max(prices)), # index 反查归属
"最低": (codes[prices.index(min(prices))], min(prices)),
}
# ---- lambda: 一次性小函数, 当排序依据最常见 ----
pairs = list(zip(stock_codes, afternoon_prices)) # [(代码, 价格), ...]
top2 = sorted(pairs, key=lambda pair: pair[1], reverse=True)[:2] # 按价格降序取前二
print(f"价格前二: {top2}")
# ---- try/except: 录入出错不崩盘 ----
raw = "7.8x" # 模拟一次手滑录入
try:
fixed = float(raw) # 尝试把字符串转成数字
except ValueError: # 转换失败走这里, 而不是整个程序崩溃
print(f"'{raw}' 不是合法数字, 已记 0 待复核")
fixed = 0.0
finally:
print("本条录入已记录, 待人工复核") # 无论成败都执行
# ---- f-string: 对齐的收盘小结 ----
for code_str, m, a in zip(stock_codes, morning_prices, afternoon_prices):
chg = pct_change(a, m) # 调用刚定义的函数
arrow = "涨" if chg > 0 else ("跌" if chg < 0 else "平")
print(f"{code_str} {m:>6.2f} -> {a:>6.2f} {chg:>+6.2f}% {arrow}")
summary = summarize(stock_codes, afternoon_prices)
n_up = sum(1 for a, m in zip(afternoon_prices, morning_prices) if pct_change(a, m) > 0)
avg_chg = mean([pct_change(a, m) for a, m in zip(afternoon_prices, morning_prices)])
print(f"均价 {summary['均价']} | 最高 {summary['最高']} | 最低 {summary['最低']}")
print(f"上涨 {n_up}/{len(stock_codes)} 只, 平均涨幅 {avg_chg:+.2f}%")
代码逐段解释
- 容器各就各位:两个价格序列用 list(要按天顺序取),
price_book用 dict(要按代码查),单条记录用 tuple(日期代码价格天然绑定、不许改),观察名单用 set(只关心「在不在」,不关心顺序)。dict(zip(代码, 价格))是把两个列表按位置配对成字典的惯用法。 - 切片三连:
[:2]前两只、[::2]隔一取一、[::-1]倒序,覆盖了日常九成的取数需求。记住「包头不包尾」,[:2]拿到的是下标 0 和 1。 - 普通循环 vs 推导式:两者算出的涨幅完全一样,
assert那行就是让 Python 替我们验一遍——不一致会直接报AssertionError。推导式读法:「对 zip 出来的每一对 (a, m),算 round((a/m-1)*100, 2),收进列表」。 pct_change与summarize:涨幅公式只写一遍,之后每处调用都是一行;summarize开头先做长度校验,raise主动抛异常——把「数据不对」拦在计算之前,这是量化代码的基本素养。prices.index(max(prices))用值反查下标,从而找到最高价属于哪只股票。- lambda 排序:
sorted(pairs, key=lambda pair: pair[1], reverse=True)按「每对的 1 号元素(价格)」从大到小排,[:2]切出前二。lambda 只在一处使用、逻辑一行时才用,复杂逻辑请老实写def。 - try/except/finally:
float("7.8x")抛ValueError,被except接住,程序打印提醒并继续;finally无论成败都执行,适合放「记日志」这类收尾动作。这个模式以后会反复出现在数据清洗和网络请求里。 - f-string 对齐输出:
{m:>6.2f}表示右对齐(>)、总宽 6、两位小数;{chg:>+6.2f}多个+,正数也显式带加号,涨跌一眼可辨。最后一行统计上涨家数用的sum(1 for ... if 条件)是「数满足条件的个数」的惯用写法。
第一段代码的运行结果(示意输出):
共记录 5 只, 第一只是 DEMO01, 最新价 32.6
前两只: ['DEMO01', 'DEMO02']
每隔一只抽样: ['DEMO01', 'DEMO03', 'DEMO05']
倒序: ['DEMO05', 'DEMO04', 'DEMO03', 'DEMO02', 'DEMO01']
DEMO03 修正后: 7.8, 列表里仍是 7.75
2026-09-28 DEMO01 收在 10.45
两个时段都在观察: {'DEMO03'}
循环版涨幅: [2.45, -2.85, 4.73, -1.36, 3.49]
推导式涨幅: [2.45, -2.85, 4.73, -1.36, 3.49]
均价: 17.59
最高: 32.6, 最低: 7.75
第二段代码的运行结果(示意输出,涨幅与第一段一致,可自行验算,如 DEMO03:(7.75/7.40 − 1) × 100 ≈ 4.73):
价格前二: [('DEMO05', 32.6), ('DEMO04', 21.8)]
'7.8x' 不是合法数字, 已记 0 待复核
本条录入已记录, 待人工复核
DEMO01 10.20 -> 10.45 +2.45% 涨
DEMO02 15.80 -> 15.35 -2.85% 跌
DEMO03 7.40 -> 7.75 +4.73% 涨
DEMO04 22.10 -> 21.80 -1.36% 跌
DEMO05 31.50 -> 32.60 +3.49% 涨
均价 17.59 | 最高 ('DEMO05', 32.6) | 最低 ('DEMO03', 7.75)
上涨 3/5 只, 平均涨幅 +1.29%
划重点:涨幅=(新价/旧价−1)×100%。这个公式是整个系列出场率最高的公式,本篇先用手写循环实现,下一篇你会看到 NumPy 怎么把它压成一行。
常见坑与小练习
常见坑:
- 下标从 0 开始数错位。现象:想取第 3 只股票,写了
codes[3]结果拿到第 4 只。原因:Python 下标从 0 起算,第 3 只的下标是 2。避免:嘴里念「下标 = 位置 − 1」,或干脆用codes[i]配合range(len(codes))循环。 - 切片包头不包尾记反。现象:
prices[1:4]以为取 4 个,实际只有 3 个。原因:终点下标不包含在内。避免:把stop理解成「边界线」,元素站在边界线左边才算数。 float()直接转换用户输入。现象:输入里混进一个空格或字母,程序当场崩溃。原因:没做异常处理。避免:凡是「外部来的数据」(手输、文件、网络),转换一律包try/except。- lambda 里塞复杂逻辑。现象:一行 lambda 写了三层嵌套,两周后自己都读不懂。原因:把 lambda 当普通函数用。避免:超过一个表达式的逻辑就写成
def函数,名字也是文档。 - 用
==比较两个浮点列表。现象:明明看着一样,判断却是False。原因:浮点数有精度误差(如 0.1 + 0.2 ≠ 0.3)。避免:比较数值用math.isclose或 NumPy 的np.allclose(下一篇登场)。
小练习:
- 给
morning_prices和afternoon_prices再加两只虚拟股票,重跑两个脚本,核对上涨家数和均价是否随数据量正确变化。思路提示:两个列表要同步加,且
summarize里的长度校验会帮你把关——故意漏加一只,看看它怎么报错。 - 把「价格前二」改成「涨幅前二」:用 lambda 对 (代码, 涨幅) 的列表降序排序,打印前二名。
思路提示:先用推导式造
[(code, pct_change(a, m)) for ...],再把key=lambda pair: pair[1]套上去。 - 给
record_prices.py加一段try/except:模拟「上午少录了一只」的场景(删掉morning_prices最后一个元素),捕获zip长度不齐导致的结果异常并打印友好提示。思路提示:
zip本身不报错、只按短的截断,所以要在循环前用if len(...) != len(...)主动raise,再在外层try里接住。
本文内容仅用于教学,不构成投资建议,实盘前请充分测试。