第二章:函数
目录 (13)
在数据处理与日常业务核算中,我们经常遇到重复出现的计算规则。例如,无论购买什么商品、买多少件,计算商品总价都遵循“单价 × 数量”;核算企业营业利润,都遵循“营业收入 - 营业成本”。
在初等数学中,我们用函数来描述这种对应规律:
其中 x 是自变量(输入数据),f 是对应法则(处理规则),y 是因变量(计算结果)。如果输入有多个,例如经济学中的生产函数 Q = f(K, L),资本 K 和劳动 L 就是两个自变量,产出 Q 则是结果。
在计算机与信息处理中,这种结构通常抽象为一个系统(System):
在 Python 中,**函数(Function)**就是这种系统思维的实现工具:
- 参数(Parameters):对应系统的输入(自变量);
- 函数体(Function Body):对应系统的处理器(计算逻辑与步骤);
- 返回值(Return Value):对应系统的输出(计算结果)。
本章核心主线介绍函数的系统模型、定义与调用、参数与返回值、冒号与代码块机制,以及局部变量的作用域。扩展阅读介绍关键字参数与默认参数、多值返回、文档字符串和外部变量。
第一部分:核心主线
2.1 从重复计算到系统化封装
在编写程序时,如果相同的规则在多处重复手写,代码会变得冗长且难以维护:
案例:计算同一件单价 28.5 元的商品在购买 4 件与 3 件时的小计金额。
unit_price = 28.5
print(unit_price * 4)
print(unit_price * 3)
输出:
114.0
85.5
两次计算中,“单价 × 数量”这一计算规则完全相同,只有数量在变化。我们可以把这个处理规则封装为一个函数:让单价和数量作为输入,让函数完成乘法处理,并把计算结果作为输出交还给我们。
2.2 定义与调用函数
2.2.1 定义函数的基本语法
定义一个函数需要使用关键字 def(define 的缩写),其基本书写格式如下:
def 函数名(参数1, 参数2):
处理语句1
处理语句2
return 结果
这里包含三个关键语法要素:
- 函数名:遵循第一章变量命名规则(小写字母加下划线,清晰表达功能);
- 圆括号与参数:括号内列出用于接收输入数据的参数名;即使函数不需要输入,圆括号也必须保留;
- 冒号
:与代码块:行末必须写一个英文冒号,用于引出下方的函数体。
2.2.2 核心机制:代码块(Code Block)与缩进
在 Python 中,**代码块(Code Block)是指属于同一个逻辑结构的一组语句。Python 不像 C、Java 那样使用大括号 {} 来包裹代码,而是通过缩进(Indentation)**来界定代码块的归属:
- 冒号引出代码块:在
def行末写下冒号:,意味着从下一行开始进入属于该函数的代码块; - 缩进规范:Python 官方 PEP 8 风格指南推荐统一使用 4 个空格 作为一个缩进层级。在 VS Code 中,状态栏右下角应确认设置为“空格: 4”,此时只需按一次键盘上的
Tab键,编辑器就会自动输入 4 个空格; - 层级一致性:同一个代码块内的所有语句必须保持完全相同的缩进距离;
- 退出代码块:当需要结束函数的定义时,只需将下一行代码恢复顶格书写,不再缩进。
这一机制是 Python 最核心的语法特征。不仅函数体是一个代码块,后续章节学习的条件分支(if 代码块)以及循环遍历(for 与 while 代码块),都会采用这种“冒号 + 缩进”的统一结构。
案例:定义商品小计函数,计算 4 件单价 28.5 元商品的小计金额。
def calculate_subtotal(unit_price, quantity):
subtotal = unit_price * quantity
return subtotal
amount = calculate_subtotal(28.5, 4)
print(amount)
输出:
114.0
2.2.3 程序的执行流程
计算机在执行上述程序时,分为“定义”与“调用”两个阶段:
- 定义阶段(第 1–3 行):
- 计算机遇到
def calculate_subtotal(...),在内存中登记这个函数名和它包含的处理规则; - 此时缩进的代码块并不会被执行,它只是被记录下来待命。
- 计算机遇到
- 调用阶段(第 5 行):
- 程序执行到
calculate_subtotal(28.5, 4),看到函数名后有圆括号,立即触发函数调用; - 系统的输入开始生效:实参
28.5传递给形参unit_price,实参4传递给形参quantity; - 计算机跳转到函数内部的代码块,执行处理器逻辑:计算
28.5 * 4,并将结果赋给内部变量subtotal; - 遇到
return subtotal,处理完成,将计算结果114.0作为输出交还给调用处; - 外面的变量
amount接收到输出值114.0。
- 程序执行到
- 后续执行(第 6 行):
- 执行
print(amount),将结果显示到终端。
- 执行
2.2.4 缩进错误排查
如果在编写函数体时没有缩进,或者同一块代码缩进空格数不一致,Python 解释器在编译时就会直接报错(IndentationError):
案例:观察折扣计算函数中缩进代码块与外层调用语句的区分。
def apply_discount(price, rate):
discounted_price = price * rate
return discounted_price
final_price = apply_discount(120.0, 0.85)
print(final_price)
输出:
102.0
试一试(必做):故意删除 discounted_price = ... 前面的 4 个空格并运行,阅读终端中提示的 IndentationError: expected an indented block after function definition。恢复缩进后再次运行,核对结果。这种单点改错练习可以帮助你快速熟悉缩进报错的特征。
2.3 参数:向函数传入数据
定义时的参数名可以看作待接收数据的名称;调用时提供的是本次使用的实际数据,也称实参。每次调用可以提供不同的数据。
2.3.1 一个参数
案例:把视频时长从分钟换算为秒,分别计算 5 分钟和 12 分钟对应的秒数。
def minutes_to_seconds(minutes):
return minutes * 60
print(minutes_to_seconds(5))
print(minutes_to_seconds(12))
输出:
300
720
2.3.2 多个位置参数
多个参数用逗号隔开。按位置传参时,数据与参数从左到右依次对应。
案例:将“小时和分钟”形式的行程时间换算为总分钟数。
def to_total_minutes(hours, minutes):
return hours * 60 + minutes
travel_minutes = to_total_minutes(1, 30)
print(travel_minutes)
输出:
90
这里 hours 接收 1,minutes 接收 30。交换两个实参的位置,会变成“30 小时 1 分钟”,即 1801 分钟。
试一试(必做):先预测 to_total_minutes(2, 15) 的结果,再运行核对。也可以尝试少传一个参数,阅读错误信息中指出的参数名。
2.3.3 传入并返回字符串
参数与返回值可以是字符串、数字或其他类型的数据,具体由函数要完成的任务决定。
案例:去掉姓名两端的空格,并生成报名确认文字。
def make_confirmation(raw_name):
name = raw_name.strip()
return f"{name},报名已确认。"
message = make_confirmation(" 李明辉 ")
print(message)
输出:
李明辉,报名已确认。
2.4 返回值与屏幕输出
2.4.1 return 与 print
return 决定本次函数调用得到什么值。print() 把内容写到输出位置,通常显示在终端;它本身返回 None。一个函数运行到结尾,若没有返回表达式,也会返回 None,表示这里没有提供具体的计算结果。
案例:分别调用“返回求和结果”和“打印求和结果”的函数,观察哪个调用结果能够继续参与运算。
def add_with_return(a, b):
return a + b
def add_with_print(a, b):
print(a + b)
total_1 = add_with_return(100, 200)
print(total_1 + 6)
total_2 = add_with_print(100, 200)
print(total_2)
# 取消下一行的注释会引发 TypeError:None 不能与整数相加。
# print(total_2 + 6)
输出:
306
300
None
第一行来自求和结果加 6。第二行是 add_with_print() 在函数内部打印的内容。第三行是这个函数的返回值。若调用者需要继续使用和,就应返回这个和;只用于显示通知的函数可以不返回具体结果。
单独写 return、不在后面跟表达式,也会返回 None。None 的更多用法见第一章补充部分。Python 函数说明。
2.4.2 return 后面的执行顺序
案例:计算商品小计,返回结果后,观察同一函数体中后面的输出语句是否执行。
def calculate_subtotal(unit_price, quantity):
subtotal = unit_price * quantity
return subtotal
print("已经执行到这一行")
amount = calculate_subtotal(28.5, 4)
print(amount)
输出:
114.0
本例执行到 return 时就返回调用处,因此后面的 print() 不会执行。观察完成后,可以删除这条不会执行的语句。
试一试(必做):把这条 print() 移到 return 前面,先预测输出顺序,再运行核对。
2.5 局部变量
2.5.1 函数内部的名称
本节函数中的参数和赋值创建的名称是局部变量。函数外部通过返回值取得结果,不能直接使用函数内部的局部名称。名称的作用域。
案例:计算一笔活动的收入减成本,用返回值在函数外继续显示结果。
def calculate_net_income(income, cost):
net = income - cost
return net
income_result = calculate_net_income(10000, 3500)
print(income_result)
# 取消下一行的注释会引发 NameError:外面没有定义 net。
# print(net)
输出:
6500
net 是函数内部的名称,income_result 是外部接收返回值的名称。函数返回后,这个结果仍可以被后面的程序使用。
2.5.2 同名局部变量
不同函数可以使用相同的局部变量名,这些名称分别属于各自的函数。
案例:分别按给定规则计算配送费和打印费,两个函数都用
fee保存本次计算结果。
def delivery_charge(distance):
fee = distance * 2.0 # 本例按每公里 2 元计算
return fee
def printing_charge(pages):
fee = pages * 0.5 # 本例按每页 0.5 元计算
return fee
print(delivery_charge(5))
print(printing_charge(6))
输出:
10.0
3.0
主线练习(必做)
练习 1:外卖账单结算函数
任务:编写函数
calculate_takeout_bill(food_total, coupon_discount, delivery_fee),根据餐品总额、优惠券金额以及配送费,计算并返回应付金额。
业务规则:
- 应付金额 = 餐品总额 - 优惠券金额 + 配送费;
- 题目保证优惠券金额不超过餐品总额,各项金额均为非负数。
编写完成后,用 print() 打印函数调用结果,并使用下表数据自检:
| 测试调用 | 期望返回值 | 说明 |
|---|---|---|
calculate_takeout_bill(80, 10, 6) |
76 |
正常使用 10 元优惠券与 6 元配送费 |
calculate_takeout_bill(80, 0, 6) |
86 |
无优惠券 |
calculate_takeout_bill(80, 10, 0) |
70 |
免配送费 |
练习 2:阶梯快递运费计算函数
任务:编写函数
calculate_freight(weight),按照给定的重量计算快递运费并返回。
计费规则:
- 寄送包裹的计费重量
weight为大于 1 的整数(单位:公斤); - 首重 1 公斤收费 12 元;
- 超出 1 公斤的续重部分,每公斤加收 5 元(续重公斤数为
weight - 1); - 运费计算公式:
12 + (weight - 1) * 5。
编写完成后,核对以下测试调用的返回值:
| 测试调用 | 期望返回值 | 计算过程 |
|---|---|---|
calculate_freight(2) |
17 |
首重 12 元 + 续重 1 公斤 × 5 元 = 17 元 |
calculate_freight(4) |
27 |
首重 12 元 + 续重 3 公斤 × 5 元 = 27 元 |
练习 3:电影票总额核算函数
任务:编写函数
calculate_ticket_fare(ticket_count, discount_rate),根据购票张数与已确定的折扣率,计算并返回实付总价。
业务规则:
- 单张电影票的原价为 45 元;
ticket_count为正整数购票张数;discount_rate为已确定的折扣比例,例如1.0表示原价无折扣,0.8表示八折优惠;- 应付总额计算公式:
ticket_count * 45 * discount_rate。
编写完成后,核对以下测试调用的返回值:
| 测试调用 | 期望返回值 | 说明 |
|---|---|---|
calculate_ticket_fare(9, 1.0) |
405.0 |
购买 9 张原价票 |
calculate_ticket_fare(10, 0.8) |
360.0 |
购买 10 张团体票,享受八折 |
提示:根据购票张数由程序自动判断并选择折扣率的逻辑,将在下一章条件分支(
if/else)中学习。当前阶段由外部调用者直接提供折扣率。
第二部分:扩展阅读
以下内容供学有余力或需要进一步提升代码灵活度的读者选读。完成核心主线练习不需要依赖本部分内容。
2.6 提高调用灵活性:关键字参数与默认参数
2.6.1 按参数名传入数据(关键字参数)
在调用函数时,除了按照形参的位置依次传参(位置参数),还可以写出 形参名 = 数据,这种调用方式称为关键字参数(Keyword Arguments)。
使用关键字参数时,实参的传递顺序可以任意调整,Python 会根据参数名自动对号入座:
案例:计算 1 小时 30 分钟对应的总分钟数,使用关键字参数明确各个输入的含义,并调换传参顺序。
def to_total_minutes(hours, minutes):
return hours * 60 + minutes
# 1. 传统位置参数调用
print(to_total_minutes(1, 30))
# 2. 关键字参数调用(顺序调换,结果依然正确)
print(to_total_minutes(minutes=30, hours=1))
输出:
90
90
当函数参数较多时,使用关键字参数能让调用处的代码含义非常明确,降低传错顺序的风险。
2.6.2 为参数设置默认值(默认参数)
在定义函数时,可以为某些参数预先指定一个默认值。如果在调用时没有提供该参数,函数就自动采用默认值;如果提供了新值,则覆盖默认值。
语法规则:在函数定义中,有默认值的参数必须放置在没有默认值的参数后面,否则会引发
SyntaxError: non-default argument follows default argument。
案例:计算订单结算金额。普通订单默认收取 6 元运费,如果是免运费订单,调用时可传入
0.0覆盖默认运费。
def checkout(subtotal, shipping_fee=6.0):
return subtotal + shipping_fee
# 未传入 shipping_fee,自动使用默认值 6.0
print(checkout(100.0))
# 传入 shipping_fee=0.0,覆盖默认值
print(checkout(100.0, shipping_fee=0.0))
输出:
106.0
100.0
案例:使用默认参数改写主线中的电影票总额函数,默认折扣率为
1.0(原价)。
def calculate_ticket_fare_with_default(ticket_count, discount_rate=1.0):
return ticket_count * 45 * discount_rate
# 普通购票无需输入折扣率
print(calculate_ticket_fare_with_default(9))
# 团体购票显式指定八折优惠
print(calculate_ticket_fare_with_default(10, discount_rate=0.8))
输出:
405.0
360.0
2.7 一次返回多个结果:多值返回与解包
在很多业务场景中,一个处理过程会产生多个关联结果。在 return 语句后用英文逗号分隔多个表达式,函数就会将它们打包成一个**元组(tuple)**一并返回。
调用者可以用单个变量接收这个元组,也可以用相同数量的变量分别接收其中的每一个值,后者称为解包(Unpacking):
案例:将总分钟数换算为“整小时数”和“剩余分钟数”,观察元组打包与变量解包的两种接收方式。
def split_duration(total_minutes):
hours = total_minutes // 60
minutes = total_minutes % 60
return hours, minutes
# 方式 1:用单个变量接收完整的元组
duration_tuple = split_duration(95)
print(duration_tuple)
# 方式 2:用两个变量分别解包接收对应的值
h, m = split_duration(95)
print(f"{h} 小时 {m} 分钟")
输出:
(1, 35)
1 小时 35 分钟
解包接收时,左侧变量的顺序必须与函数内部 return 返回值的顺序完全一致。关于元组的详细性质与高级操作,将在后续容器章节深入学习。
2.8 函数的说明文档:文档字符串(docstring)
在实际项目中,规范的代码应当附带使用说明。写在函数体第一行的字符串称为文档字符串(docstring),通常使用三引号 """ 书写。
文档字符串用于向其他阅读代码的人(以及未来的自己)说明函数的功能、参数含义和返回值格式:
案例:给商品小计函数编写规范的文档字符串,并使用内置函数
help()查看该说明。
def calculate_subtotal(unit_price, quantity):
"""
根据商品单价与购买数量,计算并返回商品小计金额。
参数:
unit_price (float): 商品单价(元)
quantity (int): 购买数量(件)
返回:
float: 商品小计金额(元)
"""
return unit_price * quantity
# 正常调用函数
print(calculate_subtotal(28.5, 4))
# 查看函数的说明文档
help(calculate_subtotal)
输出:
114.0
Help on function calculate_subtotal in module __main__:
calculate_subtotal(unit_price, quantity)
根据商品单价与购买数量,计算并返回商品小计金额。
参数:
unit_price (float): 商品单价(元)
quantity (int): 购买数量(件)
返回:
float: 商品小计金额(元)
在 VS Code 中,只要为函数编写了文档字符串,当你在其他地方输入该函数名或将鼠标悬停在函数名上方时,编辑器就会弹出格式整齐的说明浮窗。
2.9 全局变量与外部作用域
定义在函数外部、属于整个代码文件最顶层的变量称为全局变量(Global Variable)。
函数内部可以直接读取外部全局变量的值;如果需要在函数内部修改全局变量的值,则必须使用 global 关键字进行特殊声明。
案例:对比“依赖外部全局变量”与“通过参数显式传递”两种编程风格。
# 推荐风格:通过参数显式传入数据,通过返回值交回结果
def checkout_clean(subtotal, fee):
return subtotal + fee
shipping_fee = 6.0
final_bill = checkout_clean(100.0, shipping_fee)
print(final_bill)
输出:
106.0
设计建议: 在编写函数时,应当尽量通过参数将外部数据输入函数,通过返回值将计算结果输出。避免让函数隐式读取或修改外部全局变量。明确的输入与输出能让函数像独立的积木一样,在不同程序中安全、方便地重复使用。
扩展练习(选做)
练习 1:定期存款本息计算(多值返回)
任务:编写函数
calculate_deposit_return(principal, annual_rate, years),按单利规则同时计算并返回利息以及到期本息合计。需要 §2.7 多值返回与解包知识。
计算规则:
- 利息 = 本金 × 年利率 × 存期年数;
- 本息合计 = 本金 + 利息;
- 函数同时返回
(利息, 本息合计)。
编写完成后,在函数外部用两个变量分别解包接收返回值,并用下表数据核对:
| 测试调用 | 期望返回元组 | 解包打印结果 |
|---|---|---|
calculate_deposit_return(10000, 0.02, 1) |
(200.0, 10200.0) |
利息: 200.0 元,本息合计: 10200.0 元 |
calculate_deposit_return(10000, 0.02, 3) |
(600.0, 10600.0) |
利息: 600.0 元,本息合计: 10600.0 元 |
练习 2:为运费函数编写规范文档字符串
任务:为你编写的
calculate_freight(weight)函数添加规范的文档字符串。需要 §2.8 文档字符串知识。
要求在三引号说明中包含:
- 函数的业务功能简述;
- 输入参数
weight的含义、单位与“大于 1 的整数”这一取值限制; - 返回值的含义与单位。
编写完成后,使用 help(calculate_freight) 在终端输出说明文档,确认文字格式整齐。