第四章 Python进阶:函数定义与调用、模块与包、文件读写、异常处理、列表推导式
好,咱们进入Python进阶环节。说实话,基础语法就像认识字,而这一章的内容,才是真正开始「写文章」。函数、模块、文件操作、异常处理——这些是每个金融数据工程师吃饭的家伙。我当年刚入行时,就是靠把这些东西啃透,才从「能跑代码」变成了「能写工程」。
4.1 函数定义与调用:把代码装进盒子里
函数是什么?说白了,就是把一段逻辑打包,起个名字,随用随调。你想想看,如果每次计算夏普比率都要重写一遍公式,那得多崩溃?
核心语法:
def 函数名(参数1, 参数2=默认值):
"""文档字符串:说明函数做什么"""
# 函数体
return 返回值
我个人习惯,写函数一定加文档字符串。为什么?因为三个月后你自己都忘了这函数是干嘛的。我在项目中遇到过,同事写了个 calc(),没有注释,我看了半天才发现是在算年化波动率——嗯,从那以后我规定团队所有函数必须写 docstring。
4.1.1 参数传递:值传递 vs 引用传递
这里有个坑,我必须要讲。Python的参数传递,其实既不是值传递也不是引用传递,而是「对象引用传递」。什么意思?
def modify_list(lst):
lst.append(4) # 会修改原列表
lst = [1, 2, 3] # 不会修改原列表
my_list = [0]
modify_list(my_list)
print(my_list) # 输出 [0, 4]
看到了吗?append 操作修改了原对象,但重新赋值只是让局部变量指向了新对象。我曾经因为这个bug,让一个回测系统跑出了错误结果,排查了整整一个下午。
避坑指南:不要用可变对象作为默认参数!
def add_stock(stock, portfolio=[]): # 危险!
portfolio.append(stock)
return portfolio
# 正确做法:
def add_stock(stock, portfolio=None):
if portfolio is None:
portfolio = []
portfolio.append(stock)
return portfolio
4.1.2 匿名函数:lambda表达式
有些函数只用一次,没必要专门定义。这时候lambda就派上用场了。比如对股票代码排序:
stocks = ['600001', '000001', '300001']
stocks.sort(key=lambda x: x[0]) # 按首位数字排序
lambda说白了就是「一次性函数」。我建议只在逻辑简单时用,超过一行逻辑还是老老实实写def吧。
4.2 模块与包:组织你的代码
一个项目写下来,几百行代码是常事。全塞在一个文件里?那叫「屎山」。模块和包就是用来分门别类的。
知识体系:
- 模块:一个 .py 文件就是一个模块
- 包:包含 __init__.py 的文件夹
- 导入方式:import, from...import, import...as
我给你们画个图,看看金融数据项目常见的包结构:
实际项目中,我习惯在 __init__.py 里做两件事:一是定义 __all__ 控制导出,二是做懒加载。比如:
# data_fetch/__init__.py
__all__ = ['stock_api', 'bond_api']
def get_stock_data(code):
from .stock_api import get_data # 懒加载
return get_data(code)
小技巧:用 if __name__ == '__main__': 来写测试代码。这样模块被导入时不会执行测试,只有直接运行文件时才会跑。
4.3 文件读写:和硬盘打交道
金融数据工程师,天天和文件打交道。CSV、JSON、Excel——都是家常便饭。Python的文件操作,核心就三句话:打开、读写、关闭。
4.3.1 上下文管理器:with语句
我见过太多新手忘记关文件了。用 with 语句,自动帮你关,省心又安全。
# 读CSV文件
with open('stock_data.csv', 'r', encoding='utf-8') as f:
for line in f:
print(line.strip())
# 写JSON文件
import json
data = {'code': '000001', 'price': 12.5}
with open('stock_info.json', 'w') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
我个人习惯,所有文件操作都用 with。为什么?因为有一次生产环境出了异常,程序崩溃了,没来得及关文件,导致文件锁死,后续任务全部失败——嗯,从那以后我强制团队用 with。
4.3.2 常用文件格式处理
| 格式 | 推荐库 | 典型场景 | 注意事项 |
|---|---|---|---|
| CSV | csv, pandas | 日频行情数据 | 注意编码(GBK/UTF-8) |
| JSON | json | API返回、配置 | ensure_ascii=False |
| Excel | openpyxl, xlrd | 财报、研报 | 大文件慎用 |
| Parquet | pyarrow, fastparquet | 高频数据、列存 | 压缩率高,读取快 |
注意:金融数据经常遇到编码问题。我建议统一用 UTF-8,如果遇到乱码,试试 GBK 或 GB2312。特别是从Wind或聚宽导出的数据,经常是GBK编码。
4.4 异常处理:让程序更健壮
代码总会出错。网络断了、文件没了、数据格式不对——异常处理就是给程序穿上防弹衣。
4.4.1 try-except 结构
try:
price = get_stock_price('000001')
print(f"当前价格:{price}")
except ConnectionError:
print("网络连接失败,稍后重试")
except ValueError as e:
print(f"数据格式错误:{e}")
except Exception as e:
print(f"未知错误:{e}")
# 记录日志
log_error(e)
else:
print("获取成功!")
finally:
print("无论成功失败,都会执行")
我曾经在写量化回测时,没有处理除零异常,结果一个股票停牌导致波动率为0,回测直接崩溃——嗯,从那以后我养成了「能捕获的异常一定要捕获」的习惯。
最佳实践:
- 捕获具体异常,不要裸写
except: - 异常粒度要适中,太细代码冗余,太粗掩盖问题
- 关键操作(如数据库写入)一定要加异常处理
4.4.2 自定义异常
当内置异常不够用时,可以自己定义。比如金融数据特有的「数据缺失异常」:
class DataMissingError(Exception):
"""数据缺失异常"""
def __init__(self, stock_code, date):
self.stock_code = stock_code
self.date = date
super().__init__(f"{date} 缺失 {stock_code} 的数据")
# 使用
if data is None:
raise DataMissingError('000001', '2024-01-01')
4.5 列表推导式:Pythonic的写法
列表推导式,说白了就是「用一行代码搞定循环」。它让代码更简洁,更易读——前提是你得习惯这种写法。
4.5.1 基础用法
# 传统写法
prices = []
for stock in stocks:
if stock['price'] > 10:
prices.append(stock['price'])
# 列表推导式
prices = [stock['price'] for stock in stocks if stock['price'] > 10]
我个人习惯,简单的逻辑用推导式,复杂的逻辑还是老老实实写循环。为什么?因为可读性更重要。你想想看,一行代码写50个字符,别人看不懂,那还不如拆成三行。
4.5.2 进阶用法
| 场景 | 示例 | 说明 |
|---|---|---|
| 嵌套循环 | [x*y for x in [1,2] for y in [3,4]] |
先遍历x,再遍历y |
| 条件过滤 | [x for x in range(10) if x%2==0] |
只保留偶数 |
| 字典推导式 | {k:v for k,v in zip(keys, vals)} |
快速构建字典 |
| 集合推导式 | {x%3 for x in range(10)} |
去重 |
实战案例:计算股票池中所有股票的日收益率
# 假设 prices 是字典,key为股票代码,value为价格列表
daily_returns = {
code: [(prices[i]-prices[i-1])/prices[i-1]
for i in range(1, len(prices))]
for code, prices in stock_data.items()
}
注意:列表推导式虽然简洁,但不要滥用。嵌套超过两层,或者逻辑过于复杂,建议用普通循环。记住:代码是写给人看的,顺便给机器执行。
好了,这一章的内容就到这里。函数、模块、文件、异常、推导式——这些是金融数据工程师的日常工具。多写、多用、多踩坑,慢慢就熟练了。记住我的一句话:好的代码不是一次写成的,而是不断重构出来的。
公众号:蓝海资料掘金营,微信deep3321