课程章节

实验3.函数与模块综合实验

函数与模块综合实验

本实验把函数定义与调用、参数形态、返回值、Lambda、高阶函数、递归、简单装饰器整合成一个“成绩工具箱”,强调“先拆函数,再组装流程”。

实验目标

  1. 会把重复逻辑抽成函数,用默认参数和关键字参数降低调用成本。
  2. 会使用 *args 接收可变数量成绩,并正确返回统计结果(元组/字典)。
  3. 会用 lambda + sorted / filter 做轻量数据处理。
  4. 会写带递归出口的递归函数(阶乘、斐波那契)。
  5. 会写一个最简单的装饰器,给函数统一加日志。

模块划分建议

虽然本实验写在单个文件中演示,但请按“模块”的思维组织代码,将来可直接拆到 score_tools.py:

函数 职责
is_valid_score 校验单科成绩是否在 0~100
average 对可变参数求平均
grade_of 百分制转等级
stat_scores 返回最高/最低/平均/及格人数
top_n 返回分数最高的前 N 名
factorial 递归求阶乘(演示递归)
log_call 装饰器:打印“调用了谁”

参考实现

"""
成绩工具箱
覆盖:函数参数、返回值、Lambda、递归、装饰器
"""

# ---------- 装饰器 ----------
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f'[日志] 调用 {func.__name__}{args}')
        result = func(*args, **kwargs)
        return result
    return wrapper


# ---------- 基础校验与计算 ----------
def is_valid_score(score, full_mark=100):
    """判断成绩是否有效;full_mark 可按满分调整。"""
    return isinstance(score, (int, float)) and 0 <= score <= full_mark


@log_call
def average(*scores):
    """接收 0 个或多个成绩,返回平均分;没有成绩时返回 0。"""
    if not scores:
        return 0
    return sum(scores) / len(scores)


def grade_of(score):
    if score >= 90:
        return 'A'
    if score >= 80:
        return 'B'
    if score >= 70:
        return 'C'
    if score >= 60:
        return 'D'
    return 'F'


def stat_scores(scores):
    """一次性返回多种统计信息,调用方用键取用。"""
    if not scores:
        return {
            'count': 0,
            'max': None,
            'min': None,
            'average': 0,
            'pass_count': 0,
        }
    return {
        'count': len(scores),
        'max': max(scores),
        'min': min(scores),
        'average': average(*scores),
        'pass_count': sum(1 for s in scores if s >= 60),
    }


def top_n(students, n=3, key=None):
    """students: 列表,元素为 (name, score) 元组。"""
    if key is None:
        key = lambda item: item[1]
    return sorted(students, key=key, reverse=True)[:n]


# ---------- 递归 ----------
def factorial(n):
    if n < 0:
        raise ValueError('n 必须是非负整数')
    if n == 0 or n == 1:
        return 1
    return n * factorial(n - 1)


def fib(n):
    if n <= 0:
        return 0
    if n == 1:
        return 1
    return fib(n - 1) + fib(n - 2)


# ---------- 组装流程 ----------
def main():
    raw_scores = [95, 82, 67, 45, 78, 90]
    valid_scores = [s for s in raw_scores if is_valid_score(s)]
    print('有效成绩:', valid_scores)
    print('平均分:', round(average(*valid_scores), 2))
    print('等级:', [grade_of(s) for s in valid_scores])

    info = stat_scores(valid_scores)
    print('统计:', info)

    students = [
        ('张三', 95),
        ('李四', 82),
        ('王五', 67),
        ('赵六', 45),
        ('钱七', 78),
    ]
    print('前三名:', top_n(students, n=3))

    # lambda + filter:筛出及格
    passed = list(filter(lambda x: x[1] >= 60, students))
    print('及格名单:', passed)

    print('5! =', factorial(5))
    print('fib(8) =', fib(8))


if __name__ == '__main__':
    main()

预期输出

有效成绩: [95, 82, 67, 45, 78, 90]
[日志] 调用 average(95, 82, 67, 45, 78, 90)
平均分: 76.17
等级: ['A', 'B', 'D', 'F', 'C', 'A']
[日志] 调用 average(95, 82, 67, 45, 78, 90)
统计:{'count': 6, 'max': 95, 'min': 45, 'average': 76.166..., 'pass_count': 5}
前三名: [('张三', 95), ('李四', 82), ('钱七', 78)]
及格名单: [('张三', 95), ('李四', 82), ('王五', 67), ('钱七', 78)]
5! = 120
fib(8) = 21

说明:stat_scores 内部会再次调用被装饰的 average,因此日志会出现两次。

扩展挑战

  1. 把工具函数拆到 score_tools.py,主程序用 from score_tools import average 导入,体会“模块管理函数”。
  2. 给 top_n 增加关键字参数 reverse=False 时求最低分前 N 名。
  3. 把 log_call 增强为可记录耗时(提示:time.perf_counter())。

知识点总结

  1. 定义与调用:def 定义,圆括号调用;函数名见名知意;用文档字符串说明“自变量—因变量”关系。
  2. 参数形态:位置参数按顺序传入;默认参数 full_mark=100、n=3 简化常见调用;关键字参数可读性更好;*scores 聚合成元组,average(*valid_scores) 是把列表打散再传入。
  3. 返回值:单值用 return 表达式;多种信息用字典返回,避免调用方依赖脆弱的下标;没有成绩时返回合理默认值而不是让除零发生。
  4. 高阶函数与 Lambda:sorted(key=...)、filter 都接受函数;lambda item: item[1] 适合一行完成的简单规则,复杂逻辑仍应用 def。
  5. 递归:必须有递归出口(n==0 or n==1);每层把问题变小(n * factorial(n-1));非法输入用 raise 明确报错。
  6. 装饰器:log_call 返回 wrapper,从而在不改原函数源码的前提下增加日志;理解“函数可以当参数、当返回值”是进阶关键。
  7. 程序入口:if __name__ == '__main__': 保证被导入时不自动跑演示逻辑,这是模块化的重要习惯。
  8. 易错点:忘记 return 得到 None;把可变默认参数写成 def f(x=[]) 的隐患;递归缺少出口导致栈溢出;装饰器忘记写成 return wrapper 或调用时多写一层括号。
  9. 学习重点:能主动把实验1里的“一大段菜单代码”拆成职责单一的函数,并保证调用关系清晰、返回值稳定。