40.函数(定义、调用、作用域)

什么是函数?

函数(Function)是编程中最基本的抽象机制之一。

  • 数学起源:函数概念源自17世纪莱布尼茨的符号系统
  • 编程意义:函数是代码组织的基本单位,也是人类思维的载体
  • Python中的地位:函数是”一等公民”(First-Class Citizen)

函数的核心价值

函数在软件开发和数据分析中带来五个不可替代的价值:

价值 说明
抽象化 隐藏实现细节,关注”做什么”
模块化 大问题分解为小函数,单一职责
复用性 一次编写,多处调用
可测试性 独立函数更容易进行单元测试
可读性 函数名本身就是最好的文档

函数在商业分析中的应用

在商业数据分析和决策中,函数的重要性尤为突出:

  • 策略封装:业务策略可以作为函数实现,参数化配置
  • 指标计算:KPI、增长率等业务指标适合封装为函数
  • 风险管理:VaR、最大回撤等风险度量可以函数化
  • 自动化报告:整个分析流程就是一系列函数的组合

函数定义的七要素

一个完整的Python函数定义包含以下要素:

  1. def关键字:告诉Python要定义一个函数
  2. 函数名:使用snake_case命名规范
  3. 参数列表:函数的输入,放在圆括号内
  4. 冒号:表示函数头部的结束
  5. 文档字符串(Docstring):三引号包围,说明函数用途
  6. 函数体:缩进的代码块

函数定义的语法结构

Python函数定义语法结构 展示def关键字、函数名、参数、冒号、文档字符串、函数体和return语句的层次结构 def calculate_tax (salary): """计算税后工资""" if salary <= 3000: rate = 0 else: rate = 0.05 return salary - tax ① def关键字 ② 函数名(snake_case) ③ 参数列表 + 冒号 ④ 文档字符串(Docstring) ⑤ 函数体(缩进代码块) ⑥ return语句(返回结果)

示例:定义税后工资计算函数

Listing 1
# 定义函数:计算税后工资
# def关键字 + 函数名(snake_case) + 参数列表
def calculate_tax(salary):
    """
    计算税后工资

    参数:
        salary (float): 税前工资,单位为元
    返回:
        float: 税后工资,单位为元
    计算规则:
        - 不超过3000元:不征税
        - 超过3000元:对超出部分征收5%的税
    """
    # 判断工资是否超过3000元
    if salary <= 3000:
        rate = 0       # 不征税
    else:
        rate = 0.05    # 超出部分征收5%

    # 计算应缴税额和税后工资
    tax = (salary - 3000) * rate
    after_tax = salary - tax

    # 返回税后工资
    return after_tax

# 调用函数:传入5000元工资
result = calculate_tax(5000)
print(f'税后工资: {result:.0f}元')
税后工资: 4900元

函数定义 vs 函数调用

理解两者的区别是掌握函数的关键:

阶段 操作 说明
定义 def calculate_tax(salary): 创建函数,代码不会执行
调用 calculate_tax(5000) 执行函数,传入实际参数
  • 形式参数(parameter)salary,定义时的占位符
  • 实际参数(argument)5000,调用时传入的值
  • 返回值return将结果返回给调用者

⭐ 平台练习:税后工资计算

Listing 2
# 注:该代码块包含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)  # 将税后工资打印输出

参数传递:位置参数 vs 关键字参数

Python提供了两种基本的参数传递方式:

特性 位置参数 关键字参数
传递方式 按顺序传递 按名称传递
顺序要求 必须正确 可以任意
可读性 较差(容易混淆) 较好(明确清晰)
使用场景 参数少且类型不同 参数多或类型相似

默认参数:提高函数易用性

Listing 3
# 定义带有默认参数的函数
def greet2(name, greeting='您好'):
    """
    生成问候语(带默认值)

    参数:
        name (str): 人名
        greeting (str): 问候语,默认为'您好'
    返回:
        str: 完整的问候语
    """
    return f'{greeting}, {name}!'

# 调用方式1:只提供必填参数(使用默认值)
print(greet2('王五'))

# 调用方式2:覆盖默认值
print(greet2('赵六', greeting='早安'))
您好, 王五!
早安, 赵六!

默认参数的设计原则

使用默认参数时需要遵循三个原则:

  • 必填参数在前:没有默认值的参数必须放在前面
  • 默认参数在后:有默认值的参数必须放在后面
  • 使用不可变对象:默认值最好使用数字、字符串、元组

⚠️ 可变默认参数陷阱

Listing 4
# ❌ 危险示例:使用列表作为默认值
def add_item_wrong(item, items=[]):
    items.append(item)
    return items

