2.4 可以复用的代码
知识点
1.为什么需要"复用"
问题:前面写的求最大公约数、判断素数、求平均值……下一个程序还要用。如果每次都复制粘贴一遍,会有三个麻烦:
- 代码变长,可读性差;
- 改一次要改多处,容易漏改出错;
- 无法独立测试,错误难定位。
解决方案:把这些"功能单一、会被反复使用"的代码打包成函数,用时调用即可。
函数的两大来源:
| 来源 | 说明 | 例子 |
|---|---|---|
| 自定义函数 | 自己用 def 定义 | 求最大公约数、判断闰年 |
| 内置函数/库函数 | Python 自带或第三方提供 | print()、input()、len()、math.sqrt()、random.randint() |
复用带来的三个好处(答题落点):提高代码复用率、降低维护成本、便于分工协作与测试。
2.函数的定义与调用
(1)定义格式
def 函数名(参数表):
"""函数说明(可选)"""
函数体
return 返回值 # 可选
四条语法要求:
- 以关键字
def开头; - 函数名后必须有一对圆括号(没有参数也要写
()); - 括号后必须有英文冒号
:; - 函数体必须缩进(4 个空格)。
(2)调用格式
函数名(实参表)
关键理解:定义函数时函数体不执行,只有在被调用时才执行;定义一次,可以调用无数次。
# 2-10 定义一个求圆面积的函数
def circle_area(r):
"""计算圆的面积"""
return 3.14159 * r * r
print(circle_area(1)) # 3.14159
print(circle_area(2)) # 12.56636
print(circle_area(10)) # 314.159
函数名的命名规则:与变量相同(字母/数字/下划线,不能数字开头,不用关键字),见名知义,一般用动词或动宾短语(get_max、is_leap、circle_area)。
(3)函数说明文档(docstring)
函数体第一行的三引号字符串是函数说明,可用 help(函数名) 查看。这是好习惯,也是"代码规范"的考点。
3.参数:函数的"入口"
(1)形参与实参
| 概念 | 位置 | 说明 |
|---|---|---|
| 形参(形式参数) | 定义时括号里的变量 | 只是"占位符",没有具体值 |
| 实参(实际参数) | 调用时括号里的值 | 真正参与计算的数据 |
def add(a, b): # a、b 是形参
return a + b
print(add(3, 5)) # 3、5 是实参
传递过程:调用时把实参的值赋给对应的形参(a = 3, b = 5),再执行函数体。
(2)三种参数形式
| 形式 | 写法 | 说明 | 示例 |
|---|---|---|---|
| 位置参数 | 按顺序对应 | 最常见,顺序不能错 | add(3, 5) |
| 关键字参数 | 形参名=值 | 按名字对应,顺序可换 | add(b=5, a=3) |
| 默认参数 | def f(a, b=1): | 调用时可省略,取默认值 | f(3) 等价于 f(3, 1) |
默认参数的两条规则:
- 默认参数必须放在参数表末尾(不能写在无默认值参数前面);
- 默认参数的值在函数定义时确定一次,不会每次调用重新计算(了解即可)。
(3)参数个数必须匹配
def add(a, b):
return a + b
add(3) # TypeError:缺少一个参数
add(3, 5, 7) # TypeError:多了参数
报错关键词:missing 1 required positional argument(少参数)、takes 2 positional arguments but 3 were given(多参数)。
4.返回值:函数的"出口"
(1)return 的作用
- 把结果交回给调用处;
- 立即结束函数(
return之后的语句不再执行)。
def get_grade(score):
if score >= 90:
return "A"
elif score >= 60:
return "及格"
return "不及格" # 前面都没返回,才会走到这里
对比 print 与 return(极重要):
print(x) | return x | |
|---|---|---|
| 作用 | 把值显示在屏幕上 | 把值交给调用者 |
| 能否参与运算 | 不能(print(3) + 1 报错) | 能(get_grade(95) + "!" 合法) |
| 是否结束函数 | 不结束 | 立即结束 |
一句话:print 是"给人看",return 是"给程序用"。
(2)没有 return 时返回什么
返回 None(表示"没有值")。
def show(msg):
print(msg)
x = show("hi") # 屏幕输出 hi
print(x) # 输出 None
易错:忘了写 return,结果被赋给变量后是 None,后续运算报错 TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'。
(3)返回多个值
def min_max(nums):
return min(nums), max(nums) # 用逗号分隔,实际返回一个元组
lo, hi = min_max([3, 1, 4, 1, 5]) # 解包接收
print(lo, hi) # 1 5
本质:返回的是元组(tuple),可以用解包的方式接收。
5.变量的作用域
| 概念 | 定义位置 | 可见范围 | 生命周期 |
|---|---|---|---|
| 局部变量 | 函数内部定义 | 只在函数内可见 | 函数调用结束即销毁 |
| 全局变量 | 函数外部定义 | 整个程序可见(函数内可读) | 程序运行期间一直存在 |
total = 0 # 全局变量
def add(n):
s = 0 # 局部变量
for i in range(1, n + 1):
s = s + i
return s
print(add(10))
print(s) # NameError:s 在函数外不存在
三条规则:
- 函数内可以读全局变量;
- 函数内要给全局变量赋值,必须先用
global声明——但这是不推荐的做法(耦合高、易出错); - 同名变量:函数内的局部变量会遮蔽同名的全局变量,两者互不影响。
最佳实践:尽量用参数传入、用 return 传出,少用全局变量。
6.库与模块:站在别人的肩膀上
库(library)/模块(module):别人写好的、功能相关的函数集合,放在一个 .py 文件里,导入后即可使用。
Python 的标准库:随 Python 一起安装,无需额外下载(如 math、random、time、turtle)。
(1)三种导入方式
| 写法 | 使用方式 | 特点 |
|---|---|---|
import math | math.sqrt(16) | 推荐:来源清楚,不会与自定义函数重名 |
from math import sqrt, pi | sqrt(16)、pi | 简洁,但可能重名冲突 |
import math as m | m.sqrt(16) | 起别名,名字长时方便 |
from math import * | 全部导入 | 不推荐:污染命名空间,不知道函数来自哪 |
(2)math 库常用函数
| 函数/常量 | 作用 | 示例 → 结果 |
|---|---|---|
math.sqrt(x) | 平方根 | math.sqrt(16) → 4.0 |
math.pi | 圆周率 π | 3.141592653589793 |
math.e | 自然常数 e | 2.718281828459045 |
math.pow(x, y) | 幂(等价于 x ** y) | math.pow(2, 3) → 8.0 |
math.fabs(x) | 绝对值(返回 float) | math.fabs(-3) → 3.0 |
math.ceil(x) | 向上取整 | math.ceil(3.2) → 4 |
math.floor(x) | 向下取整 | math.floor(3.8) → 3 |
math.gcd(a, b) | 最大公约数 | math.gcd(48, 18) → 6 |
math.sin/cos/tan(x) | 三角函数(弧度) | math.sin(math.pi/2) → 1.0 |
math.log(x) | 自然对数 | math.log(math.e) → 1.0 |
math.factorial(n) | 阶乘 | math.factorial(5) → 120 |
注意:math.sqrt() 的参数不能为负数(会报 ValueError: math domain error);math.ceil 与 math.floor 返回的是整数。
(3)random 库常用函数
| 函数 | 作用 | 示例 → 可能结果 |
|---|---|---|
random.random() | 产生 0~1 之间的随机小数(含 0 不含 1) | 0.7234... |
random.randint(a, b) | 产生 a~b 之间(含两端) 的随机整数 | random.randint(1, 6) → 骰子点数 |
random.randrange(a, b, step) | 按 range 规则取随机整数(含头不含尾) | random.randrange(1, 10, 2) → 1、3、5、7、9 |
random.choice(seq) | 从序列中随机选一个 | random.choice(["石头","剪刀","布"]) |
random.shuffle(lst) | 原地打乱列表顺序 | 洗牌 |
random.sample(seq, k) | 随机取 k 个不重复元素 | 抽奖 |
random.uniform(a, b) | 产生 a~b 之间的随机小数 | random.uniform(1, 10) |
最容易错的一对:randint 含两端,randrange 含头不含尾。
# 2-11 猜数字小游戏(random + while + if 的综合运用)
import random
answer = random.randint(1, 100)
count = 0
while True:
guess = int(input("猜一个 1~100 的数:"))
count += 1
if guess > answer:
print("大了")
elif guess < answer:
print("小了")
else:
print(f"猜对了!共猜 {count} 次")
break
(4)自定义模块
把自己写的函数保存为 mytools.py,在同一目录的新程序里 import mytools 即可用 mytools.函数名() 调用。
# mytools.py
def is_leap(year):
return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
# main.py
import mytools
print(mytools.is_leap(2024)) # True
意义:把自己积累的工具函数沉淀成模块,下次直接复用——这就是"可以复用的代码"的完整含义。
7.递归入门(可了解)
递归:函数自己调用自己,必须包含两个部分:
- 递归关系(把大问题化成同类的小问题);
- 终止条件(边界,否则无限递归导致
RecursionError)。
# 2-12 递归求阶乘
def fact(n):
if n == 1: # 终止条件
return 1
return n * fact(n - 1) # 递归关系
print(fact(5)) # 120
递归 vs 循环:递归代码短、贴近数学定义,但占用栈空间、层次深时效率低;能用循环解决的简单问题,优先用循环。汉诺塔等问题的递归解法见 4.3。
8.易错点清单
| 序号 | 错误 | 说明 | 正确做法 |
|---|---|---|---|
| 1 | def f() 后忘冒号 | SyntaxError | def f(): |
| 2 | 定义后直接写 f 不写 f() | 只是引用函数对象,不执行 | 调用要加括号 |
| 3 | 函数定义在调用之后 | 程序自上而下执行,会 NameError | 先定义后调用 |
| 4 | 忘写 return | 返回 None | 需要结果就 return |
| 5 | 用 print 代替 return | 结果无法参与后续运算 | 用 return |
| 6 | 默认参数写在前面 | SyntaxError | 默认参数放末尾 |
| 7 | 实参个数与形参不符 | TypeError | 个数对应或用默认值 |
| 8 | 函数内改全局变量未声明 | 实际创建了局部变量,全局值没变 | global 声明(不推荐) |
| 9 | import math 后直接写 sqrt() | NameError | math.sqrt() 或 from math import sqrt |
| 10 | math.sqrt(-1) | ValueError | 先判断非负 |
| 11 | randint(1, 6) 与 randrange(1, 6) 混同 | 前者含 6,后者不含 | 看清是否需要取到端点 |
| 12 | 三角函数直接传角度 | 结果是错的 | 先转弧度(* math.pi / 180) |
9.速查表
| 序号 | 考点 | 结论 |
|---|---|---|
| 1 | 定义函数关键字 | def |
| 2 | 定义语法要点 | def 名(参数): + 缩进函数体 |
| 3 | 调用 | 函数名(实参),必须加括号 |
| 4 | 形参/实参 | 定义处的叫形参,调用处的叫实参 |
| 5 | 默认参数位置 | 必须放参数表末尾 |
| 6 | return 作用 | 返回值 + 立即结束函数 |
| 7 | 无 return 时返回值 | None |
| 8 | print 与 return | print 给人看,return 给程序用 |
| 9 | 返回多个值 | 用逗号,本质是元组,可解包接收 |
| 10 | 局部变量 | 函数内定义,函数外不可见 |
| 11 | 全局变量 | 函数外定义,函数内可读,改需 global |
| 12 | 库的导入 | import、from … import …、import … as … |
| 13 | math.sqrt(16) | 4.0(参数不能为负) |
| 14 | math.pi | 圆周率常量 |
| 15 | 向上/向下取整 | math.ceil/math.floor |
| 16 | 最大公约数 | math.gcd(a, b) |
| 17 | 三角函数单位 | 弧度,需角度转弧度 |
| 18 | random.random() | 0~1 的小数(不含 1) |
| 19 | random.randint(1, 6) | 含 1 和 6 |
| 20 | random.randrange(1, 6) | 含 1 不含 6 |
| 21 | random.choice(seq) | 随机取一个元素 |
| 22 | 自定义模块 | 保存为 .py,同目录 import 即可 |
| 23 | 递归两要素 | 终止条件 + 递归关系 |
10.课堂提示(讲课参考)
- 先制造"复制粘贴的痛苦":让学生把求最大公约数的代码粘到第二个程序里,再改需求,体会"改三处、漏一处"的麻烦,再引入函数——需求驱动比直接讲语法有效。
- print 与 return 用一个题区分:
def f(x): print(x*2)与def g(x): return x*2,让学生写f(3)+1和g(3)+1,一个报错一个得 7。 math.sqrt与**0.5都演示一遍,说明"同一个功能可以有多条路"。- 猜数字游戏是 2.4 的最佳综合案例:
random+while+if+break全用上,学生做出成品会有成就感,也为 4.4 综合问题打基础。 - 提醒"randint 含端点"这个坑:掷骰子写
randrange(1, 6)永远掷不出 6,让学生自己试出来。