5-函数
约 7540 字大约 25 分钟
2026-10-30
经过前四章的学习,已经掌握了变量、三种流程控制结构,以及列表、元组、字典、集合四种容器,还开发了班级成绩管理小程序。但回头看看写过的代码,会发现一个越来越明显的毛病:同一段逻辑反复出现。例题4-2里"求平均分"的代码出现过不止一次,如果要分别统计一班、二班的平均分,几乎要把同一段循环原样复制两遍。复制粘贴的代码就像复印的合同:一旦规则变了(比如平均分要保留两位小数),得挨份去找、挨份去改,漏改一处就埋下一个Bug。
解决这个问题的武器就是函数(Function)。其实从第一章起,大家一直在用函数:print()负责打印、input()负责接收输入、len()负责数个数——它们都是别人写好的函数,"一次定义、处处调用"。本章要学的,就是把自己的代码也封装成这样的"零件":写一次,起个名字,以后随叫随到。这正是从"写代码"走向"写程序"的关键一步——真正的软件就是由成百上千个函数组装起来的。
本章分三步走:第一步学习函数的定义与调用,解决"怎么造零件";第二步学习参数与返回值,解决"零件怎么接进线、怎么出成果",这是本章的重点;第三步学习作用域、可变参数、lambda等进阶内容,并对递归做个简单了解。学完本章,第六章的"模块"就水到渠成了——模块,说白了就是把一组相关的函数放进同一个文件里打包管理。
【学习目标】
知识目标
理解函数的概念和"一次定义、处处调用"的价值,掌握def定义函数和调用函数的语法。
掌握位置参数、关键字参数、默认值参数的使用规则,了解可变默认值的陷阱。
掌握return返回值的用法,理解无返回值时默认返回None。
掌握返回多个值的方法,理解其本质是返回元组。
理解局部变量与全局变量的作用域规则。
了解*args、**kwargs的收集作用和lambda匿名函数的典型用法。
了解递归的概念,能够看懂简单的递归函数。
能力目标
能够把重复出现的代码提炼成函数,消除复制粘贴。
能够编写带参数、带返回值的通用函数,解决成绩统计等实际问题。
能够用函数把一个小程序拆分成若干功能模块,并通过菜单组织起来。
素养目标
培养模块化思维,体会"一次定义、处处调用"的工程价值。
培养规范意识,养成写文档字符串的习惯,让代码可读、可维护。
培养严谨细致的编程作风,警惕可变默认值陷阱,慎用global。
1.函数的定义与调用
1.1 为什么需要函数
先看一段"疼痛体验"。两个班各有一份成绩列表,都要算平均分:
代码 5-1:复制粘贴式代码
# 一班:求平均分
scores1 = [85, 92, 78, 96, 60]
total1 = 0
for score in scores1:
total1 += score
average1 = total1 / len(scores1)
print(f"一班平均分:{average1}")
# 二班:同样的逻辑,几乎原样再抄一遍
scores2 = [70, 88, 95, 81]
total2 = 0
for score in scores2:
total2 += score
average2 = total2 / len(scores2)
print(f"二班平均分:{average2}")程序运行效果如下:
一班平均分:82.2
二班平均分:83.5代码能跑,但隐患不小:现在只有两个班,假如全校二十个班都要算,这段循环就要抄二十遍;哪天老师要求"平均分保留两位小数",就得改二十处,漏改一处就是Bug。重复代码越多,程序越难维护。
函数就是专门对付重复的。把"求平均分"这段逻辑封装成一个函数,起名叫calc_average,以后谁要算平均分,一句calc_average(成绩列表)就搞定,逻辑只在函数里维护一份。这正是函数的两大价值:复用(写一次,用无数次)和封装(内部怎么实现,调用者不必关心,就像用print()从来不需要知道它内部怎么运作)。
1.2 定义函数
Python里用def关键字定义函数,def正是define(定义)的缩写。先看一个最简单的无参函数:
代码 5-2:定义并调用一个函数
def show_menu():
"""显示主菜单"""
print("=" * 20)
print("1. 添加学生")
print("2. 查询成绩")
print("0. 退出")
print("=" * 20)
show_menu() # 调用函数程序运行效果如下:
====================
1. 添加学生
2. 查询成绩
0. 退出
====================一个函数定义由四部分组成:
| 组成部分 | 上例对应 | 说明 |
|---|---|---|
| 关键字def | def | 告诉Python:我要定义函数了 |
| 函数名 | show_menu | 命名规则与变量相同,用小写字母加下划线,取名要"见名知义" |
| 参数列表 | () | 括号里写参数,无参就空着,别忘了冒号 |
| 函数体 | 缩进的四行print | 缩进的部分属于函数,和if、for的规则一致 |
有两个初学者最容易犯的错,特别提醒:第一,函数定义只是"登记造册",def下面的代码此时并不会执行——把代码5-2最后的show_menu()删掉再运行,你会发现什么都没打印;只有调用(写上函数名加括号)时,函数体才真正执行。第二,函数必须先定义、后调用,定义代码要写在第一次调用之前。
1.3 调用函数
现在用函数改造代码5-1,体会"一次定义、处处调用":
代码 5-3:用函数消除重复
def calc_average(scores):
"""计算成绩列表的平均分"""
total = 0
for score in scores:
total += score
return total / len(scores)
scores1 = [85, 92, 78, 96, 60]
scores2 = [70, 88, 95, 81]
print(f"一班平均分:{calc_average(scores1)}")
print(f"二班平均分:{calc_average(scores2)}")程序运行效果如下:
一班平均分:82.2
二班平均分:83.5效果和代码5-1完全一样,但循环只写了一遍。调用时程序的执行路线值得琢磨:主程序从上往下执行,遇到calc_average(scores1),立刻"跳进"函数体内,把实参scores1交给形参scores,执行完函数遇到return,再带着结果"跳回"调用处,接着往下执行。函数就像一个独立的车间:原料(参数)从门口送进去,成品(返回值)从门口送出来,车间里怎么加工,外面不用管。
1.4 文档字符串
好函数不仅要写得好,还要"自带说明书"。写在函数体第一行的三引号字符串叫文档字符串(docstring),它是Python官方推荐的函数说明方式:
代码 5-4:文档字符串与help()函数
def calc_average(scores):
"""计算成绩列表的平均分
参数scores:成绩列表,如[85, 92, 78]
返回值:平均分
"""
return sum(scores) / len(scores)
help(calc_average)程序运行效果如下:
Help on function calc_average in module __main__:
calc_average(scores)
计算成绩列表的平均分
参数scores:成绩列表,如[85, 92, 78]
返回值:平均分help()会把文档字符串提取出来展示,第六章查询random模块、math模块的用法时,看到的正是别人写的文档字符串。习惯上,docstring先一句话说清功能,再说明参数和返回值。别嫌麻烦——这份说明书是写给三个月后忘了细节的自己,以及接手代码的同学的。
【例题 5-1】成绩报表打印函数
某老师每学期都要打印各班的成绩报表,报表包括居中的标题、逐条的成绩和平均分。请编写函数print_report,实现"传一个班名和一份成绩,就打出一张报表"。
代码 5-5:成绩报表打印函数
def print_report(class_name, scores):
"""打印班级成绩报表
参数class_name:班级名称,如"一班"
参数scores:成绩列表
"""
print(class_name.center(30, "="))
print(f"人数:{len(scores)}人")
for i in range(len(scores)):
print(f"第{i + 1}名:{scores[i]}分")
print("=" * 30)
average = sum(scores) / len(scores)
print(f"平均分:{average:.1f}分")
print_report("一班成绩表", [85, 92, 78, 96, 60])
print() # 空一行,接着打第二张报表
print_report("二班成绩表", [70, 88, 95, 81])程序运行效果如下:
============一班成绩表============
人数:5人
第1名:85分
第2名:92分
第3名:78分
第4名:96分
第5名:60分
==============================
平均分:82.2分
============二班成绩表============
人数:4人
第1名:70分
第2名:88分
第3名:95分
第4名:81分
==============================
平均分:83.5分这个例子复习了第三章的center()、f-string格式化,也演示了函数最朴素的用法:把一段"打印流程"打包,两行调用就产出两张报表。假如老师要求报表再加一行"最高分",只需要改函数一处,两个班的报表同时升级——这就是封装的红利。
2.函数的参数与返回值
2.1 形参与实参
函数括号里的参数,在定义和调用时有两个不同的名字:定义时叫形参(形式参数),是占位置的"形",相当于车间门口的原料入口标签;调用时叫实参(实际参数),是真正传进去的值,就是实际送进车间的原料。
代码 5-6:形参与实参
def check_score(score): # score是形参:只负责"占位"
if score >= 60:
print(f"{score}分:及格")
else:
print(f"{score}分:不及格")
check_score(85) # 85是实参:真正传进去的值
check_score(45) # 同一个函数,每次可以传不同的实参程序运行效果如下:
85分:及格
45分:不及格形参在函数内部就当普通变量用。一个函数可以有多个形参,用逗号隔开;调用时实参的个数、顺序都要和形参对上,否则直接报错。
2.2 传参方式
实参怎么"对号入座"?Python提供了灵活的规则。
最基本的是位置参数:实参按顺序依次对应形参,第一个对第一个、第二个对第二个:
代码 5-7:位置参数与关键字参数
def introduce(name, age, city):
print(f"我叫{name},今年{age}岁,来自{city}")
introduce("张三", 18, "太原") # 位置参数:按顺序对应
introduce(city="北京", name="李四", age=20) # 关键字参数:指名道姓,顺序随意
introduce("王五", city="上海", age=22) # 混合使用:位置参数必须在前程序运行效果如下:
我叫张三,今年18岁,来自太原
我叫李四,今年20岁,来自北京
我叫王五,今年22岁,来自上海第二种是关键字参数:调用时写"形参名=值",指名道姓地传递,顺序就无所谓了。参数一多,关键字参数的可读性优势特别明显——introduce(name="张三", age=18)一眼就知道谁是谁,而introduce("张三", 18)得回头数形参顺序。两种方式可以混用,但位置参数必须写在关键字参数前面。
第三种是默认值参数:定义形参时直接给它一个"出厂默认值",调用时不传就用默认值,传了就覆盖:
代码 5-8:默认值参数
def greet(name, greeting="你好"):
print(f"{greeting},{name}!")
greet("张三") # greeting不传,用默认值"你好"
greet("李四", "早上好") # 位置方式覆盖默认值
greet("王五", greeting="晚上好") # 关键字方式覆盖默认值程序运行效果如下:
你好,张三!
早上好,李四!
晚上好,王五!注意语法规则:带默认值的形参必须放在不带默认值的形参后面。写成def greet(greeting="你好", name)会直接报语法错误——想想也好理解,要是默认值参数排在前面,位置参数就没法"按顺序对号入座"了。
默认值参数还有一个著名的陷阱:默认值不能是列表、字典这类可变对象。看一个错误示范:
代码 5-9:可变默认值的陷阱
# 错误示范:默认值用了空列表
def add_score(new, scores=[]):
scores.append(new)
return scores
print(add_score(85)) # 输出:[85]
print(add_score(92)) # 输出:[85, 92]——上一次的85怎么还在?!
# 正确写法:默认值用None,函数体内再创建列表
def add_score2(new, scores=None):
if scores is None:
scores = []
scores.append(new)
return scores
print(add_score2(85)) # 输出:[85]
print(add_score2(92)) # 输出:[92],各是各的,互不干扰原因在于:默认值只在函数定义时创建一次,此后所有"没传参"的调用共用同一个列表。初学阶段记住口诀就行:默认值用数字、字符串、None这些不可变对象,别用列表和字典。三种传参方式汇总见表5-1。
表 5-1:三种传参方式对比
| 传参方式 | 写法示例 | 特点 |
|---|---|---|
| 位置参数 | introduce("张三", 18, "太原") | 按顺序对应,最常用 |
| 关键字参数 | introduce(name="张三", age=18) | 指名道姓,顺序无关,可读性强 |
| 默认值参数 | def greet(name, greeting="你好") | 不传用默认值,传了则覆盖 |
| 混合使用 | introduce("张三", age=18) | 位置参数必须写在关键字参数前面 |
2.3 return与返回值
函数的"产出"靠return送出。return有两个作用:把结果交给调用处,并立即结束函数。第二条经常被忽略——return一执行,函数体内后面的代码全部不再执行:
代码 5-10:return的用法
def check_pass(score):
if score >= 60:
return "及格" # 一旦执行return,函数立即结束
return "不及格" # 能走到这一行,说明score一定小于60
print(check_pass(85)) # 输出:及格
print(check_pass(45)) # 输出:不及格
def say_hi():
print("你好!") # 只有print,没有return
result = say_hi() # 打印:你好!
print(result) # 输出:Nonecheck_pass里两处return的写法很常见:不同分支从不同的"出口"离开函数,谁先命中走谁。而say_hi演示了另一个事实:函数不写return,Python默认返回None——这就是为什么把一个"只打印不计算"的函数的结果存进变量,打印出来是None。
还要区分两个动作:print是把内容显示给"人"看,return是把结果交还给"程序"用。calc_average(score)的返回值可以继续参与运算、可以存进变量、可以放进f-string;而只print不return的函数,外面拿不到任何值。判断标准很简单:这个函数的结果后面还要不要用?要,就return。
2.4 返回多个值
一个函数能带出好几个结果吗?能,而且写法非常自然——return后面用逗号隔开多个值即可。它的本质是返回一个元组,正好兑现第四章"函数返回多个值其实就是返回元组"的预告:
代码 5-11:返回多个值
def min_max(scores):
return min(scores), max(scores)
lowest, highest = min_max([85, 92, 78, 96, 60]) # 元组解包:几个变量接几个值
print(lowest, highest) # 输出:60 96
result = min_max([85, 92])
print(result) # 输出:(85, 92)——用元组整体接收也行
print(type(result)) # 输出:<class 'tuple'>返回端写return a, b,接收端要么用两个变量解包(第四章学的解包在这里派上用场),要么用一个变量接住整个元组。返回值的数量和接收变量的数量要对齐,多一个少一个都会报错。
【例题 5-2】成绩统计函数包
请编写三个函数:calc_average计算平均分(保留1位小数,空列表返回0)、find_max找出最高分(不用内置max,自己遍历)、get_grade把分数转换为等级(90及以上优秀、80及以上良好、70及以上中等、60及以上及格、否则不及格),并调用它们完成一次班级成绩统计。
代码 5-12:成绩统计函数包
def calc_average(scores):
"""计算平均分
参数scores:成绩列表,如[85, 92, 78]
返回值:保留1位小数的平均分;空列表返回0
"""
if len(scores) == 0:
return 0
average = sum(scores) / len(scores)
return round(average, 1)
def find_max(scores):
"""找出列表中的最高分"""
max_score = scores[0]
for score in scores:
if score > max_score:
max_score = score
return max_score
def get_grade(score):
"""把分数转换为等级"""
if score >= 90:
return "优秀"
elif score >= 80:
return "良好"
elif score >= 70:
return "中等"
elif score >= 60:
return "及格"
else:
return "不及格"
scores = [85, 92, 78, 96, 60]
average = calc_average(scores)
print(f"平均分:{average}")
print(f"最高分:{find_max(scores)}")
print(f"平均分等级:{get_grade(average)}")
print("-" * 20)
for score in scores:
print(f"{score}分:{get_grade(score)}")程序运行效果如下:
平均分:82.2
最高分:96
平均分等级:良好
--------------------
85分:良好
92分:优秀
78分:中等
96分:优秀
60分:及格这三个函数分工明确:calc_average负责"算"、find_max负责"找"、get_grade负责"判",这就是模块化设计的雏形。透露一个后续剧情:第六章讲自定义模块时,会把这一组函数原封不动搬进一个叫score_utils.py的文件,供其他程序import使用——现在把每个函数写扎实,就是在为第六章备料。
3.函数进阶
3.1 变量作用域
函数体内定义的变量叫局部变量,只在函数内部有效,函数一结束就销毁;函数外层定义的变量叫全局变量,整个程序都能访问:
代码 5-13:局部变量出了函数就不存在
def calc_total(scores):
total = 0 # 局部变量:只在calc_total内部存在
for score in scores:
total += score
return total
calc_total([85, 92, 78])
# print(total) # 报错!NameError: name 'total' is not defined局部变量是函数的"隐私":外界看不见也摸不着,这正是封装的体现——每个函数在自己的一亩三分地里干活,互不干扰,同名也不怕。
全局变量则在函数内外都能读,但函数内部想修改它,必须先用global声明:
代码 5-14:全局变量与global声明
school = "阳光职业技术学院" # 全局变量
def show_school():
print(f"学校:{school}") # 读取全局变量:直接读
def change_school():
school = "新学院" # 不声明global,这只是创建了一个同名局部变量!
print(f"函数内:{school}")
change_school()
show_school() # 全局变量并没有被修改程序运行效果如下:
函数内:新学院
学校:阳光职业技术学院注意这个"坑":函数内不加global直接给school赋值,Python会认为你在创建一个同名的局部变量,全局变量纹丝不动——change_school白忙一场。要真正修改全局变量,得写global school再赋值。不过特别提醒:global要慎用。全局变量谁都能改,一旦出错很难追查,规范的做法是多用参数传入、用return传出,让函数之间的数据流向清清楚楚。两种变量的对比见表5-2。
表 5-2:局部变量与全局变量对比
| 对比项 | 局部变量 | 全局变量 |
|---|---|---|
| 定义位置 | 函数内部 | 函数外部(顶层代码) |
| 有效范围 | 仅限所在函数 | 整个程序 |
| 函数内读取 | 直接读 | 直接读 |
| 函数内修改 | 直接改 | 需先global声明(慎用) |
| 使用建议 | 优先使用 | 尽量少用 |
3.2 可变对象作参数
把列表或字典传给函数,函数内的修改会"透传"到函数外面;把字符串或数字传进去,函数内怎么改都不影响外面。看对比:
代码 5-15:可变对象与不可变对象作参数
def add_student(students, name, score):
"""向花名册字典中添加一名学生"""
students[name] = score # 直接改的是传进来的字典本身
students = {"张三": 85}
add_student(students, "李四", 92)
print(students) # {'张三': 85, '李四': 92}——外面也被改了!
def try_change(text):
text = text + "!"
print(f"函数内:{text}")
message = "你好"
try_change(message)
print(f"函数外:{message}") # 字符串没变程序运行效果如下:
{'张三': 85, '李四': 92}
函数内:你好!
函数外:你好可以打个比方:传列表、字典,交出去的是"仓库钥匙",函数拿着钥匙进仓库搬东西,改动是真实的;传字符串、数字,交出去的是"复印件",函数在复印件上怎么涂改,原件安然无恙——因为字符串不可变,text = text + "!"其实是创建了一个新字符串。
这个特性是"利器"也是"暗雷"。利器在于:例题5-3就要利用它,add_student(students, ...)一句话就把学生真正写进花名册,连return都省了。暗雷在于:如果只想"借用"列表算点东西、并不想改动它,函数内就要避免append、pop这类修改操作,或先复制一份(如new_list = scores[:])再操作。第六章score_utils模块用列表作参数传递批量数据,用的正是本节的原理。
3.3 任意数量的参数
前面的函数参数个数都是定死的。假如要写一个求和函数,有时候算3个数、有时候算5个数,怎么办?在形参前面加一个星号,它就会把多余的位置参数统统收进一个元组:
代码 5-16:*args收集任意数量的位置参数
def calc_total(*args):
"""计算任意多个数的总和"""
print(args) # args是一个元组
total = 0
for n in args:
total += n
return total
print(calc_total(85, 92, 78)) # 输出:(85, 92, 78) 和 255
print(calc_total(1, 2, 3, 4, 5)) # 输出:(1, 2, 3, 4, 5) 和 15
print(calc_total()) # 输出:() 和 0程序运行效果如下:
(85, 92, 78)
255
(1, 2, 3, 4, 5)
15
()
0形参名叫args只是约定俗成,起作用的其实是那个星号:它把散落的实参85, 92, 78"打包"成元组(85, 92, 78)。函数体内就把args当普通元组遍历即可。
两个星号则收集任意数量的关键字参数,打包成字典:
代码 5-17:**kwargs收集关键字参数
def connect_db(**kwargs):
"""打印数据库连接参数"""
print(kwargs) # kwargs是一个字典
for key, value in kwargs.items():
print(f"{key} = {value}")
connect_db(host="localhost", port=3306, user="root")
print()
# 配置先存成字典,再用两个星号"拆包"传进去
db_config = {"host": "192.168.1.10", "port": 3307, "user": "admin"}
connect_db(**db_config)程序运行效果如下:
{'host': 'localhost', 'port': 3306, 'user': 'root'}
host = localhost
port = 3306
user = root
{'host': '192.168.1.10', 'port': 3307, 'user': 'admin'}
host = 192.168.1.10
port = 3307
user = admin第二个用法请重点留意:connect_db(**db_config)里的两个星号和定义端的两个星号正好是一对——定义端负责"收"(把关键字参数打包成字典),调用端负责"拆"(把字典摊开成一个个关键字参数)。第九章连接数据库时写的connect(**db_config),用的就是这一招,到时可以直接回来翻这段。
3.4 匿名函数lambda
有些函数体只有一句话,专门为def它开个"户口"显得小题大做。Python提供了匿名函数语法lambda:一句话定义、不取名字、用完即弃:
代码 5-18:lambda的基本语法
add = lambda a, b: a + b # 冒号左边是参数,右边是表达式(即返回值)
print(add(3, 5)) # 输出:8
# 上面的写法完全等价于:
def add2(a, b):
return a + blambda的格式是"lambda 形参: 表达式":冒号前写参数,冒号后写表达式,表达式的值就是返回值,连return都省了。它有两个限制:只能写一个表达式,不能写if语句、for循环这类多行逻辑。所以lambda只适合"小活儿",复杂逻辑老老实实用def。
lambda最重要的舞台是给sorted()、max()、min()当key参数。第四章学集合和字典排序时按下不表的"按成绩给学生排名",现在可以揭晓了:
代码 5-19:lambda配合sorted()、max()使用
students = [
{"name": "张三", "score": 85},
{"name": "李四", "score": 92},
{"name": "王五", "score": 78},
]
# key=lambda s: s["score"]:告诉sorted按每个学生的"score"排,而不是按字典排
ranked = sorted(students, key=lambda s: s["score"], reverse=True)
for i in range(len(ranked)):
print(f"第{i + 1}名:{ranked[i]['name']}({ranked[i]['score']}分)")
# max配key:直接挑出成绩最高的那个学生(字典)
top = max(students, key=lambda s: s["score"])
print(f"榜首:{top['name']}")程序运行效果如下:
第1名:李四(92分)
第2名:张三(85分)
第3名:王五(78分)
榜首:李四key参数接收一个"单参函数",sorted每处理一个元素就调用它一次,用它的返回值作为排序依据。lambda s: s["score"]读作:"给我一个学生s,我告诉你按他的score排"。不写key时,sorted面对一列表格字典根本不知道按什么排,会直接报错——这就是第四章跳过它的原因:得先学会函数,才讲得清key。
3.5 递归——函数调用自己
最后简单认识一种特殊的函数调用方式:递归,即函数在自己内部调用自己。计算阶乘是最经典的例子——5的阶乘5! = 5×4×3×2×1,它可以理解为5! = 5×4!,而4! = 4×3!……一路化小,直到1! = 1:
代码 5-20:用递归计算阶乘
def factorial(n):
"""计算n的阶乘"""
if n == 1: # 递归出口:问题小到不能再小,直接给答案
return 1
return n * factorial(n - 1) # 自己调用自己,问题规模缩小一号
print(factorial(5)) # 输出:120factorial(5)的执行过程是一条"去而复返"的链路:factorial(5)要等factorial(4)的结果,factorial(4)要等factorial(3)……一路问到factorial(1)拿到明确的答案1,再一层层乘着返回去:1→2→6→24→120。
递归必须具备两个要素:出口(n == 1时直接返回,不再继续调用)和规模缩小(每次调用n - 1)。漏掉出口,函数会无限调用自己,最终报RecursionError。客观地说,递归能解决的问题,循环大多也能解决,而且循环更省内存、更好调试;递归的价值在于某些"天然分形"的问题(如文件夹遍历、汉诺塔)用递归表达特别自然。初学阶段:能看懂、会跑通factorial即可,日常优先用循环。
【例题 5-3】函数版班级成绩管理
用函数重构第四章例题4-2的班级成绩管理:把"添加学生、查看全部、班级统计"拆分成三个函数,主程序只负责菜单循环和调度。每个学生的数据用"姓名: [语文, 数学, Python成绩]"的字典结构管理。
代码 5-21:函数版班级成绩管理小程序
def add_student(students, name, scores):
"""添加一名学生
参数students:花名册字典
参数name:姓名
参数scores:三门课成绩的列表[语文, 数学, Python]
"""
students[name] = scores # 可变对象作参数:直接写入花名册
def get_average(scores):
"""计算一名学生的平均分(保留1位小数)"""
return round(sum(scores) / len(scores), 1)
def show_all(students):
"""打印全体学生成绩"""
print("=" * 30)
print(f"共{len(students)}名学生")
for name, scores in students.items():
average = get_average(scores)
print(f"{name}:语文{scores[0]}、数学{scores[1]}、Python {scores[2]},平均分{average}")
print("=" * 30)
def statistics(students):
"""班级统计:总平均分与个人平均分最高的学生"""
all_scores = []
best_name = ""
best_average = 0
for name, scores in students.items():
all_scores += scores # 把每个人的三门成绩合并进总表
average = get_average(scores)
if average > best_average:
best_average = average
best_name = name
print(f"班级总平均分:{get_average(all_scores)}")
print(f"个人平均分最高:{best_name}({best_average}分)")
# 主程序:菜单循环,只负责"调度",具体活儿都交给函数
students = {}
print("===班级成绩管理===")
while True:
print("1.添加学生 2.查看全部 3.班级统计 0.退出")
choice = input("请选择:")
if choice == "1":
name = input("姓名:")
chinese = int(input("语文成绩:"))
math_score = int(input("数学成绩:"))
python_score = int(input("Python成绩:"))
add_student(students, name, [chinese, math_score, python_score])
print(f"已添加{name}")
elif choice == "2":
show_all(students)
elif choice == "3":
statistics(students)
elif choice == "0":
print("再见!")
break
else:
print("输入有误,请重新选择")程序运行效果如下(输入部分用【】标注):
===班级成绩管理===
1.添加学生 2.查看全部 3.班级统计 0.退出
请选择:【1】
姓名:【张三】
语文成绩:【85】
数学成绩:【92】
Python成绩:【78】
已添加张三
1.添加学生 2.查看全部 3.班级统计 0.退出
请选择:【1】
姓名:【李四】
语文成绩:【70】
数学成绩:【88】
Python成绩:【95】
已添加李四
1.添加学生 2.查看全部 3.班级统计 0.退出
请选择:【1】
姓名:【王五】
语文成绩:【90】
数学成绩:【76】
Python成绩:【82】
已添加王五
1.添加学生 2.查看全部 3.班级统计 0.退出
请选择:【2】
==============================
共3名学生
张三:语文85、数学92、Python 78,平均分85.0
李四:语文70、数学88、Python 95,平均分84.3
王五:语文90、数学76、Python 82,平均分82.7
==============================
1.添加学生 2.查看全部 3.班级统计 0.退出
请选择:【3】
班级总平均分:84.0
个人平均分最高:张三(85.0分)
1.添加学生 2.查看全部 3.班级统计 0.退出
请选择:【0】
再见!对比例题4-2,功能一样,但结构脱胎换骨:主程序只剩一个清晰的菜单循环,每个功能是一个独立函数,要加"删除学生"只需新写一个函数、菜单添一行——程序从此可以像搭积木一样生长。本章的函数、第四章的字典、第三章的列表与格式化,在这道题里全部会师。
至此,前五章的知识拼图已经完整:流程控制是"做事的顺序",四种容器是"装数据的器皿",函数是"可复用的零件"。下一章学习模块:把本章这些函数按用途分组、放进各自的.py文件,就成了别人也能import的模块——Python生态里那几十万个第三方库,本质上就是一个个写满了函数的文件。
