46 Python自定义函数
本章定位(复习与补充训练层):本章对应正课第3章《函数定义 算术平均收益率计算》(章节 3),并与第40章《函数 定义、调用、作用域》(章节 41)互为补充训练,用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。
46.1 引言:函数在编程中的核心地位
函数(Function)是编程中最基本的抽象机制:把一段计算封装成可复用、可测试、可读的单元,让策略、指标与风险度量都能参数化地反复调用。本章用三个平台任务检验你用 lambda 与 def 定义均值函数、波动率函数并对结果做比较的复习效果。
46.2 本章学习目标
本章为复习与补充训练,目标是检验你是否能脱离正课独立完成下列任务:
- 能独立完成平台任务1:读取利率数据后,用
lambda x: sum(x)/len(x)计算三组利率的均值,并用round(..., 6)输出 - 能独立完成平台任务2:用
def加文档字符串定义波动率函数f_sigma(x),按样本标准差公式(Σ(x-u_mean)²/(n-1))**0.5计算并输出 - 能独立完成平台任务3:用比较表达式对任务1、2得到的均值与波动率做布尔判断并输出
- 能为函数编写完整的文档字符串,区分
return与print的使用场景 - 能解释多返回值本质是元组、变量查找遵循 LEGB 规则,以及 lambda 表达式的适用边界
若上述任务无法独立完成,请回到正课第3章对应小节补弱,再回到本章练习。
46.3 函数的定义与调用基础
Python中定义函数使用def关键字,后跟函数名和参数列表。理解函数定义的各个组成部分对于编写高质量代码至关重要。
动手之前先明确函数为什么值得封装——它在软件工程与金融分析中的价值是后面一切语法设计的出发点:
函数的核心价值
在软件开发和数据分析中,函数带来了五个不可替代的价值:
抽象化(Abstraction):函数将复杂的实现细节隐藏在简洁的接口之后,使程序员能够关注”做什么”而非”怎么做”。这种抽象能力是人类处理复杂性的关键。
模块化(Modularization):通过将大问题分解为小函数,每个函数专注于单一任务,遵循单一职责原则(Single Responsibility Principle)。这不仅降低了认知负担,也使代码更易于理解和维护。
复用性(Reusability):一次编写,多处调用。这不仅节省了开发时间,更重要的是减少了出错机会——修改一处,所有调用点都受益。
可测试性(Testability):独立的函数更容易进行单元测试,可以验证其正确性而不依赖整个系统的上下文。
可读性(Readability):函数名本身就是最好的文档。
calculate_sharpe_ratio()比一段内联的代码更能传达意图。
在金融分析中的特殊意义
在金融数据分析和量化交易中,函数的重要性更加突出:
- 策略封装:交易策略可以作为函数实现,参数化配置
- 指标计算:技术指标(如MA、MACD、RSI)适合封装为函数
- 风险管理:VaR、最大回撤等风险度量都可以函数化
- 回测框架:整个回测流程就是一系列函数的组合
函数定义的解剖学
一个完整的Python函数定义包含以下要素:
平台任务1(平台原始代码)
以下代码与教学平台任务要求完全一致:
任务要求:从指定Excel直链读取Shibor、美元Libor、Hibor三组3个月期利率数据,把各列数据逐个读入列表;用 lambda 定义均值函数 f_mean,分别计算三组利率的均值,并用 round(..., 6) 输出。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
# 从Excel文件读取数据存入data
data = pd.read_excel(r"https://huoran.oss-cn-shenzhen.aliyuncs.com/20220811/xlsx/1557658167503511552.xlsx")
data.columns =["日期","Shibor","Libor","Hibor"] # 定义列表data.columns
Shibor = [] # 定义列表Shibor
for i in data["Shibor"]: # 遍历data["Shibor"]中的每个i
Shibor.append(i) # 将Shibor利率数据添加到列表
Libor = [] # 定义列表Libor
for i in data["Libor"]: # 遍历data["Libor"]中的每个i
Libor.append(i) # 将美元Libor利率数据添加到列表
Hibor = [] # 定义列表Hibor
for i in data["Hibor"]: # 遍历data["Hibor"]中的每个i
Hibor.append(i) # 将港元Hibor利率数据添加到列表
f_mean = lambda x: sum(x)/len(x) #用lambda函数定义并且参数x以列表的数据结构输入
Shibor_mean = f_mean(x=Shibor) #计算Shibor的平均值
Libor_mean = f_mean(x=Libor) #计算美元Libor的平均值
Hibor_mean = f_mean(x=Hibor) #计算美元Hibor的平均值
print("3月11日到29日期间3个月期Shibor平均值",round(Shibor_mean,6)) # 输出3月11日到29日期间3个月期Shibor平均值
print("3月11日到29日期间3个月期美元Libor平均值",round(Libor_mean,6)) # 输出3月11日到29日期间3个月期美元Libor平均值
print("3月11日到29日期间3个月期Hibor平均值",round(Hibor_mean,6)) # 输出3月11日到29日期间3个月期Hibor平均值预期输出(OSS 直链本机实测,具体以平台运行结果为准;利率数值随数据快照更新,尾数可能略有差异):
3月11日到29日期间3个月期Shibor平均值 0.028065
3月11日到29日期间3个月期美元Libor平均值 0.026077
3月11日到29日期间3个月期Hibor平均值 0.017619
平台任务2(平台原始代码)
以下代码与教学平台任务要求完全一致:
注:块内
任务要求:同样读取三组利率数据,用 def 定义带文档字符串的波动率函数 f_sigma(x),按样本标准差公式(平方和的均值分母取 n-1)计算各组利率的波动率,并用 round(..., 6) 输出。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
# 从Excel文件读取数据存入data
data = pd.read_excel(r"https://huoran.oss-cn-shenzhen.aliyuncs.com/20220811/xlsx/1557658167503511552.xlsx")
data.columns =["日期","Shibor","Libor","Hibor"] # 定义列表data.columns
Shibor = [] # 定义列表Shibor
for i in data["Shibor"]: # 遍历data["Shibor"]中的每个i
Shibor.append(i) # 将Shibor利率数据添加到列表
Libor = [] # 定义列表Libor
for i in data["Libor"]: # 遍历data["Libor"]中的每个i
Libor.append(i) # 将美元Libor利率数据添加到列表
Hibor = [] # 定义列表Hibor
for i in data["Hibor"]: # 遍历data["Hibor"]中的每个i
Hibor.append(i) # 将港元Hibor利率数据添加到列表
def f_sigma(x): # 定义函数f_sigma
'''通过Python定义一个计算变量波动率的函数
x:代表变量的样本值,可以用列表的数据结构输入'''
n = len(x) # 获取数据长度
u_mean = sum(x)/n #计算变量样本值的均值
z = [] #生成一个空列表
for t in range(n): # 遍历range(n)中的每个t
z.append((x[t]-u_mean)**2) # 将计算结果累加到z列表
return (sum(z)/(n-1))**0.5 #计算波动率
Shibor_sigma = f_sigma(x=Shibor) #计算Shibor波动率
Libor_sigma = f_sigma(x=Libor) #计算Libor波动率
Hibor_sigma = f_sigma(x=Hibor) #计算Hibor波动率
print("3月11日到29日期间3个月期Shibor波动率",round(Shibor_sigma,6)) # 输出3月11日到29日期间3个月期Shibor波动率
print("3月11日到29日期间3个月期没有Libor波动率",round(Libor_sigma,6)) # 输出3月11日到29日期间3个月期美元Libor波动率
print("3月11日到29日期间3个月期Hibor波动率",round(Hibor_sigma,6)) # 输出3月11日到29日期间3个月期Hibor波动率预期输出(OSS 直链本机实测,具体以平台运行结果为准;利率数值随数据快照更新,尾数可能略有差异。第二行”没有Libor”为平台题面原文):
3月11日到29日期间3个月期Shibor波动率 0.000286
3月11日到29日期间3个月期没有Libor波动率 0.000111
3月11日到29日期间3个月期Hibor波动率 0.000702
平台任务3(平台原始代码)
以下代码与教学平台任务要求完全一致:
任务要求:给定任务1、2得到的六个结果常量(三组利率的均值与波动率),用比较表达式 > 依次判断Shibor、美元Libor、Hibor三者均值之间、波动率之间的大小关系,并逐行输出布尔结果。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
Shibor_mean = 0.028065 # 设置期数/数量为0
Libor_mean = 0.026077 # 设置期数/数量为0
Hibor_mean = 0.017619 # 设置期数/数量为0
Shibor_sigma = 0.000286 # 设置数组长度参数为0
Libor_sigma = 0.000111 # 设置数组长度参数为0
Hibor_sigma = 0.000702 # 设置数组长度参数为0
print(Shibor_mean > Libor_mean) #判断Shibor的平均值是否大于美元Libor的平均值
print(Libor_mean > Hibor_mean) #判断美元Libor的平均值是否大于Hibor的平均值
print(Shibor_sigma > Libor_sigma) #判断Shibor的波动率是否大于Libor的波动率
print(Libor_sigma > Hibor_sigma) #判断美元Libor的波动率是否大于Hibor的波动率
print(Shibor_sigma > Hibor_sigma) #判断Shibor的波动率是否大于Hibor的波动率预期输出(本机实际运行结果,具体以平台运行结果为准):
True
True
True
False
False
def greet(name):
"""
问候函数:向指定名字的人问好
参数:
name (str): 要问候的人名
返回:
str: 包含问候语的字符串
"""
return f'您好, {name}!'
# 调用函数
# 函数名后跟括号,括号内传入实际参数
message = greet('李四')
# 打印返回值
print(message) # 输出: 您好, 李四!
# 可以在表达式中直接使用函数返回值
print(greet('张三')) # 输出: 您好, 张三!函数定义的各个部分详解:
def关键字:告诉Python我们要定义一个函数。这是define的缩写。
函数名:遵循Python命名规范,使用小写字母和下划线(snake_case)。函数名应该:
- 使用动词或动词短语开头:
calculate_,get_,process_ - 描述性要强:
calculate_sharpe_ratio()比calc()好 - 避免与内置函数重名:不要命名为
sum(),max()等
- 使用动词或动词短语开头:
参数列表:函数的输入,放在圆括号内,多个参数用逗号分隔。参数是函数与外界交互的接口。
冒号:表示函数头部的结束,这是Python语法的要求。
文档字符串(Docstring):三引号包围的字符串,用于说明函数的用途、参数和返回值。这是Python代码文档的核心。
函数体:缩进的代码块,包含函数执行的语句。
return语句:返回函数结果。如果没有return语句,函数返回
None。
函数调用的机制
理解函数调用的底层机制对于编写高效代码很重要:
def demonstrate_call_process(x, y):
"""
演示函数调用的完整过程
"""
print(f'接收到参数: x={x}, y={y}')
# 执行函数逻辑
result = x + y
print(f'计算结果: {result}')
# 返回结果
return result
# 函数调用过程分解
# 1. 创建栈帧(Stack Frame):为函数调用分配内存空间
# 2. 参数传递:将实参值绑定到形参名
# 3. 执行函数体:按顺序执行函数内语句
# 4. 返回值:将结果返回给调用者
# 5. 销毁栈帧:释放函数调用占用的内存
a, b = 10, 20
print('准备调用函数...')
# 函数调用:程序跳转到函数体执行
value = demonstrate_call_process(a, b)
print(f'函数返回值: {value}')
print('函数调用结束,继续执行主程序')空函数与占位符
在开发过程中,有时需要先定义函数框架,稍后填充实现。Python提供了pass语句:
def placeholder_function():
"""
这是一个占位函数,稍后实现
使用pass语句避免语法错误
"""
pass # pass是Python的空操作语句
# 调用空函数不会产生任何效果
placeholder_function()
# 另一种常见的占位写法:使用省略号(...)
def another_placeholder():
"""另一个占位函数"""
... # 省略号在Python中也是有效的表达式
another_placeholder() # 不会报错46.4 函数参数:灵活性与安全性的平衡
Python提供了丰富的参数传递机制,既保证了使用的灵活性,又维持了必要的安全性。理解这些机制是掌握Python函数编程的关键。
位置参数(Positional Arguments)
最基础的参数类型,按照定义时的顺序传递:
def calculate_profit(revenue, cost):
"""
计算利润的函数
参数:
revenue (float): 营业收入
cost (float): 营业成本
返回:
float: 利润额
"""
profit = revenue - cost
return profit
# 调用时必须按照定义的顺序传递参数
profit1 = calculate_profit(1000, 600)
print(f'利润1: {profit1}') # 输出: 利润1: 400
# 如果顺序错误,会得到错误结果
profit2 = calculate_profit(600, 1000)
print(f'利润2: {profit2}') # 输出: 利润2: -400 (错误!)关键字参数(Keyword Arguments)
通过参数名传递,不受顺序限制,代码更清晰:
def calculate_profit(revenue, cost):
"""计算利润"""
return revenue - cost
# 使用关键字参数,顺序可以任意
profit1 = calculate_profit(revenue=1000, cost=600)
profit2 = calculate_profit(cost=600, revenue=1000)
print(f'利润1: {profit1}') # 输出: 利润1: 400
print(f'利润2: {profit2}') # 输出: 利润2: 400 (正确!)
# 关键字参数提高了代码的可读性
# 尤其当函数有多个参数时,不容易混淆默认参数(Default Arguments)
为参数提供默认值,使函数调用更加灵活:
def calculate_tax(salary, rate=0.05, insurance_rate=0.02):
"""
计算税后工资
参数:
salary (float): 税前工资
rate (float): 税率,默认5%
insurance_rate (float): 社保扣除比例,默认2%
返回:
float: 税后工资
"""
# 计算税金
tax = salary * rate
# 计算社保扣除
insurance = salary * insurance_rate
# 计算实发工资
net_salary = salary - tax - insurance
return net_salary
# 情况1:只提供必填参数
net1 = calculate_tax(5000)
print(f'税后工资1(默认税率): {net1:.2f}元')
# 情况2:覆盖默认税率
net2 = calculate_tax(5000, rate=0.10)
print(f'税后工资2(10%税率): {net2:.2f}元')
# 情况3:使用关键字参数指定特定参数
net3 = calculate_tax(5000, insurance_rate=0.05)
print(f'税后工资3(5%社保): {net3:.2f}元')
# 情况4:所有参数都指定
net4 = calculate_tax(5000, rate=0.08, insurance_rate=0.03)
print(f'税后工资4(全部指定): {net4:.2f}元')默认参数的陷阱:可变默认参数
这是一个经典的新手陷阱,必须特别注意:
# 错误示例:使用列表作为默认参数
def append_item(item, items=[]): # 危险!
"""向列表添加项目"""
items.append(item)
return items
# 第一次调用
result1 = append_item('A')
print(f'第一次: {result1}') # 输出: ['A']
# 第二次调用
result2 = append_item('B')
print(f'第二次: {result2}') # 输出: ['A', 'B'] 意外!
# 问题:默认列表在函数定义时创建,所有调用共享同一个列表
# 正确做法:使用None作为默认值
def append_item_correct(item, items=None):
"""向列表添加项目(正确版本)"""
if items is None: # 每次调用都创建新列表
items = []
items.append(item)
return items
# 测试正确版本
result3 = append_item_correct('A')
print(f'正确第一次: {result3}') # 输出: ['A']
result4 = append_item_correct('B')
print(f'正确第二次: {result4}') # 输出: ['B'] 符合预期!**可变位置参数(*args)**
当不确定会传入多少位置参数时,使用*args:
def sum_all(*args):
"""
计算所有传入数字的和
参数:
*args: 可变数量的位置参数
返回:
float/int: 所有参数的和
"""
print(f'接收到 {len(args)} 个参数: {args}')
# args是一个元组,包含所有位置参数
total = sum(args)
return total
# 调用示例
result1 = sum_all(1, 2, 3)
print(f'和1: {result1}') # 输出: 和1: 6
result2 = sum_all(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
print(f'和2: {result2}') # 输出: 和2: 55
# 也可以传递列表或元组,使用*解包
numbers = [10, 20, 30, 40]
result3 = sum_all(*numbers) # *将列表解包为独立参数
print(f'和3: {result3}') # 输出: 和3: 100可变关键字参数(kwargs)**
接收任意数量的关键字参数:
def create_portfolio(**kwargs):
"""
创建投资组合
参数:
**kwargs: 股票代码和持仓数量的键值对
返回:
dict: 投资组合字典
"""
print('构建投资组合...')
# kwargs是一个字典,包含所有关键字参数
for symbol, shares in kwargs.items():
print(f' {symbol}: {shares}股')
return kwargs
# 调用示例
portfolio1 = create_portfolio(AAPL=100, MSFT=50, GOOGL=30)
print(f'组合1: {portfolio1}')
# 也可以传递字典,使用**解包
stocks = {'600519.SH': 500, '000858.SZ': 1000}
portfolio2 = create_portfolio(**stocks) # **将字典解包为关键字参数
print(f'组合2: {portfolio2}')参数组合规则
当同时使用多种参数类型时,必须遵循以下顺序:
def complex_function(a, b, c=10, *args, **kwargs):
"""
演示各种参数的组合使用
参数顺序规则:
1. 位置参数 (a, b)
2. 默认参数 (c=10)
3. 可变位置参数 (*args)
4. 可变关键字参数 (**kwargs)
"""
print(f'a={a}, b={b}, c={c}')
print(f'args: {args}')
print(f'kwargs: {kwargs}')
# 调用示例
complex_function(1, 2, 3, 4, 5, x=100, y=200)
# 输出:
# a=1, b=2, c=3
# args: (4, 5)
# kwargs: {'x': 100, 'y': 200}46.5 函数返回值:多返回值与返回语义
Python函数的一个强大特性是能够返回多个值,这是通过元组打包实现的。
单一返回值
最简单的情况,函数返回一个值:
def calculate_return(initial, final):
"""
计算简单收益率
参数:
initial (float): 初始价格
final (float): 最终价格
返回:
float: 收益率(小数形式)
"""
return (final - initial) / initial
# 使用示例
initial_price = 100
final_price = 115
return_rate = calculate_return(initial_price, final_price)
print(f'收益率: {return_rate:.2%}') # 输出: 收益率: 15.00%多返回值
Python函数可以返回多个值,实际上是返回一个元组:
def calculate_return_metrics(prices):
"""
计算收益率、最大回撤和波动率
参数:
prices (list): 价格序列
返回:
tuple: (收益率, 最大回撤, 波动率)
其中最大回撤为负数(如-2.86%表示最深回撤2.86%)
"""
# 计算收益率
initial_price = prices[0]
final_price = prices[-1]
return_rate = (final_price - initial_price) / initial_price
# 计算最大回撤:任一时点相对此前历史最高价的跌幅
import numpy as np # 导入NumPy用于向量化计算
prices_arr = np.array(prices) # 将价格列表转为NumPy数组
peak = np.maximum.accumulate(prices_arr) # 逐时点滚动更新历史最高价
drawdown = (prices_arr - peak) / peak # 各时点相对历史高点的回撤(非正数)
max_drawdown = drawdown.min() # 取最深的回撤作为最大回撤(负数)
# 计算波动率(简化版)
import statistics
volatility = statistics.stdev(prices) / statistics.mean(prices)
# 返回多个值,实际是返回一个元组
return return_rate, max_drawdown, volatility
# 调用函数
prices = [100, 105, 102, 108, 112, 110, 115]
# 方式1:接收整个元组
metrics = calculate_return_metrics(prices)
print(f'所有指标(元组): {metrics}')
# 方式2:解包到多个变量
ret, dd, vol = calculate_return_metrics(prices)
print(f'收益率: {ret:.2%}') # 输出: 收益率: 15.00%
print(f'最大回撤: {dd:.2%}') # 输出: 最大回撤: -2.86%(峰值为105时跌至102)
print(f'波动率: {vol:.2%}') # 输出: 波动率: 5.04%
# 方式3:只接收部分值,使用_忽略不需要的
ret_only, _, _ = calculate_return_metrics(prices)
print(f'只要收益率: {ret_only:.2%}')提前返回(Early Return)
函数可以在多个地方返回,这通常用于处理边界条件:
def calculate_yearly_return(prices):
"""
计算年化收益率,包含错误处理
参数:
prices (list): 价格序列
返回:
float: 年化收益率,如果出错返回None
"""
# 边界条件1:数据不足
if len(prices) < 2:
print('错误:至少需要2个价格点')
return None # 提前返回
# 边界条件2:价格为0或负数
if prices[0] <= 0:
print('错误:初始价格必须为正数')
return None # 提前返回
# 正常逻辑
initial = prices[0]
final = prices[-1]
# 计算年化收益率(假设252个交易日)
trading_days = len(prices)
yearly_return = (final / initial) ** (252 / trading_days) - 1
return yearly_return # 正常返回
# 测试用例
test1 = calculate_yearly_return([100, 105, 110])
print(f'测试1(正常): {test1:.2%}' if test1 else test1)
test2 = calculate_yearly_return([100]) # 数据不足
print(f'测试2(数据不足): {test2}')
test3 = calculate_yearly_return([0, 100]) # 初始价格为0
print(f'测试3(价格为0): {test3}')无返回值与None
如果函数没有return语句或return后没有值,函数返回None:
def log_message(message):
"""记录日志消息"""
print(f'[LOG] {message}')
# 没有return语句,返回None
result = log_message('系统启动')
print(f'返回值: {result}') # 输出: 返回值: None
print(f'返回值类型: {type(result)}') # 输出: <class 'NoneType'>
def explicit_none():
"""显式返回None"""
print('执行完毕')
return None # 显式返回None
result2 = explicit_none()
print(f'返回值2: {result2}') # 输出: 返回值2: None46.6 递归函数:自我调用的艺术
以下函数进阶内容(含递归、作用域LEGB规则、lambda 表达式与 map/filter/reduce 等函数式工具)为进阶内容(选读),不影响本章平台任务的完成。时间紧张时可先完成文末的平台任务,再回头选读本部分。
递归(Recursion)是函数式编程的核心概念之一。一个函数调用自身,称为递归。递归在处理具有自相似结构的问题时特别有效。
递归的基本要素
一个正确的递归函数必须包含两个要素:
- 基准情况(Base Case):递归终止的条件
- 递归情况(Recursive Case):将问题分解为更小的子问题
def factorial(n):
"""
计算阶乘:n! = n × (n-1) × ... × 2 × 1
参数:
n (int): 非负整数
返回:
int: n的阶乘
递归关系:
- 0! = 1 (基准情况)
- n! = n × (n-1)! (递归情况)
"""
# 基准情况:递归的终止条件
if n <= 1:
return 1
# 递归情况:将问题分解
# n! = n × (n-1)!
return n * factorial(n - 1)
# 使用示例
print(f'0! = {factorial(0)}') # 输出: 0! = 1
print(f'5! = {factorial(5)}') # 输出: 5! = 120
print(f'10! = {factorial(10)}') # 输出: 10! = 3628800
# 递归执行过程演示(以factorial(3)为例)
# factorial(3)
# → 3 * factorial(2)
# → 2 * factorial(1)
# → 1 (基准情况)
# = 2 * 1 = 2
# = 3 * 2 = 6递归在金融中的应用:折现现金流
递归在金融估值中有重要应用,特别是处理多期现金流:
def present_value(cash_flows, discount_rate, period=0):
"""
使用递归计算现金流的现值
参数:
cash_flows (list): 现金流列表
discount_rate (float): 折现率
period (int): 当前期数(内部使用)
返回:
float: 所有现金流的现值
递归关系:
PV = CF₀ + CF₁/(1+r) + CF₂/(1+r)² + ...
"""
# 基准情况:没有更多现金流
if period >= len(cash_flows):
return 0.0
# 获取当前期现金流
cf = cash_flows[period]
# 计算当前现金流的现值
discount_factor = (1 + discount_rate) ** period
pv = cf / discount_factor
# 递归计算剩余现金流的现值
remaining_pv = present_value(cash_flows, discount_rate, period + 1)
# 返回总现值
return pv + remaining_pv
# 示例:3年期债券
cash_flows = [50, 50, 1050] # 前两年每年50利息,第三年本金+利息
rate = 0.05 # 5%折现率
pv = present_value(cash_flows, rate)
print(f'现值: {pv:.2f}元')
# 递归调用过程:
# present_value([50, 50, 1050], 0.05, 0)
# → 50/(1.05)⁰ + present_value([50, 50, 1050], 0.05, 1)
# → 50/(1.05)¹ + present_value([50, 50, 1050], 0.05, 2)
# → 1050/(1.05)² + present_value([50, 50, 1050], 0.05, 3)
# → 0 (基准情况)
# = 1000.0递归的优缺点
递归的优点:
- 代码简洁,符合数学定义
- 处理树形、图形结构自然
- 某些算法(如快速排序、归并排序)本质上是递归的
递归的缺点:
- 每次调用都有栈开销
- 可能导致栈溢出(Stack Overflow)
- Python有递归深度限制(默认1000)
import sys
# 查看默认递归深度限制
print(f'递归深度限制: {sys.getrecursionlimit()}')
# 尝试超过限制会导致错误
def deep_recursion(n):
"""深度递归演示"""
if n <= 0:
return 0
return n + deep_recursion(n - 1)
# 这会导致 RecursionError
try:
result = deep_recursion(2000)
except RecursionError as e:
print(f'错误:递归深度超限 - {e}')
# 解决方法:使用循环代替递归
def iterative_sum(n):
"""循环版本,无深度限制"""
total = 0
for i in range(n + 1):
total += i
return total
result = iterative_sum(2000)
print(f'循环版本结果: {result}')46.7 变量作用域:LEGB规则
理解变量作用域(Scope)是掌握Python函数编程的关键。Python使用LEGB规则来查找变量:
LEGB规则详解
LEGB代表变量查找的四个层级:
- L(Local):局部作用域(函数内部)
- E(Enclosing):嵌套函数的外层函数作用域
- G(Global):全局作用域(模块级别)
- B(Built-in):内置作用域(Python内置模块)
# 全局变量(G层)
x = 10 # 全局变量x
y = 100 # 全局变量y
def outer_function():
"""外层函数(E层)"""
# 外层函数的局部变量
y = 20 # 遮蔽全局变量y
def inner_function():
"""内层函数(L层)"""
# 内层函数的局部变量
z = 5 # 局部变量z
# 访问各层变量
print(f'内层局部变量 z: {z}')
print(f'外层变量 y: {y}')
print(f'全局变量 x: {x}')
# 调用内层函数
inner_function()
# 调用外层函数
outer_function()
# 在全局作用域访问x和y
print(f'全局变量 x: {x}')
print(f'全局变量 y: {y}')
# 尝试访问局部变量会报错
try:
print(z)
except NameError as e:
print(f'错误:局部变量在外部不可访问 - {e}')global关键字
如果需要在函数内部修改全局变量,使用global关键字:
# 全局变量
counter = 0
total_sum = 0
def increment_counter():
"""增加计数器"""
global counter # 声明使用全局变量
counter += 1 # 修改全局变量
print(f'计数器: {counter}')
def add_to_total(value):
"""累加到总和"""
global total_sum # 声明使用全局变量
total_sum += value
print(f'累加后总和: {total_sum}')
# 测试
print(f'初始counter: {counter}')
increment_counter() # counter变为1
increment_counter() # counter变为2
print(f'最终counter: {counter}')
print(f'初始total_sum: {total_sum}')
add_to_total(100) # total_sum变为100
add_to_total(200) # total_sum变为300
print(f'最终total_sum: {total_sum}')nonlocal关键字
对于嵌套函数,使用nonlocal修改外层函数的变量:
def make_accumulator():
"""累加器工厂函数"""
# 外层函数的局部变量
total = 0
def accumulator(value):
"""内层累加函数"""
nonlocal total # 声明使用外层变量
total += value
return total
return accumulator # 返回内层函数
# 创建累加器
acc1 = make_accumulator()
acc2 = make_accumulator()
# 每个累加器有独立的状态
print(f'acc1累加10: {acc1(10)}') # 输出: 10
print(f'acc1累加20: {acc1(20)}') # 输出: 30
print(f'acc2累加100: {acc2(100)}') # 输出: 100 (独立的total)46.8 lambda表达式:匿名函数的优雅
lambda表达式是Python中创建匿名函数的简洁方式。它来源于λ演算(Lambda Calculus),是函数式编程的核心概念。
lambda的基本语法
# lambda语法:lambda 参数列表: 表达式
# 特点:单个表达式,自动返回结果
# 示例1:简单平方函数
square = lambda x: x**2
print(f'5的平方: {square(5)}') # 输出: 5的平方: 25
# 示例2:等价于以下普通函数
def square_normal(x):
return x**2
print(f'6的平方: {square_normal(6)}') # 输出: 6的平方: 36
# 示例3:多参数lambda
add = lambda x, y: x + y
print(f'3 + 5 = {add(3, 5)}') # 输出: 3 + 5 = 8
# 示例4:lambda在金融中的应用 - 计算投资净值
net_value = lambda prices, shares: sum(p * s for p, s in zip(prices, shares))
prices = [10.5, 20.3, 15.8]
shares = [100, 200, 150]
value = net_value(prices, shares)
print(f'投资净值: {value:.2f}元')lambda的典型应用场景
lambda最适合用于简短的一次性函数,特别是作为高阶函数的参数:
# 场景1:与map()结合 - 对序列中每个元素应用函数
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(f'平方列表: {squared}') # 输出: [1, 4, 9, 16, 25]
# 场景2:与filter()结合 - 筛选符合条件的元素
stocks = [
('AAPL', 150),
('MSFT', 280),
('GOOGL', 2700)
]
# 筛选价格大于200的股票
expensive = list(filter(lambda s: s[1] > 200, stocks))
print(f'高价股票: {expensive}') # 输出: [('MSFT', 280), ('GOOGL', 2700)]
# 场景3:与sorted()结合 - 自定义排序
portfolio = [
{'symbol': 'AAPL', 'shares': 100},
{'symbol': 'MSFT', 'shares': 50},
{'symbol': 'GOOGL', 'shares': 200}
]
# 按持股数量排序
sorted_by_shares = sorted(portfolio, key=lambda x: x['shares'])
print(f'按持股排序:')
for item in sorted_by_shares:
print(f" {item['symbol']}: {item['shares']}股")
# 场景4:与max()和min()结合 - 基于复杂条件找极值
# 找出收益率最高的股票
stock_returns = [('AAPL', 0.15), ('MSFT', 0.20), ('GOOGL', 0.12)]
best_stock = max(stock_returns, key=lambda x: x[1])
print(f'最佳股票: {best_stock[0]}, 收益率: {best_stock[1]:.2%}')lambda的局限性
lambda虽然简洁,但也有明显限制:
- 只能包含单个表达式
- 不能包含语句(如if、for、while)
- 不适合复杂逻辑
- 缺少文档字符串
对于复杂逻辑,普通函数更清晰:
# 不好的lambda示例:过于复杂
# calculate_tax_bad = lambda salary: salary * 0.05 if salary <= 5000 else salary * 0.10
# 更好的做法:使用普通函数
def calculate_tax(salary):
"""
计算个人所得税
参数:
salary (float): 税前工资
返回:
float: 应缴税额
"""
if salary <= 5000:
return salary * 0.05
else:
return salary * 0.10
# 使用
print(f'税额(3000): {calculate_tax(3000):.2f}元')
print(f'税额(8000): {calculate_tax(8000):.2f}元')46.9 函数式编程工具:map、filter和reduce
Python提供了一些内置函数,支持函数式编程风格。这些函数接收其他函数作为参数,称为高阶函数(Higher-Order Functions)。
map():对每个元素应用函数
def calculate_return(prices):
"""计算价格序列的收益率"""
returns = []
for i in range(1, len(prices)):
ret = (prices[i] - prices[i-1]) / prices[i-1]
returns.append(ret)
return returns
# 使用map处理多个股票
stocks = {
'AAPL': [150, 152, 148, 155],
'MSFT': [280, 285, 282, 290],
'GOOGL': [2700, 2720, 2680, 2750]
}
# 对每只股票应用收益率计算
returns_dict = dict(map(
lambda item: (item[0], calculate_return(item[1])),
stocks.items()
))
print('各股票收益率:')
for symbol, returns in returns_dict.items():
print(f'{symbol}: {[f"{r:.2%}" for r in returns]}')filter():筛选符合条件的元素
stocks = [
{'symbol': 'AAPL', 'price': 150, 'pe': 25},
{'symbol': 'MSFT', 'price': 280, 'pe': 30},
{'symbol': 'GOOGL', 'price': 2700, 'pe': 20},
{'symbol': 'TSLA', 'price': 800, 'pe': 100}
]
# 筛选PE < 30的股票
value_stocks = list(filter(lambda s: s['pe'] < 30, stocks))
print('价值股票(PE < 30):')
for stock in value_stocks:
print(f" {stock['symbol']}: PE={stock['pe']}")reduce():累积计算
from functools import reduce
def calculate_compound_return(returns):
"""
计算复合收益率
参数:
returns (list): 期收益率列表
返回:
float: 复合收益率
"""
# 复合收益率 = (1+r₁) × (1+r₂) × ... × (1+rₙ) - 1
# 使用reduce累积计算
compound = reduce(
lambda acc, r: acc * (1 + r), # 累积乘法
returns,
1.0 # 初始值
)
return compound - 1
# 示例:4个季度的收益率
quarterly_returns = [0.05, 0.03, 0.08, 0.02]
annual_return = calculate_compound_return(quarterly_returns)
print(f'年复合收益率: {annual_return:.2%}')46.10 函数文档:编写自文档化代码
良好的函数文档是专业代码的标志。Python使用文档字符串(Docstring)作为函数文档的标准方式。
Google风格文档字符串
def calculate_sharpe_ratio(returns, risk_free_rate=0.03):
"""
计算夏普比率(Sharpe Ratio)
夏普比率是衡量风险调整后收益的指标,计算公式为:
Sharpe Ratio = (E[R] - Rf) / σ(R)
其中E[R]是预期收益,Rf是无风险利率,σ(R)是收益标准差
参数:
returns (list[float]): 资产收益率序列,应为小数形式(如0.05表示5%)
risk_free_rate (float, optional): 无风险利率,默认0.03(3%)
返回:
float: 夏普比率。值越大表示单位风险的超额收益越高
异常:
ValueError: 如果returns为空或包含无效值
示例:
>>> returns = [0.05, 0.03, 0.08, -0.02, 0.06]
>>> sharpe = calculate_sharpe_ratio(returns)
>>> print(f'夏普比率: {sharpe:.2f}')
注意:
- 收益率应为小数形式,不是百分比
- 至少需要2个数据点
- 如果收益标准差为0,返回None
作者:
Claude Code
创建日期: 2024-01-15
"""
import statistics
# 验证输入
if not returns or len(returns) < 2:
raise ValueError('收益率序列至少需要2个数据点')
# 计算平均收益
avg_return = statistics.mean(returns)
# 计算收益标准差
std_return = statistics.stdev(returns)
# 如果标准差为0,返回None(避免除零错误)
if std_return == 0:
return None
# 计算夏普比率
sharpe = (avg_return - risk_free_rate) / std_return
return sharpe
# 使用示例
test_returns = [0.05, 0.03, 0.08, -0.02, 0.06]
try:
sharpe_ratio = calculate_sharpe_ratio(test_returns)
print(f'夏普比率: {sharpe_ratio:.2f}')
except ValueError as e:
print(f'错误: {e}')46.11 延伸阅读(选读)
历史背景与理论发展
函数的概念起源于数学,最早可以追溯到17世纪莱布尼茨(Gottfried Leibniz)的符号系统。在20世纪30年代,数学家阿隆佐·邱奇(Alonzo Church)提出了λ演算(Lambda Calculus),这是所有函数式编程语言的理论基础。Python中的lambda表达式正是对这一数学传统的致敬。
在编程语言发展史上,函数经历了从简单的子程序(Subroutine)到高阶函数(Higher-Order Function)的演进:
- 早期阶段(1950s-1960s):Fortran的函数主要用于数学计算
- 结构化编程(1970s):Pascal等语言强调函数的结构化作用
- 面向对象(1980s-1990s):函数成为对象的方法
- 函数式复兴(2000s-至今):函数作为一等公民(First-Class Citizen)
Python的函数设计融合了多种编程范式的优点,既支持传统的命令式编程,又支持函数式编程特性。
46.12 本章小结
要点:
- 函数定义包含
def、函数名、参数、文档字符串、函数体与return;定义时不执行,调用时才执行 - 平台任务1用 lambda 定义单行均值函数
sum(x)/len(x);平台任务2用def定义多行波动率函数,样本标准差的分母是n-1 - 参数体系:位置参数、关键字参数、默认参数、可变参数
*args与**kwargs - 多返回值返回的是元组,可用多个变量一次解包
- lambda 只能写单个表达式,适合简单转换与作为高阶函数的参数;复杂逻辑用
def;文档字符串是函数的”使用说明书”,可用help()查看 - 作用域与设计规范:变量查找遵循 LEGB 层次,避免滥用全局变量;函数设计遵循单一职责、动词命名、参数精简,优先写成无副作用的纯函数
- 金融计算函数要留意浮点数值稳定性与空数据、零值等边界条件,并提供有意义的错误信息;大数据集优先用 NumPy/Pandas 向量化替代逐元素循环
易错点:
- 样本波动率的分母是
n-1(平台任务2),总体标准差才是n,两者不可混用 - 函数内
print不等于return:只打印不返回值,调用方拿到的结果是None - 平台任务3直接把任务1、2的结果写成字面量参与比较,源数据更新后这些常数必须同步重算
- 可变默认参数(如
items=[])在多次调用间共享同一列表,应默认None后在函数内新建 - lambda 中不能出现赋值等语句,强行写入会报语法错误
46.13 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
概念自测(不看正课):
def与 lambda 各适合什么场景?函数返回多个值时返回的到底是什么类型?文档字符串的作用是什么、用什么命令查看?参考答案(点开前请先独立完成)
解题思路:
def适合有函数名、多语句、需要文档字符串或复用性高的正式函数;lambda 只适合“一行表达式”的匿名小函数(如f_mean = lambda x: sum(x)/len(x)),常配合sorted(key=...)、map()、filter()使用,不能写语句、不能换行。return a, b, c返回的其实是一个元组——多值返回是“打包成元组返回+调用处解包”,type验证为tuple。文档字符串写在函数体首行三引号内,说明参数、返回值与用途,是函数的“在线说明书”,用help(函数名)查看(或函数名.__doc__)。回扣主线章:函数定义、返回值与文档字符串的系统讲解见第 章节 3 章。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
f_mean = lambda x: sum(x) / len(x) def f_sigma(x): n = len(x) u = f_mean(x) z = [(t - u) ** 2 for t in x] return (sum(z) / (n - 1)) ** 0.5 r = [0.02, 0.03, 0.01] print(round(f_mean(r), 4)) print(round(f_sigma(r), 4)) print(f_sigma(r) > 0.01)参考答案(先写下你的预测再点开)
解题思路:
f_mean(r) = (0.02+0.03+0.01)/3 = 0.02,round(..., 4)输出0.02;f_sigma(r)按样本标准差计算——均值u=0.02,离差平方为[0, 0.0001, 0.0001],平方和 0.0002 除以n-1=2得 0.0001,开方得 0.01,round(..., 4)输出0.01;最后f_sigma(r) > 0.01:开方结果按二进制浮点存储时略小于等于 0.01,比较返回False——浮点数慎用==、>直接比较边界值的又一例证。# 验证脚本:原样运行题面代码 f_mean = lambda x: sum(x) / len(x) # 用lambda定义均值函数 def f_sigma(x): # 用def定义样本标准差函数 n = len(x) # 样本量 u = f_mean(x) # 均值 z = [(t - u) ** 2 for t in x] # 各观测的离差平方 return (sum(z) / (n - 1)) ** 0.5 # 平方和除以n-1后开方 r = [0.02, 0.03, 0.01] # 三期收益率序列 print(round(f_mean(r), 4)) # 均值保留四位小数 print(round(f_sigma(r), 4)) # 样本标准差保留四位小数 print(f_sigma(r) > 0.01) # 与0.01的边界比较预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
0.02 0.01 False回扣主线章:lambda 定义均值函数与浮点比较的讲解见第 章节 3 章。
动手验证:把
f_sigma中的分母n - 1改成n,对比两次输出差异,并说明哪一版是样本标准差、哪一版是总体标准差。参考答案(点开前请先独立完成)
解题思路:分母
n - 1的一版是样本标准差(\(s=\sqrt{\frac{\sum(t-\bar{r})^2}{n-1}}\),用样本均值代替总体均值损失了一个自由度,除以 \(n-1\) 予以校正,即贝塞尔校正);分母n的一版是总体标准差(假定数据就是全体)。对r = [0.02, 0.03, 0.01]:样本版为 0.01,总体版为 \(\sqrt{0.0002/3}\approx0.008165\)——分母更大、结果更小,样本量越小差异越明显。# 验证脚本:分母n-1与n两版标准差对照 f_mean = lambda x: sum(x) / len(x) # 均值函数 def f_sigma_sample(x): # 分母n-1:样本标准差 n = len(x) # 样本量 u = f_mean(x) # 均值 z = [(t - u) ** 2 for t in x] # 离差平方列表 return (sum(z) / (n - 1)) ** 0.5 # 除以n-1后开方 def f_sigma_population(x): # 分母n:总体标准差 n = len(x) # 观测个数 u = f_mean(x) # 均值 z = [(t - u) ** 2 for t in x] # 离差平方列表 return (sum(z) / n) ** 0.5 # 除以n后开方 r = [0.02, 0.03, 0.01] # 三期收益率序列 print('样本标准差(n-1):', round(f_sigma_sample(r), 6)) # 样本版 print('总体标准差(n):', round(f_sigma_population(r), 6)) # 总体版预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
样本标准差(n-1): 0.01 总体标准差(n): 0.008165回扣主线章:样本波动率函数的同型写法见第 章节 3 章与本章平台任务2。
变式任务(平台任务1同型改造):仿照任务1,用 lambda 定义极差函数
f_range = lambda x: max(x) - min(x),对三组自选的利率或收益率序列分别计算极差,并用round(..., 6)输出。参考答案(点开前请先独立完成)
解题思路:与平台任务1同源——同一 OSS 直链里的 3 个月期 Shibor、美元 Libor、Hibor 三组利率序列,均值函数换成极差函数
f_range = lambda x: max(x) - min(x)。极差只由序列的最大值与最小值决定,反映区间内利率波动的最大幅度;三组数据分别为 15 个交易日观测(2019-03-11 至 2019-03-29)。# 变式程序:三组3个月期利率的极差计算 import pandas as pd # 导入pandas用于读取Excel data = pd.read_excel(r'https://huoran.oss-cn-shenzhen.aliyuncs.com/20220811/xlsx/1557658167503511552.xlsx') # 读取平台OSS直链的利率数据 data.columns = ['日期', 'Shibor', 'Libor', 'Hibor'] # 重命名列便于按简称引用 f_range = lambda x: max(x) - min(x) # 用lambda定义极差函数 print('3个月期Shibor极差:', round(f_range(list(data['Shibor'])), 6)) # 输出Shibor极差 print('3个月期美元Libor极差:', round(f_range(list(data['Libor'])), 6)) # 输出美元Libor极差 print('3个月期Hibor极差:', round(f_range(list(data['Hibor'])), 6)) # 输出Hibor极差预期输出(OSS 直链 2026-08-27 本机 peter 环境实际拉取运行结果,具体以平台运行结果为准;该直链为公开镜像,若失效请改在教学平台读取同一数据文件):
3个月期Shibor极差: 0.000815 3个月期美元Libor极差: 0.000408 3个月期Hibor极差: 0.002104注意:以上为本题变式的独立代码;列表 46.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣主线章:同型 lambda 均值函数与
round(..., 6)输出格式见第 章节 3 章与本章平台任务1。