第三章 类型注解与文档字符串:让代码自己会说话
说实话,我见过太多交易系统的代码了。有些代码写得像天书,变量名全是 a、b、c,函数参数全靠猜。你想想看,一个策略信号生成函数,传进来三个参数,你根本不知道哪个是价格、哪个是成交量、哪个是时间戳。这种代码,别说别人看不懂,过两周你自己都忘了。
这一章,我们就来解决这个问题。用类型注解让代码意图一目了然,用文档字符串让函数自己会说话。我在量化交易系统里吃过不少亏,后来养成了写类型注解和文档的习惯,代码质量提升了一个档次。
3.1 Python类型注解入门
Python 3.5 之后引入了类型注解(Type Hints)。说白了,就是给变量、参数、返回值标注上类型。Python 本身不强制检查类型,但有了注解,IDE 和静态检查工具就能帮你发现潜在问题。
核心思想:类型注解不是约束,而是文档。它告诉读代码的人:「这个参数应该是 int,那个返回值是 str」。
先看个最简单的例子:
def add(a: int, b: int) -> int:
return a + b
这里的 a: int 表示 a 应该是整数,-> int 表示返回值也是整数。嗯,就这么简单。
我在项目中遇到过一个问题:有个同事写了个计算夏普比率的函数,参数传进来的是字符串,结果运行时直接报错。如果当时写了类型注解,这种低级错误根本不会发生。
再看几个常见场景:
# 列表类型
def calculate_returns(prices: list) -> list:
return [prices[i] - prices[i-1] for i in range(1, len(prices))]
# 字典类型
def get_position(symbol: str, portfolio: dict) -> float:
return portfolio.get(symbol, 0.0)
# 可选类型
def fetch_data(symbol: str, start_date: str = None) -> dict:
if start_date is None:
start_date = "2020-01-01"
# ... 获取数据逻辑
我的习惯:所有公开函数都写类型注解。内部辅助函数可以酌情省略,但核心逻辑一定要有。这花不了多少时间,但能省下大量调试时间。
3.2 Typing模块详解
基础类型注解够用了,但交易系统里经常遇到更复杂的类型。比如一个函数返回「要么是 float,要么是 None」,或者参数是「字符串列表」。这时候就需要 typing 模块了。
我个人最常用的几个类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| Optional | 可选类型,等价于 Union[X, None] | def get_price(symbol: str) -> Optional[float] |
| Union | 联合类型,可以是多种类型之一 | def parse_value(data: Union[str, float]) -> float |
| List | 列表,可指定元素类型 | def filter_symbols(symbols: List[str]) -> List[str] |
| Dict | 字典,可指定键值类型 | def update_portfolio(holdings: Dict[str, float]) |
| Tuple | 元组,可指定每个位置类型 | def get_best_bid_ask() -> Tuple[float, float] |
| Callable | 可调用对象(函数) | def apply_strategy(signal_fn: Callable[[float], bool]) |
看个实际例子。我在写回测引擎时,有个函数需要接收一个「信号生成函数」:
from typing import List, Dict, Callable, Optional
def run_backtest(
prices: List[float],
signal_generator: Callable[[List[float]], List[int]],
initial_capital: float = 10000.0
) -> Dict[str, float]:
"""
运行回测
Args:
prices: 价格序列
signal_generator: 信号生成函数,输入价格序列,输出信号列表(1买入,-1卖出,0持有)
initial_capital: 初始资金
Returns:
包含回测结果的字典,如 {'total_return': 0.15, 'sharpe': 1.2}
"""
# ... 回测逻辑
pass
你看,有了类型注解,这个函数的输入输出一目了然。别人调用时,不用翻文档就知道该传什么。
避坑指南:我曾经在代码里用了 List 但忘了从 typing 导入,结果 IDE 报错。记住:基础类型如 list、dict 可以直接用,但泛型类型如 List[int] 必须从 typing 导入。
3.3 Google风格Docstring编写
类型注解解决了「是什么类型」的问题,但没解决「这个函数是干什么的」的问题。这就需要文档字符串(Docstring)了。
文档字符串的写法有很多种,我个人最推荐 Google 风格。为什么?因为它结构清晰,可读性强,而且能被 Sphinx 自动解析生成文档。
Google 风格的基本结构:
def calculate_moving_average(
prices: List[float],
window: int = 20
) -> List[float]:
"""
计算移动平均线。
使用简单移动平均(SMA)方法,对价格序列进行平滑处理。
常用于趋势跟踪策略的信号生成。
Args:
prices: 价格序列,按时间升序排列
window: 移动窗口大小,默认20
Returns:
移动平均线序列,长度与输入相同,前 window-1 个值为 NaN
Raises:
ValueError: 当 window 小于 1 或 prices 为空时抛出
Example:
>>> prices = [1.0, 2.0, 3.0, 4.0, 5.0]
>>> calculate_moving_average(prices, 3)
[nan, nan, 2.0, 3.0, 4.0]
"""
if window < 1 or not prices:
raise ValueError("window must be >= 1 and prices must not be empty")
result = [float('nan')] * (window - 1)
for i in range(window - 1, len(prices)):
result.append(sum(prices[i - window + 1:i + 1]) / window)
return result
注意几个要点:
- 第一行是简短描述,用一句话说清楚函数功能
- 空一行后写详细描述,可以多行
- Args 部分:每个参数一行,格式是「参数名: 描述」
- Returns 部分:描述返回值,包括类型和含义
- Raises 部分:列出可能抛出的异常
- Example 部分:给出使用示例,方便别人快速理解
我的经验:写文档字符串时,把自己当成第一次看这个函数的人。想想你最想知道什么?参数含义、返回值、异常情况、使用示例。把这些写清楚就够了。
3.4 Sphinx自动生成文档
代码写好了,文档也写了,但没人愿意翻代码看文档。这时候就需要 Sphinx 了。它能自动从你的代码和文档字符串中生成漂亮的 HTML 文档。
配置 Sphinx 其实很简单:
# 1. 安装 Sphinx
pip install sphinx sphinx-autodoc
# 2. 初始化 Sphinx 项目
sphinx-quickstart docs
# 3. 在 conf.py 中启用 autodoc
extensions = ['sphinx.ext.autodoc']
# 4. 生成文档
cd docs
make html
然后你需要在 .rst 文件中引用你的模块:
.. automodule:: your_trading_module
:members:
:undoc-members:
:show-inheritance:
运行 make html 后,Sphinx 会扫描你的代码,提取所有文档字符串,生成漂亮的 HTML 文档。每个函数、类、模块都有独立的页面,参数、返回值、示例都清晰展示。
我在团队里推行过这个流程。一开始大家觉得麻烦,但看到自动生成的文档后,都说「真香」。特别是新同事入职时,直接看文档就能上手,不用追着老同事问了。
核心价值:类型注解 + 文档字符串 + Sphinx = 代码即文档。你只需要维护一份代码,文档自动生成,永远不会过时。
3.5 本章知识体系
下面这张图展示了本章的核心知识结构:
这三者相辅相成:类型注解告诉别人「是什么」,文档字符串告诉别人「干什么」,Sphinx 把这一切变成漂亮的文档。少了任何一个环节,代码的可维护性都会打折扣。
好了,这一章的内容就到这里。记住:写类型注解和文档字符串不是浪费时间,而是在为未来的自己铺路。你想想看,三个月后你回来改代码,看到清晰的类型注解和完整的文档,那种感觉有多爽?