4. Python基础速通(二):文件读写操作、异常处理机制、面向对象编程入门、装饰器与闭包

好,咱们接着聊。上一章我们把Python的基础语法和数据结构过了一遍,算是把工具箱里的螺丝刀和扳手认全了。这一章,我们要开始真正干活了。

做量化交易,你每天面对的是什么?是数据。数据从哪来?从文件里来,从API里来。数据脏了怎么办?得处理异常。代码写多了怎么复用?得用面向对象。想给函数加点额外功能又不破坏原有逻辑?装饰器就是干这个的。

说白了,这一章就是让你从「能写代码」进化到「能写能用的代码」。我当年刚入行时,就是卡在这一步很久——代码能跑,但一遇到真实数据就崩。嗯,咱们今天就把这些坑填上。

本章核心脉络:文件操作让你能读写数据 → 异常处理让你程序不崩 → 面向对象让你代码好维护 → 装饰器让你代码更优雅。一条线串下来,你就能写出真正能用的量化工具了。

Python基础速通(二)知识体系 文件读写操作 异常处理机制 面向对象编程 装饰器与闭包 open() / with / 读写模式 CSV / JSON 文件处理 try / except / finally 自定义异常 / 断言 类 / 对象 / 继承 / 多态 封装 / 魔术方法 闭包概念 / 装饰器语法 @语法糖 / 带参装饰器 量化交易实战应用 读取股票CSV数据 异常数据清洗 策略类封装 日志/计时装饰器

4.1 文件读写操作:你的数据仓库

做量化交易,数据就是命根子。你想想看,每天开盘前第一件事是什么?拉数据。收盘后最后一件事是什么?存数据。文件读写,就是你和数据之间的桥梁。

4.1.1 打开文件的正确姿势

Python里打开文件,最经典的方式就是 open() 函数。但我个人习惯,永远用 with 语句。为什么?因为省心。你不用记着关文件,Python自动帮你收尾。

# 不推荐的方式
f = open('stock_data.csv', 'r')
data = f.read()
f.close()  # 万一忘了关,文件句柄就泄漏了

# 推荐的方式
with open('stock_data.csv', 'r') as f:
    data = f.read()
# 出了with块,文件自动关闭

我的小技巧:读大文件时别用 read(),它会一次性把文件全读到内存里。我处理过几百万行的逐笔交易数据,用 read() 直接内存爆了。后来改用 for line in f: 逐行读取,稳得很。

4.1.2 读写模式一览

模式 说明 量化场景
'r' 只读模式 读取历史行情数据
'w' 写入模式(覆盖) 保存回测结果
'a' 追加模式 记录实时交易日志
'r+' 读写模式 更新配置文件

4.1.3 实战:读取股票CSV数据

做量化最常打交道的就是CSV文件。我早期做回测时,每天手动下载CSV,然后用Python解析。这里有个坑——CSV里经常有脏数据,比如空行、乱码、格式不对。

import csv

