4、Python基础回顾(一):数据类型、控制流、函数定义,重点讲解生成器与装饰器在数据处理中的应用
说实话,很多做金融数据分析的朋友,Python基础其实都停留在「能跑就行」的阶段。我自己带团队时发现,真正卡住大家的往往不是那些高大上的算法,而是对基础数据结构的理解不够深。今天咱们就花点时间,把这块地基夯实了。
4.1 数据类型:选对容器,事半功倍
Python内置的数据类型,说白了就是装数据的容器。选错了容器,后面写代码会非常痛苦。
| 类型 | 特点 | 金融场景举例 |
|---|---|---|
| list | 有序、可修改、可重复 | 存储每日收盘价序列 |
| tuple | 有序、不可修改 | 存储交易对(BTC/USDT) |
| set | 无序、唯一、可去重 | 筛选出现过的股票代码 |
| dict | 键值对、查找快 | 股票代码→公司名称映射 |
我个人习惯是:能用dict就别用list做查找。我在项目中遇到过有人用list存了10万条交易记录,每次查找都遍历一遍,结果一个接口响应时间飙到3秒。换成dict后,直接降到毫秒级。
核心原则:数据量超过1000条时,优先考虑dict或set。list适合小规模、需要保持顺序的场景。
4.2 控制流:别写「面条代码」
控制流就是if-else和循环。但很多人写着写着就变成了「面条代码」——层层嵌套,自己都看不懂。
举个例子,判断一个交易信号是否有效:
# 不好的写法
if signal == 'buy':
if volume > 1000:
if price < ma20:
execute_trade()
else:
pass
else:
pass
else:
pass
# 我推荐的写法
def should_execute(signal, volume, price, ma20):
if signal != 'buy':
return False
if volume <= 1000:
return False
if price >= ma20:
return False
return True
if should_execute(signal, volume, price, ma20):
execute_trade()
你想想看,第二种写法是不是清晰多了?提前return这个技巧,能帮你省掉无数个else。
小技巧:循环里尽量用for而不是while。for天然有边界,不容易写出死循环。我曾经在回测系统里写了个while True忘记加break,结果服务器跑了整整一宿……
4.3 函数定义:小而美才是王道
函数设计的核心就一句话:一个函数只做一件事。
我见过最离谱的函数,一个函数里同时做了:数据清洗、计算指标、生成图表、发送邮件。这种函数改起来简直是噩梦。
# 好的拆分方式
def clean_data(raw_data):
"""清洗数据:去空值、去异常值"""
pass
def calculate_ma(prices, window=20):
"""计算移动平均线"""
pass
def plot_chart(dates, prices, ma):
"""绘制K线图"""
pass
def send_report(email, chart_path):
"""发送邮件报告"""
pass
每个函数控制在20行以内。如果超过20行,说明该拆了。
4.4 生成器:处理海量数据的利器
做金融数据处理,最怕的就是内存爆炸。比如你要处理一年的逐笔交易数据,几千万行,一次性读到内存里,16G的机器直接卡死。
这时候生成器就派上用场了。生成器不会一次性把所有数据加载到内存,而是用多少取多少。
def read_trades(file_path):
"""逐行读取交易数据,返回生成器"""
with open(file_path, 'r') as f:
for line in f:
yield parse_trade(line)
# 使用生成器处理
for trade in read_trades('trades.csv'):
if trade['price'] > 100:
process_trade(trade)
注意那个yield关键字。它和return的区别是:return结束函数,yield暂停函数,下次调用时从暂停处继续。
避坑指南:生成器只能遍历一次。我曾经犯过这个错——把生成器传给两个函数,第二个函数拿到的是空的。解决办法很简单:如果需要多次使用,用list()转成列表,或者重新创建生成器。
生成器还有一个妙用:无限序列。比如生成斐波那契数列,用列表存会爆内存,但生成器可以一直生成下去:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 取前10个
fib = fibonacci()
for _ in range(10):
print(next(fib))
4.5 装饰器:给函数「开挂」
装饰器是Python里最优雅的特性之一。说白了,它就是在不修改原函数代码的情况下,给函数增加额外功能。
金融场景里最常用的装饰器就是计时和重试。
import time
from functools import wraps
def timer(func):
"""计算函数执行时间"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f'{func.__name__} 耗时 {elapsed:.2f} 秒')
return result
return wrapper
@timer
def backtest_strategy(data):
"""回测策略"""
# 模拟耗时计算
time.sleep(2)
return {'sharpe': 1.5, 'max_drawdown': 0.1}
# 调用时自动打印耗时
result = backtest_strategy(data)
另一个实用场景是重试装饰器。做量化交易时,网络请求经常超时,手动写重试逻辑很烦:
def retry(max_attempts=3, delay=1):
"""失败自动重试"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f'第{attempt+1}次失败,{delay}秒后重试...')
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=5, delay=2)
def fetch_market_data(symbol):
"""获取行情数据"""
# 可能超时的网络请求
pass
注意:装饰器会改变函数的元信息(如函数名、文档字符串)。记得用@wraps修复,否则调试时看到的函数名全是wrapper,非常坑。
4.6 生成器+装饰器:组合拳打天下
这两个东西组合起来,能解决很多实际问题。我举个例子:流式数据处理管道。
def data_source():
"""数据源:生成器"""
for i in range(100):
yield i
@timer
def process_pipeline(source):
"""处理管道"""
# 过滤
filtered = (x for x in source if x % 2 == 0)
# 转换
transformed = (x * 2 for x in filtered)
# 聚合
result = sum(transformed)
return result
# 整个管道都是惰性求值,内存占用极低
result = process_pipeline(data_source())
print(f'结果: {result}')
你想想看,如果数据量是1亿条,用列表推导式直接内存爆炸。但用生成器表达式(注意是圆括号不是方括号),内存占用几乎为0。
我的经验:在金融数据处理中,能惰性求值就别急着计算。生成器+装饰器的组合,让我在处理千万级逐笔数据时,内存占用始终控制在200MB以内。而同事用列表实现同样的功能,直接OOM了。
知识体系总览
下面这张图,帮你把今天讲的内容串起来:
嗯,今天的内容就到这。记住一句话:基础不牢,地动山摇。这些看似简单的知识点,在实际项目中能帮你省下大量调试时间。下次遇到数据处理性能问题,先想想是不是数据类型选错了,或者能不能用生成器优化一下。
公众号:蓝海资料掘金营,微信deep3321