print('第一次调用:', add_item_wrong('A'))  # ['A']
print('第二次调用:', add_item_wrong('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'] ✅
第一次调用: ['A']
第二次调用: ['A', 'B']
第一次调用: ['A']
第二次调用: ['B']

可变参数:*args 和 **kwargs

Python提供了处理任意数量参数的强大能力:

  • *args:收集所有额外的位置参数,打包成元组
  • **kwargs:收集所有额外的关键字参数,打包成字典
*args和**kwargs参数收集示意图 展示可变参数如何收集位置参数为元组、关键字参数为字典 *args → 元组 summary(1, 2, 3, 4, 5) ↓ 收集为 args = (1, 2, 3, 4, 5) 类型:tuple(元组) **kwargs → 字典 summary(a=1, b=2, c=3) ↓ 收集为 kwargs = {'a':1,'b':2,'c':3} 类型:dict(字典)

可变参数使用示例

Listing 5
def summary(*args, **kwargs):
    """
    演示可变参数的使用

    参数:
        *args: 可变数量的位置参数(收集为元组)
        **kwargs: 可变数量的关键字参数(收集为字典)
    """
    print(f'位置参数: {args}')
    print(f'关键字参数: {kwargs}')

    if args:
        total = sum(args)
        print(f'和: {total}')

    if kwargs:
        print(f'键值对数量: {len(kwargs)}')

# 调用示例1:只传递位置参数
summary(1, 2, 3, 4, 5)
位置参数: (1, 2, 3, 4, 5)
关键字参数: {}
和: 15

可变参数的混合调用

Listing 6
def summary(*args, **kwargs):
    """演示可变参数的混合使用"""
    print(f'位置参数: {args}')
    print(f'关键字参数: {kwargs}')
    if args:
        print(f'和: {sum(args)}')

# 只传递关键字参数
print('--- 关键字参数 ---')
summary(a=1, b=2, c=3)

# 混合传递:位置参数 + 关键字参数
print('--- 混合参数 ---')
summary(1, 2, x=10, y=20)
--- 关键字参数 ---
位置参数: ()
关键字参数: {'a': 1, 'b': 2, 'c': 3}
--- 混合参数 ---
位置参数: (1, 2)
关键字参数: {'x': 10, 'y': 20}
和: 3

变量作用域:LEGB规则

Python使用LEGB规则来查找变量,代表四个层级:

LEGB作用域层级 从内到外展示Local、Enclosing、Global、Built-in四层变量查找顺序 B — Built-in(内置作用域:print, len, range...) G — Global(全局作用域:模块级变量) E — Enclosing(外层函数作用域) L — Local(局部作用域) 函数内部定义的变量 ↑ 变量查找方向:由内向外 ↑

局部变量 vs 全局变量

Listing 7
# 全局变量
global_var = '我是全局变量'

def demo_scope():
    """演示局部作用域"""
    # 局部变量:只在函数内部可见
    local_var = '我是局部变量'
    print(f'函数内部 - 全局变量: {global_var}')
    print(f'函数内部 - 局部变量: {local_var}')

demo_scope()
print(f'函数外部 - 全局变量: {global_var}')
# print(local_var)  # ❌ 报错!局部变量在函数外部不可见
函数内部 - 全局变量: 我是全局变量
函数内部 - 局部变量: 我是局部变量
函数外部 - 全局变量: 我是全局变量

⭐ 平台练习:现值计算函数

Listing 8
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#需要传入的参数,期数以及每期的现金流,单位为元。
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元

现值函数代码解析

上面的PV函数综合运用了多个知识点:

  • *NCF可变参数:接收任意数量的现金流
  • for循环遍历:逐期计算折现值
  • pow()幂函数:计算 \((1+R)^n\)
  • round()四舍五入:保留2位小数
  • 累加求和pv += ... 将各期折现值累加

\[\large PV = \sum_{n=1}^{N} \frac{CF_n}{(1+R)^n}\]

作用域管理的最佳实践

原则 说明
避免全局变量 使用参数传递数据,降低耦合度
局部变量优先 限制变量的作用范围
常量大写命名 全局常量用 UPPER_CASE
慎用global 如必须修改全局变量,明确声明

函数设计最佳实践总结

函数设计三原则:

  • 单一职责:每个函数只做一件事
  • 参数合理:不超过5个位置参数
  • 文档完整:包含参数说明和返回值说明

参数传递选择:

  • 位置参数:用于必需的、类型不同的参数
  • 关键字参数:用于可选的、有默认值的参数
  • 可变参数:用于数量不确定的输入

本章知识回顾

函数章节知识点回顾 总结函数定义、参数传递和作用域三大模块的核心知识点 函数 — 核心知识点回顾 函数定义 • def 关键字 • snake_case 命名 • 文档字符串 docstring • return 返回值 • 形参 vs 实参 • 定义时不执行 参数传递 • 位置参数(必填) • 关键字参数(明确) • 默认参数(便捷) • *args(可变位置) • **kwargs(可变关键字) • ⚠️ 可变默认值陷阱 变量作用域 • L — 局部作用域 • E — 外层函数 • G — 全局作用域 • B — 内置作用域 • 避免全局变量 • 常量用 UPPER_CASE