5.1 函数入门
先看一段"惨不忍睹"的代码。假设要在程序的 3 个地方打印同样的欢迎横幅:
print("=" * 20)
print("欢迎使用本系统")
print("=" * 20)
# ……中间省略 50 行其他代码……
print("=" * 20)
print("欢迎使用本系统")
print("=" * 20)
# ……又省略 80 行……
print("=" * 20)
print("欢迎使用本系统")
print("=" * 20)
同样三行复制了 3 遍。哪天想把横幅改成 30 个等号,就得翻遍全文改 3 处——漏一处就是 bug。第 2 章变量解决了"数据重复"的问题,现在要解决的是"代码重复":把一段代码打包、起名、随叫随用。这就是函数(function)。
其实你早就是函数的老用户了:print()、input()、len()、type() 都是 Python 提前写好的内置函数。这一章学的是自己定义函数。
定义函数
def say_hello():
print("=" * 20)
print("欢迎使用本系统")
print("=" * 20)
拆解语法:
def say_hello ( ) :
↑ ↑ ↑ ↑
def 函数名 括号 冒号
关键字 (命名规则 ↓
同变量) 缩进的部分是"函数体"
——打包进函数的代码
def是 define(定义)的缩写- 函数名的命名规则和变量完全一样(小写下划线风格,如
calc_total) - 冒号 + 缩进——和 if、for 一个规矩,缩进结束的地方就是函数结束的地方
调用函数
关键规则:定义不执行,调用才执行。 只写上面那段 def,运行后屏幕上什么都没有——你只是"录制"了这段代码,还没"播放"。播放的动作叫调用:
def say_hello():
print("=" * 20)
print("欢迎使用本系统")
print("=" * 20)
say_hello() # 调用:函数名 + 括号
say_hello() # 想用几次调几次,每次都完整执行一遍函数体
开头那段惨不忍睹的代码,现在变成定义一次 + 三行调用。改横幅?只改函数体一处,三处调用全部生效。
两个新手常犯的调用错误:
say_hello # ⚠️ 忘了括号:不报错,但什么都不会执行(这是"不报错的错")
hello_say() # ❌ NameError: name 'hello_say' is not defined(名字拼错)
另外注意顺序:必须先定义,后调用。程序自上而下执行,调用写在 def 前面时,Python 还不认识这个名字,同样报 NameError。
参数:给函数传数据
上面的函数每次干的事一模一样。更多时候我们希望"同一套逻辑,处理不同的数据"——在括号里声明参数:
def say_hello(name): # name 是参数:一个占位的变量
print(f"你好,{name}!")
say_hello("小明") # 你好,小明!
say_hello("小红") # 你好,小红!
执行过程:调用 say_hello("小明") 时,"小明" 被塞给参数 name,然后函数体开始执行——相当于函数体开头自动做了一次 name = "小明" 的赋值。
两个术语了解一下(看别人文章会遇到):定义时括号里的 name 叫形参(形式上的参数,空架子);调用时传的 "小明" 叫实参(实际的数据)。
多个参数用逗号隔开,调用时按顺序对号入座:
def introduce(name, age):
print(f"我叫{name},今年{age}岁")
introduce("小明", 18) # name="小明",age=18
参数个数必须对上,多了少了都报错:
introduce("小明") # ❌ TypeError: introduce() missing 1 required positional argument: 'age'
introduce("小明", 18, 175) # ❌ TypeError: introduce() takes 2 positional arguments but 3 were given
这两条 TypeError 信息非常直白:"缺 1 个参数:age"、"只收 2 个你给了 3 个"——学会读它们,以后调试快一半。
返回值:让函数交出结果
参数是数据的"入口",返回值就是数据的"出口"。用 return 把结果交回给调用的地方:
def add(a, b):
return a + b
result = add(3, 5) # add(3, 5) 执行完"变成"了 8,赋给 result
print(result) # 8
理解返回值的诀窍:调用表达式本身会被替换成返回值。add(3, 5) 在程序眼里就是 8,所以它能出现在任何 8 能出现的地方:
print(add(10, 20) * 2) # 60,返回值直接参与运算
total = add(1, 2) + add(3, 4) # 3 + 7 = 10
全节最重要:print 和 return 的区别
这是函数学习的第一道分水岭,必须彻底搞清楚:
print:把结果显示在屏幕上给人看,程序本身拿不到这个值return:把结果交回给程序继续用,但屏幕上什么都不显示
做实验对比:
def add_print(a, b):
print(a + b) # 只负责"喊出来"
def add_return(a, b):
return a + b # 只负责"交上去"
x = add_print(3, 5) # 屏幕显示 8(print 干的)
print(x) # None !
y = add_return(3, 5) # 屏幕安安静静
print(y) # 8
x 为什么是 None?因为 add_print 里没有 return——没有 return 的函数,返回值一律是 None("什么都没有"的意思)。屏幕上那个 8 只是喊给人听的,程序手里接到的是空。
眼熟吗?4.1 节 lst = lst.append(x) 得到 None,就是同一件事——append 没有返回值。现在你彻底明白那个坑的原理了。
💡 经验法则:函数尽量用 return 交结果,把"要不要打印"的决定权留给调用者。 一个
return的函数,结果可以打印、可以存变量、可以继续运算;一个只会
return 会立刻结束函数
return 一执行,函数当场结束,后面的代码不再运行——利用这一点可以写"提前返回":
def check_age(age):
if age < 0:
return "年龄不合法" # 不合法就提前走人
return f"年龄是 {age}" # 能走到这行,说明 age 一定 >= 0
print(check_age(-5)) # 年龄不合法
print(check_age(18)) # 年龄是 18
"先把异常情况 return 掉,剩下的都是正常流程"是很受推崇的写法,比层层嵌套 if-else 清爽得多。
返回多个值
想同时交出几个结果?直接逗号隔开——本质是打包成元组返回(4.2 的解包在这里派上大用场):
def min_max(nums):
return min(nums), max(nums) # 实际返回元组 (最小值, 最大值)
low, high = min_max([3, 8, 1, 9]) # 解包接收
print(low, high) # 1 9
给函数写说明:文档字符串
函数体第一行用三引号写说明,叫文档字符串(docstring):
def calc_bmi(height, weight):
"""计算 BMI 指数并返回带评价的字符串。height 单位米,weight 单位公斤。"""
bmi = weight / height ** 2
if bmi < 18.5:
return f"BMI {bmi:.1f},偏瘦"
elif bmi < 24:
return f"BMI {bmi:.1f},正常"
else:
return f"BMI {bmi:.1f},偏胖"
print(calc_bmi(1.75, 65)) # BMI 21.2,正常
它不影响运行,但在 VS Code 里把鼠标悬停在函数名上就能看到这段说明——给函数配说明书是专业习惯,从现在养成。
顺便看看这个函数:它就是 2.4 节 BMI 计算器的"函数版"。当初的一次性脚本,包装成函数后就能反复调用、被别的程序复用——这就是函数的价值。
本节报错速查
| 报错 | 人话翻译 | 本节的触发场景 |
|---|---|---|
NameError | 名字不存在 | 函数名拼错;调用写在定义之前 |
TypeError: missing ... argument | 缺参数 | 调用时传的参数比定义的少 |
TypeError: takes ... but ... were given | 参数多了 | 调用时传的参数比定义的多 |
外加一个不报错的错:调用忘写括号(say_hello),以及拿 print 型函数的"返回值"(得到 None)。
练习
新建 practice51.py 完成:
1. 定义 max2(a, b):返回两个数中较大的(不许用内置 max)。
2. 定义 is_even(n):判断 n 是否为偶数,返回 True/False。提示:想想 2.2 节的 % 和比较运算,能不能一行搞定?
3. 定义 total_price(price, count):返回总价,满 100 打 9 折。
4. 下面的程序想算出两次考试的总分,但结果是报错,找出原因修好它:
def show_score(name, score):
print(f"{name}:{score} 分")
s1 = show_score("期中", 85)
s2 = show_score("期末", 92)
print(f"总分:{s1 + s2}")
点击查看答案
# 1
def max2(a, b):
if a > b:
return a
return b # 能走到这里说明 a <= b,不用再写 else
# 2
def is_even(n):
return n % 2 == 0 # 比较表达式的结果本身就是 True/False,直接返回
# 3
def total_price(price, count):
total = price * count
if total >= 100:
total *= 0.9
return total
print(max2(3, 7)) # 7
print(is_even(10)) # True
print(total_price(30, 4)) # 108.0
第 4 题: show_score 只 print 不 return,s1、s2 拿到的都是 None,None + None 报 TypeError。把函数改成 return,打印交给调用者:
def get_score_line(name, score):
return score
# 更实际的改法:函数直接返回成绩参与计算,展示用 f-string
def show_score(name, score):
print(f"{name}:{score} 分")
return score
s1 = show_score("期中", 85)
s2 = show_score("期末", 92)
print(f"总分:{s1 + s2}") # 总分:177
本章小结
- 函数解决代码重复:
def 函数名(参数):定义,函数名(实参)调用 - 定义不执行,调用才执行;先定义后调用;调用别忘括号
- 参数是入口,
return是出口;调用表达式会被替换成返回值 - print 给人看,return 给程序用;没有 return 的函数返回 None
- return 立刻结束函数(提前返回);逗号隔开可返回多个值(元组 + 解包)
- 用 docstring 给函数写说明书
下一节:5.2 函数参数