41 函数 定义、调用、作用域
本章定位(复习与补充训练层):本章对应正课第3章《函数定义 算术平均收益率计算》(章节 3),并与第45章《Python自定义函数》(章节 46)互为补充训练,用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。
41.1 引言函数式编程的革命
函数(Function)是编程中最基本也是最重要的抽象机制:把一段计算封装成可复用、可测试、可读的单元,让策略、指标与风险度量都能以参数化的方式反复调用。本章用两个平台任务检验你对函数定义、参数传递与作用域的复习效果。
41.2 本章学习目标
本章为复习与补充训练,目标是检验你是否能脱离正课独立完成下列任务:
- 能独立写出完整的函数定义:说明
def、参数、文档字符串、return各自的作用,并独立完成税后工资函数calculate_tax(salary)的定义与调用 - 能独立完成平台任务一:读懂用
input()输入基本工资、按阈值判定税率并输出税后工资的脚本,并说出它与函数封装版本的等价关系 - 能独立完成平台任务二:定义现值函数
PV(R, *NCF),用可变参数接收任意期现金流,借助pow()与round()逐期贴现累加并输出 - 能区分位置参数与关键字参数,会为函数设计默认参数,并能说出可变默认参数的陷阱及
None惯用法 - 能按 LEGB 规则判断变量的可见范围,说明函数内重新绑定全局变量时为何需要
global声明
若上述任务无法独立完成,请回到正课第3章对应小节补弱,再回到本章练习。
41.3 函数定义与调用基础
复习语法细节之前,先明确函数为什么值得封装——它在软件工程与金融分析中的价值是后面一切语法设计的出发点:
函数的核心价值
在软件开发和数据分析中,函数带来了五个不可替代的价值:
抽象化(Abstraction): 函数将复杂的实现细节隐藏在简洁的接口之后,使程序员能够关注”做什么”而非”怎么做”
模块化(Modularization): 通过将大问题分解为小函数,每个函数专注于单一任务,遵循单一职责原则(Single Responsibility Principle)
复用性(Reusability): 一次编写,多处调用。这不仅节省了开发时间,更重要的是减少了出错机会
可测试性(Testability): 独立的函数更容易进行单元测试,可以验证其正确性
可读性(Readability): 函数名本身就是最好的文档。
calculate_sharpe_ratio()比一段内联的代码更能传达意图
在金融分析中的特殊意义
在金融数据分析和量化交易中,函数的重要性更加突出:
- 策略封装: 交易策略可以作为函数实现,参数化配置
- 指标计算: 技术指标(如MA、MACD、RSI)适合封装为函数
- 风险管理: VaR、最大回撤等风险度量都可以函数化
- 回测框架: 整个回测流程就是一系列函数的组合
理论背景:函数定义的解剖学
一个完整的Python函数定义包含以下要素:
- def关键字: 告诉Python我们要定义一个函数(define的缩写)
- 函数名: 遵循Python命名规范,使用小写字母和下划线(snake_case)
- 参数列表: 函数的输入,放在圆括号内
- 冒号: 表示函数头部的结束
- 文档字符串(Docstring): 三引号包围的字符串,用于说明函数用途
- 函数体: 缩进的代码块,包含执行的语句
- return语句: 返回函数结果
# =============================================================================
# 示例:定义和调用函数计算税后工资
# =============================================================================
# 本示例演示Python函数的完整定义和调用过程,包括参数、返回值和文档字符串
# ==================== 函数定义部分 ====================
# def关键字用于定义函数
# - def是"define"(定义)的缩写
# - 告诉Python解释器:我们要定义一个新的函数
#
# 函数命名规范:
# - 函数名:calculate_tax
# - 使用小写字母
# - 多个单词之间用下划线连接(snake_case风格)
# - 函数名应该清晰描述函数的功能(这里计算税收)
#
# 参数列表:
# - salary是函数的参数(parameter)
# - 参数放在圆括号()内
# - 参数是函数的输入,类似数学函数f(x)中的x
# - 这里salary表示税前工资,数据类型应该是数字
def calculate_tax(salary):
# ==================== 文档字符串 ====================
# 三引号"""包围的文本是函数的文档字符串(docstring)
# 文档字符串是函数的"使用说明书",应该包含:
# 1. 函数功能的简要描述
# 2. 参数说明(参数名、类型、含义)
# 3. 返回值说明(类型、含义)
# 4. 使用示例或注意事项(可选)
#
# 可以通过help(calculate_tax)命令查看这个文档字符串
"""
计算税后工资
参数:
salary (float): 税前工资,单位为元
返回:
float: 税后工资,单位为元
计算规则:
- 不超过3000元:不征税,税率为0%
- 超过3000元:对超出部分征收5%的税
"""
# ==================== 函数体:核心逻辑 ====================
# 函数体是缩进的代码块,包含函数要执行的所有语句
# Python使用缩进(通常是4个空格)来表示代码块的层次关系
# 第一步:判断工资是否超过3000元
# if语句是条件判断语句
# - if是Python关键字,表示"如果"
# - salary <= 3000是条件表达式
# - <=是比较运算符,表示"小于或等于"
# - 如果条件为真(True),执行冒号后的缩进代码块
if salary <= 3000:
# 工资不超过3000元时的处理
# rate变量存储税率
# =是赋值运算符,将右侧的值赋给左侧的变量
# 0表示0%的税率,即不征税
rate = 0
else:
# else语句与if配对,表示"否则"
# 当if条件为假(False)时,执行else后的代码块
# 工资超过3000元时的处理
# 0.05表示5%的税率(小数形式)
rate = 0.05
# 第二步:计算应缴税额
# 税额 = (工资 - 免税额度) × 税率
# 这里3000是免税额度,只有超出部分才需要征税
# 圆括号用于改变运算优先级,先计算减法,再计算乘法
tax = (salary - 3000) * rate
# 第三步:计算税后工资
# 税后工资 = 税前工资 - 应缴税额
# 这是一个简单的减法运算
after_tax = salary - tax
# 第四步:返回计算结果
# return关键字将结果返回给函数的调用者
# - return后跟要返回的值或表达式
# - 执行return后,函数立即结束,后续代码不会执行
# - 如果没有return语句,函数返回None
# - 这里返回税后工资的计算结果
return after_tax
# ==================== 函数调用部分 ====================
# 函数定义完成后,需要调用才能执行
# 函数调用的语法:函数名(参数值)
# 调用calculate_tax函数
# - calculate_tax是函数名
# - 5000是实际参数(argument),传入给函数
# - 5000表示税前工资为5000元
# - 函数执行后,return语句返回税后工资
# - 返回值赋值给变量result存储
result = calculate_tax(5000)
# 税收计算过程:
# salary = 5000
# 因为5000 > 3000,所以rate = 0.05(5%)
# tax = (5000 - 3000) × 0.05 = 2000 × 0.05 = 100元
# after_tax = 5000 - 100 = 4900元
# result = 4900
# ==================== 输出结果 ====================
# 使用f-string格式化字符串输出结果
# f-string是Python 3.6+引入的字符串格式化方法
# - f表示format(格式化)
# - 花括号{}内可以放入变量或表达式
# - :.0f是格式化说明符
# - :表示格式化选项的开始
# - .0表示保留0位小数
# - f表示浮点数(float)
# - 整体意思是:将result格式化为不显示小数的浮点数
print(f'税后工资: {result:.0f}元')
# 预期输出: 税后工资: 4900元代码深度解析:
函数定义的执行时机:
- 定义函数时,代码不会立即执行
- 只有在调用函数时,函数体内的代码才会运行
参数传递:
# salary是形式参数(formal parameter),占位符 # 5000是实际参数(actual parameter),实际传入的值return语句:
- return将结果返回给调用者
- 执行return后,函数立即结束,后续代码不会执行
- 如果没有return语句,函数返回None
文档字符串的重要性:
- 这是函数的”使用说明书”
- 可以通过
help(calculate_tax)查看 - 专业代码都应该有完整的文档字符串
41.4 参数传递:灵活性与安全性的平衡
Python提供了丰富的参数传递机制,既保证了使用的灵活性,又维持了必要的安全性。
41.4.1 位置参数与关键字参数
任务要求:从键盘输入基本工资(float),按”不超过3000元税率为0、超过3000元的部分按5%“的规则用 if-else 判定扣税率,计算并按 %d 格式输出税后工资。请在教学平台的交互环境中运行,判定以平台为准。
# 注:该代码块包含input()交互输入,需在平台环境运行
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目一
salary = float(input("请输入基本工资:")) # 从键盘上输入基本工资
if salary <= 3000: # 判断基本工资是否小于等于3000
rate = 0 # 基本工资小于等于3000,不扣税,即扣税率为0%
else: # 不满足以上条件时
rate = 0.05 # 基本工资打印300,扣税率为0.05,即5%
# 计算税后工资,基本工资-扣税,扣税额为超过3000部分的乘以扣税率
salary = salary - (salary - 3000) * rate
print("税后工资为:%d" % salary) # 将税后工资打印输出预期输出(判读要点,以平台运行结果为准):该代码块包含 input() 交互输入,须在教学平台环境运行。判读要点:程序提示”请输入基本工资:“并等待输入;输入不超过3000时税率取0,税后工资与输入相同;超过3000时对超出部分按5%扣税。本机以管道输入模拟交互实测(输入 5000 时输出 税后工资为:4900,输入 3000 时输出 税后工资为:3000),输入 5000 的完整输出如下:
请输入基本工资:税后工资为:4900
位置参数 vs 关键字参数:
| 特性 | 位置参数 | 关键字参数 |
|---|---|---|
| 传递方式 | 按顺序传递 | 按名称传递 |
| 顺序要求 | 必须正确 | 可以任意 |
| 可读性 | 较差(容易混淆) | 较好(明确清晰) |
| 使用场景 | 参数少且类型不同 | 参数多或类型相似 |
41.4.2 默认参数:提高函数易用性
# 定义带有默认参数的函数
# greeting参数有默认值'您好',调用时可以省略
def greet2(name, greeting='您好'):
"""
生成问候语(带默认值)
参数:
name (str): 人名
greeting (str): 问候语,默认为'您好'
返回:
str: 完整的问候语
"""
# 格式化并返回问候语
return f'{greeting}, {name}!'
# 调用方式1:只提供必填参数
# greeting使用默认值'您好'
print(greet2('王五'))
# 调用方式2:覆盖默认值
# 显式指定greeting为'早安'
print(greet2('赵六', greeting='早安'))默认参数的设计原则:
- 必填参数在前: 没有默认值的参数必须放在前面
- 默认参数在后: 有默认值的参数必须放在后面
- 不可变对象: 默认值最好使用不可变对象(如数字、字符串、元组)
警告:可变默认参数陷阱:
# 危险示例:使用列表作为默认值
def add_item(item, items=[]): # 危险!
items.append(item)
return items
# 第一次调用
print(add_item('A')) # ['A']
# 第二次调用:问题出现了!
print(add_item('B')) # ['A', 'B'] 而不是 ['B']!
# 正确做法:使用None作为默认值
def add_item_correct(item, items=None):
if items is None:
items = [] # 每次调用都创建新列表
items.append(item)
return items
print(add_item_correct('A')) # ['A']
print(add_item_correct('B')) # ['B'] 符合预期!41.5 可变参数:处理任意数量的输入
**理论背景:*args和kwargs的设计哲学
Python中的*args和**kwargs提供了处理可变参数的强大能力:
*args: 收集所有额外的位置参数,打包成元组**kwargs: 收集所有额外的关键字参数,打包成字典
def summary(*args, **kwargs):
"""
演示可变参数的使用
参数:
*args: 可变数量的位置参数(收集为元组)
**kwargs: 可变数量的关键字参数(收集为字典)
返回:
None: 此函数仅用于演示,不返回值
"""
# 打印所有位置参数
print(f'位置参数: {args}')
# 打印所有关键字参数
print(f'关键字参数: {kwargs}')
# 如果有位置参数,计算它们的和
if args:
total = sum(args) # sum()函数计算元组所有元素的和
print(f'和: {total}')
# 如果有关键字参数,显示数量
if kwargs:
print(f'键值对数量: {len(kwargs)}')
# 调用示例1:只传递位置参数
# 1, 2, 3, 4, 5被收集到args元组中
summary(1, 2, 3, 4, 5)
# 调用示例2:只传递关键字参数
# a=1, b=2, c=3被收集到kwargs字典中
summary(a=1, b=2, c=3)
# 调用示例3:混合传递
# 1, 2被收集到args元组中
# x=10, y=20被收集到kwargs字典中
summary(1, 2, x=10, y=20)可变参数的金融应用:
# 应用1:计算多只股票的加权收益率
def weighted_return(*returns, **weights):
"""
计算加权平均收益率
参数:
*returns: 多个收益率值
**weights: 权重参数,如w1=0.3, w2=0.7
返回:
float: 加权平均收益率
"""
# 检查收益率和权重数量是否匹配
if len(returns) != len(weights):
raise ValueError('收益率和权重数量不匹配')
# 计算加权平均
total = sum(r * w for r, w in zip(returns, weights.values()))
return total
# 使用示例
result = weighted_return(0.05, 0.03, 0.08, w1=0.5, w2=0.3, w3=0.2)
print(f'加权收益率: {result:.2%}')41.6 作用域:变量的可见性与生命周期
理论背景:LEGB规则
Python使用LEGB规则来查找变量,代表四个层级的作用域:
- L(Local): 局部作用域(函数内部)
- E(Enclosing): 嵌套函数的外层函数作用域
- G(Global): 全局作用域(模块级别)
- B(Built-in): 内置作用域(Python内置模块)
任务要求:定义现值函数 PV(R, *NCF)——给定贴现率 R 与任意期现金流,按年间隔逐期折现并累加得到现值、输出结果;随后用两组不同期数的现金流调用该函数,体会 *NCF 可变参数的接收方式。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#需要传入的参数,期数以及每期的现金流,单位为元。
def PV(R,*NCF):
pv=0 #初始化,后续要累加
n=1 #间隔为1年
for cf in NCF: # 遍历NCF中的每个cf
pv+=round(cf/pow((1+R),n),2) #pow()是幂函数
n+=1 # 更新n的值
return print("现值结果是:{:.2f}元".format(pv)) # 返回计算结果
#调用自定义的现值函数PV()
PV(0.05,-10000,8000,12000)
PV(0.05,-20000,-500,2000,10000,16000,30000) # 调用自定义的现值函数,传入利率与各期现金流预期输出(本机实际运行结果,具体以平台运行结果为准):
现值结果是:8098.48元
现值结果是:25376.45元
作用域的金融应用:
# 全局配置:交易系统的参数
MAX_POSITION = 1000000 # 最大持仓金额(元)
RISK_LIMIT = 0.02 # 风险限制(2%)
def calculate_position_size(capital, risk_level):
"""
根据资金和风险水平计算持仓规模
参数:
capital (float): 可用资金
risk_level (float): 风险水平(0-1之间)
返回:
float: 建议持仓金额
"""
# 使用全局变量进行风险控制
if risk_level > RISK_LIMIT:
# 风险过高,降低持仓
position = capital * 0.5
else:
position = capital * 0.8
# 确保不超过最大持仓限制
return min(position, MAX_POSITION)
# 使用示例
position = calculate_position(500000, 0.03)
print(f'建议持仓: {position:,.0f}元')global关键字的使用原则:
- 尽量避免使用: 全局变量会增加代码耦合度
- 明确声明: 如果必须修改全局变量,使用global声明
- 替代方案: 使用类(Class)来封装状态和数据
最佳实践总结:
- 函数设计:
- 单一职责:每个函数只做一件事
- 参数合理:不超过5个位置参数
- 文档完整:包含参数说明和返回值说明
- 参数传递:
- 位置参数:用于必需的、类型不同的参数
- 关键字参数:用于可选的、有默认值的参数
- 默认参数:提高函数易用性,但避免使用可变对象
- 作用域管理:
- 避免全局变量:使用参数传递数据
- 局部变量优先:限制变量的作用范围
- 常量大写:全局常量用大写字母命名
函数是Python编程的核心构建块,掌握函数的高级用法对于编写高质量代码至关重要。在金融分析中,合理的函数设计能够显著提高代码的可读性、可维护性和复用性。
41.7 延伸阅读(选读)
函数概念从早期数学符号系统、λ 演算,到现代语言中”函数作为一等公民”的函数式复兴,演进脉络与分阶段详情见第 章节 46 章的延伸阅读,此处不再重复。
41.8 本章小结
要点:
- 函数定义七要素:
def、函数名、参数列表、冒号、文档字符串、函数体、return;定义时不执行,调用时才执行 - 位置参数按顺序传递,关键字参数按名称传递;默认参数必须放在必填参数之后
*args把多余的实参收集为元组,**kwargs收集为字典;平台任务二的PV(R, *NCF)正是用*NCF接收任意期现金流- 作用域查找遵循 LEGB(Local→Enclosing→Global→Built-in);函数内可直接读取全局变量,重新绑定则需要
global - 好的函数单一职责、参数精炼、文档完整,
return返回结果而不是只print
易错点:
- 没有
return的函数返回None;执行到return后函数立即结束 - 平台任务二写的是
return print(...),函数实际返回的是print的返回值None;自己封装时建议return pv,再在调用处打印 - 可变默认参数陷阱:
def f(item, items=[])的默认列表在多次调用间共享,应改用items=None后在函数内新建 - 必填参数写在默认参数之后会直接报
SyntaxError
41.9 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
概念自测(不看正课):形参与实参的区别是什么?
*args与**kwargs分别把多余参数收集成什么类型?LEGB 的查找顺序是怎样的?参考答案(点开前请先独立完成)
解题思路:形参是函数定义时括号里占位的变量名(如
def f(x, rate=0.05)中的x、rate),实参是调用时实际传入的值(如f(6000, rate=0.1)中的 6000 与 0.1);调用时实参按位置或关键字绑定给形参。*args把多余的位置参数收集成元组,**kwargs把多余的关键字参数收集成字典。LEGB 查找顺序:Local(函数内部)→ Enclosing(外层嵌套函数)→ Global(模块级)→ Built-in(内置),由内向外逐层查找、先找到先用。回扣主线章:函数定义与调用的系统讲解见第 章节 3 章。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
def f(x, rate=0.05): if x <= 3000: t = 0 else: t = (x - 3000) * rate return x - t print(f(5000)) print(f(3000)) print(f(6000, rate=0.1)) def add_item(item, items=[]): items.append(item) return items print(add_item('A')) print(add_item('B'))参考答案(先写下你的预测再点开)
解题思路:
f(5000)未传rate、用默认值 0.05,税额 (5000−3000)×0.05=100,返回 4900.0;f(3000)落在x <= 3000分支,税额 0,返回整数 3000;f(6000, rate=0.1)用关键字参数覆盖默认值,税额 (6000−3000)×0.1=300,返回 5700.0。add_item的默认参数items=[]是可变默认参数陷阱——默认列表在函数定义时只创建一次、所有调用共享:第一次调用后它已是['A'],第二次再追加'B'得['A', 'B'],而不是新建空列表。# 验证脚本:原样运行题面代码 def f(x, rate=0.05): # rate为默认参数 if x <= 3000: # 未超免征额 t = 0 # 税额为0 else: t = (x - 3000) * rate # 超出部分计税 return x - t # 返回税后金额 print(f(5000)) # 用默认税率调用 print(f(3000)) # 恰在免征边界 print(f(6000, rate=0.1)) # 关键字参数覆盖默认税率 def add_item(item, items=[]): # 可变默认参数(陷阱示范) items.append(item) # 向默认列表追加元素 return items # 返回列表 print(add_item('A')) # 第一次调用 print(add_item('B')) # 第二次调用,复用同一默认列表预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
4900.0 3000 5700.0 ['A'] ['A', 'B']回扣主线章:默认参数与可变默认参数陷阱的讲解见第 章节 3 章。
动手验证:编写函数
after_tax(salary, base=3000, rate=0.05)返回税后工资,分别用位置参数、关键字参数两种方式调用并输出;再构造一种会报错的调用(例如缺少必填参数)并解释错误信息。参考答案(点开前请先独立完成)
解题思路:
after_tax(8000, 3000, 0.05)全按位置绑定(8000→salary、3000→base、0.05→rate),after_tax(salary=8000, rate=0.05)按关键字绑定、base用默认值,两者都输出 7750.0(税额 (8000−3000)×0.05=250)。报错调用after_tax()缺少必填的位置参数salary,抛TypeError: after_tax() missing 1 required positional argument: 'salary'——信息明确指出缺的是哪个必填参数。# 验证脚本:两种调用方式对照,以及缺参报错 def after_tax(salary, base=3000, rate=0.05): # salary必填,base与rate有默认值 if salary <= base: return salary # 未超免征额,不扣税 return salary - (salary - base) * rate # 超出部分计税后返回 print(after_tax(8000, 3000, 0.05)) # 位置参数调用:三个实参按顺序绑定 print(after_tax(salary=8000, rate=0.05)) # 关键字参数调用:base用默认值 try: after_tax() # 缺少必填参数salary(预期报错) except TypeError as error_msg: print('错误信息:', error_msg) # 捕获并显示错误信息预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
7750.0 7750.0 错误信息: after_tax() missing 1 required positional argument: 'salary'回扣主线章:位置参数、关键字参数与默认参数的绑定规则见第 章节 3 章。
变式任务(平台任务二同型改造):仿照
PV(R, *NCF)的写法,定义终值函数FV(R, *CF),把各期现金流按复利滚算到最后一期并求和,调用FV(0.05, -10000, 8000, 12000)输出结果。参考答案(点开前请先独立完成)
解题思路:与
PV(R, *NCF)镜像对称——PV 把第 \(n\) 期现金流除以 \((1+R)^n\) 折现到期初,FV 把第 \(n\) 期现金流乘以 \((1+R)^{N-n}\) 滚算到最后一期(\(N\) 为现金流总期数)。调用FV(0.05, -10000, 8000, 12000)时 \(N=3\):\(-10000\times1.05^{2}+8000\times1.05+12000=-11025+8400+12000=9375\),输出 9375.00 元。# 变式程序:终值函数FV,各期现金流滚算到最后一期求和 def FV(R, *CF): # R为利率,*CF收集各期现金流为元组 fv = 0 # 初始化终值累加器 last_period = len(CF) # 最后一期的期数 n = 1 # 当前现金流所在期数 for cf in CF: # 遍历各期现金流 fv += round(cf * pow((1 + R), (last_period - n)), 2) # 第n期现金流复利滚算到最后一期并累加 n += 1 # 期数推进 return print('终值结果是:{:.2f}元'.format(fv)) # 输出终值结果 FV(0.05, -10000, 8000, 12000) # 调用:期初投入1万,随后两期回收8千与1.2万预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
终值结果是:9375.00元注意:以上为本题变式的独立代码;列表 41.5 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。