def load_stock_data(file_path):
    """加载股票CSV数据,返回列表"""
    data = []
    with open(file_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        for row in reader:
            # 过滤空行
            if not row['date']:
                continue
            data.append({
                'date': row['date'],
                'open': float(row['open']),
                'close': float(row['close']),
                'volume': int(row['volume'])
            })
    return data

# 使用示例
stock_data = load_stock_data('sh600519.csv')
print(f"共加载 {len(stock_data)} 条数据")

注意:CSV文件的编码问题是个大坑。我曾经从某数据商拿到的文件是GBK编码,直接用UTF-8读全是乱码。建议先用 chardet 库检测编码,或者直接指定 encoding='gbk' 试试。

4.2 异常处理机制:让你的程序不崩

写代码最怕什么?最怕程序跑着跑着突然崩了。尤其是在实盘交易中,一个异常没处理好,可能就错过了最佳买卖点。异常处理,说白了就是给你的程序穿上防弹衣。

4.2.1 try-except 的基本用法

我刚开始写量化策略时,代码里全是 if...else 判断,生怕哪个数据不对。后来发现,用 try...except 更优雅——你只管写核心逻辑,异常交给专门的处理块。

def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("除数不能为0,返回None")
        result = None
    except TypeError:
        print("类型错误,请检查输入")
        result = None
    else:
        print("计算成功!")
    finally:
        print("无论是否异常,我都会执行")
    return result

# 测试
print(safe_divide(10, 2))   # 正常
print(safe_divide(10, 0))   # 除零异常
print(safe_divide(10, 'a')) # 类型异常

4.2.2 量化中的异常处理实战

在真实交易中,数据源经常出问题。比如某天交易所数据延迟,或者某只股票停牌导致数据缺失。我习惯写一个健壮的数据加载函数:

def robust_data_loader(file_path):
    """健壮的数据加载器"""
    try:
        data = load_stock_data(file_path)
        if not data:
            raise ValueError(f"文件 {file_path} 为空")
        return data
    except FileNotFoundError:
        print(f"文件不存在: {file_path}")
        return []
    except ValueError as e:
        print(f"数据异常: {e}")
        return []
    except Exception as e:
        print(f"未知错误: {e}")
        return []

我的经验:不要滥用 except Exception,它会吞掉所有错误,包括你没想到的bug。我建议先捕获具体的异常类型,最后再用一个 except Exception 兜底,同时把错误信息打印出来方便排查。

4.3 面向对象编程入门:把代码组织起来

写量化策略,一开始你可能只是写几个函数,调来调去。但策略一多,参数一复杂,代码就乱成一锅粥。面向对象编程,说白了就是把相关的数据和操作打包在一起,让代码更清晰、更好复用。

4.3.1 类与对象:模板与实例

类就是模板,对象就是根据模板造出来的具体东西。比如「股票」是一个类,而「贵州茅台」就是一个具体的股票对象。

class Stock:
    """股票类"""
    def __init__(self, code, name, price):
        self.code = code    # 股票代码
        self.name = name    # 股票名称
        self.price = price  # 当前价格
        self.history = []   # 历史价格
    
    def update_price(self, new_price):
        """更新价格并记录历史"""
        self.history.append(self.price)
        self.price = new_price
    
    def get_return(self):
        """计算收益率"""
        if len(self.history) < 1:
            return 0
        return (self.price - self.history[-1]) / self.history[-1]

# 创建对象
maotai = Stock('600519', '贵州茅台', 1800.0)
maotai.update_price(1820.0)
print(f"{maotai.name} 收益率: {maotai.get_return():.2%}")

4.3.2 继承:站在巨人的肩膀上

做量化时,你可能有很多种策略,但它们都有一些共同点。比如都需要初始化、都需要计算信号、都需要执行交易。用继承,你可以把公共部分提取到父类里。

class BaseStrategy:
    """策略基类"""
    def __init__(self, name):
        self.name = name
        self.positions = {}
    
    def generate_signal(self, data):
        """生成交易信号,子类必须实现"""
        raise NotImplementedError("子类必须实现此方法")
    
    def execute(self, data):
        """执行策略"""
        signal = self.generate_signal(data)
        print(f"{self.name} 生成信号: {signal}")
        return signal

class MovingAverageStrategy(BaseStrategy):
    """均线策略"""
    def __init__(self, short_window=5, long_window=20):
        super().__init__("均线策略")
        self.short_window = short_window
        self.long_window = long_window
    
    def generate_signal(self, data):
        # 计算均线并生成信号
        if len(data) < self.long_window:
            return "hold"
        short_ma = sum(data[-self.short_window:]) / self.short_window
        long_ma = sum(data[-self.long_window:]) / self.long_window
        return "buy" if short_ma > long_ma else "sell"

# 使用
ma_strategy = MovingAverageStrategy(5, 20)
ma_strategy.execute([100, 102, 101, 103, 105, 104, 106])

核心思想:面向对象不是炫技,是为了让代码更好维护。我见过有人一个策略写500行函数,后来要加个参数,改得头皮发麻。用类封装后,改一个属性就行。

4.4 装饰器与闭包:给函数加点料

装饰器是Python里比较高级的特性,但说白了,它就是一个「函数包装器」。你有一个函数,想在不改它代码的前提下,给它加点功能——比如打日志、算时间、做权限校验。装饰器就是干这个的。

4.4.1 闭包:函数里套函数

闭包是装饰器的基础。简单说,就是在一个函数内部定义另一个函数,并且内部函数用到了外部函数的变量。

def make_multiplier(n):
    """创建一个乘以n的函数"""
    def multiplier(x):
        return x * n
    return multiplier

# 创建两个闭包
double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))  # 10
print(triple(5))  # 15

4.4.2 装饰器:@语法糖

装饰器本质上就是一个接受函数作为参数、返回新函数的闭包。在量化交易中,我经常用装饰器来记录策略执行时间、打印日志、做参数校验。

import time

def timer_decorator(func):
    """计算函数执行时间的装饰器"""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行耗时: {end - start:.4f}秒")
        return result
    return wrapper

@timer_decorator
def backtest_strategy(data):
    """回测策略"""
    time.sleep(0.5)  # 模拟计算
    return "回测完成"

# 调用
result = backtest_strategy([1, 2, 3])
print(result)

4.4.3 带参数的装饰器

有时候你想让装饰器更灵活,比如指定日志级别。那就再包一层:

def log_decorator(level='INFO'):
    """带参数的日志装饰器"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用函数: {func.__name__}")
            result = func(*args, **kwargs)
            print(f"[{level}] 函数返回: {result}")
            return result
        return wrapper
    return decorator

@log_decorator(level='WARNING')
def calculate_ma(data):
    return sum(data) / len(data)

calculate_ma([100, 102, 101])

避坑指南:使用装饰器时,被装饰函数的元信息(比如 __name____doc__)会被替换成 wrapper 的。解决办法是用 functools.wraps 装饰器。我曾经因为这个bug,调试了半天才发现函数名不对。

from functools import wraps

def my_decorator(func):
    @wraps(func)  # 保留原函数信息
    def wrapper(*args, **kwargs):
        print("装饰器生效")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def hello():
    """打招呼函数"""
    print("Hello!")

print(hello.__name__)  # 输出: hello(而不是wrapper)
print(hello.__doc__)   # 输出: 打招呼函数

好了,这一章的内容就到这。文件读写让你能跟数据打交道,异常处理让你程序更健壮,面向对象让代码更清晰,装饰器让功能扩展更优雅。这四个技能,是写出高质量量化代码的基石。下一章,我们会把这些知识串起来,开始真正搭建一个简单的量化回测框架。


专注资料整理