跳到主要内容

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 的函数,结果可以打印、可以存变量、可以继续运算;一个只会 print 的函数,除了看一眼什么都做不了。

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 函数参数