05 · 函数(Functions)

📅 预计 70 分钟 | ⭐ = 高频考点
✍️ 本讲配套练习:本地 python grader.py 05,或网页练习


5.1 函数定义与调用

把函数想成做菜的菜谱:菜谱躺在书架上不会自己做菜,只有你照着步骤动手(调用),才会端出菜。Python 里 def 就是在"写菜谱",用"函数名 + 括号"就是在"照着做一次"。

def add(a, b):        # def + 函数名 + 参数 + 冒号
    return a + b      # 返回结果

print(add(3, 4))      # 7

几个关键规则:

  • 函数体必须缩进,不缩进的代码不属于这个函数。
  • 没写 return 的函数,返回 None(考试超爱考)。
  • 一个函数只做一件事,名字要能"望文生义"。
def greet(name):
    print("你好", name)     # 只打印,没有 return

print(greet("小明"))         # 先打印「你好 小明」,再打印 None

小练习:猜猜 print(greet("小明")) 会输出几行?答案:两行——先执行函数里的 print,再打印函数的返回值 None

⚠️ 常见错误

  • 忘写冒号def add(a, b) 结尾漏了冒号,直接 SyntaxError
  • 缩进错位:混用 Tab 和空格会报 IndentationError: unexpected indent
  • 以为没 return 的函数有返回值:没有 return → 返回值是 None,别拿它继续做运算。

5.2 参数:四种类型 ⭐

参数就是菜谱上列的食材:菜谱写"放鸡蛋",但没说是哪个鸡蛋——真正做菜那一刻才把具体的蛋递进去。**形参(parameter)**是菜谱上的占位名,**实参(argument)**是调用时真正传入的值。

# 1) 位置参数 positional:按顺序一一对应
def f(a, b):
    return a - b

f(10, 3)     # 7,a=10、b=3(位置决定是谁)

# 2) 默认参数 default:必须放在最后 ⭐
def greet(name, greeting="你好"):
    return greeting + "," + name

greet("小明")            # '你好,小明'(用默认值)
greet("小明", "早上好")   # '早上好,小明'(覆盖默认值)

# 3) 关键字参数 keyword:调用时指名道姓,顺序随便
f(b=3, a=10)             # 7,等价于 f(10, 3)

# 4) 可变参数 *args / **kwargs ⭐
def total(*nums):            # * 把任意多个位置参数收成一个元组
    return sum(nums)

total(1, 2, 3)               # 6
total()                      # 0(一个都不传也行)

def info(**kw):              # ** 把任意多个关键字参数收成一个字典
    return kw

info(name="小明", age=20)    # {'name':'小明','age':20}

对比记忆:*args 收到的是元组(有序),**kwargs 收到的是字典(带名字)。名字本身不重要,*** 才关键。

⚠️ 常见错误

  • 默认参数位置放错def f(a, b=1, c) 是语法错误——默认参数必须都在无默认参数的后面
  • 默认参数用可变对象(列表/字典):默认值只在定义时创建一次,多次调用会"记住"上次改的结果,俗称"变参陷阱"。
def bad(items=[]):        # 危险!所有调用共享同一个列表
    items.append(1)
    return items

print(bad())              # [1]
print(bad())              # [1, 1] ← 上一次的结果还在

def good(items=None):     # 安全写法:用 None 占位,函数里再新建
    items = [] if items is None else items
    items.append(1)
    return items

5.3 返回值:函数做好的菜

return 就是出锅:菜做好端出来,后面的工序(代码)不再执行。return 还可以一次端出好几道菜——本质是把它们打包成一个元组。

def stats(nums):
    return sum(nums), len(nums)   # 返回一个元组 (和, 个数)

s, n = stats([1, 2, 3])           # 解包接收:s=6, n=3

提前 return 可以当"提前结束开关":

def check(score):
    if score < 60:
        return "挂科"      # 提前 return,直接结束函数
    return "通过"

print(check(50), check(90))   # 挂科 通过

⚠️ 常见错误

  • return 后面的代码不执行(不可达代码,笔试爱考):
def f():
    return 1
    print("这行永远打不出来")
  • 多返回值忘了解包r = stats([1, 2, 3]) 拿到的是元组 (6, 3),不是一个 6 一个 3
  • 分支漏 return:有的分支返回了值,有的分支忘了 return,那个分支会悄悄返回 None

5.4 作用域 scope ⭐

作用域就是变量的活动范围,好比房间:小房间里的东西在客厅看不见,客厅里的东西小房间里能看见。函数内是"小房间"(局部),函数外是"客厅"(全局)。

x = 10                    # 全局变量(客厅里的东西)

def f():
    x = 20                # 这是小房间里新建的变量,不影响客厅
    print(x)              # 20

f()
print(x)                  # 10,客厅没变
  • 函数内全局变量:直接用,读得到。
  • 函数内全局变量:必须 global 声明,否则 Python 会认为你只是新建了局部变量。
def g():
    global x
    x = 99                # 这才真的改了全局

g()
print(x)                  # 99

变量查找顺序:局部 → 外层 → 全局 → 内置,英文缩写 LEGB(Local → Enclosing → Global → Built-in)。

⚠️ 常见错误

  • 改全局忘了 global:函数里 x = 99 只是新建局部变量,全局还是原样(5.4 第一个例子的 20/10 就是这个坑)。
  • 先读后写同一个名字:函数里既有 print(x) 又有 x = 2,Python 看到有赋值就把 x 判为局部变量,前面的读就变成"读未赋值的局部",报 UnboundLocalError
x = 1
def bad():
    print(x)     # 报错 UnboundLocalError
    x = 2

5.5 lambda 匿名函数与高阶函数 ⭐

lambda没有名字的一次性函数,适合"用完就扔"的小逻辑。Python 里函数是"一等公民":能当参数传、能当返回值返回,这样的函数叫高阶函数

f = lambda x: x * 2        # 等价于 def f(x): return x*2
print(f(3))                # 6

# 常和排序一起用 ⭐
students = [("小明", 90), ("小红", 85)]
students.sort(key=lambda s: s[1])    # 按成绩升序

# map / filter / sorted 全家桶 ⭐
nums = [1, 2, 3, 4]
list(map(lambda x: x * 2, nums))            # [2, 4, 6, 8]
list(filter(lambda x: x % 2 == 0, nums))    # [2, 4]
sorted(nums, key=lambda x: -x)              # [4, 3, 2, 1] 降序

三个函数怎么记:

  • map(fn, 可迭代):把 fn 作用到每个元素上,映射出新的可迭代对象(要 list() 才看得到)。
  • filter(fn, 可迭代):留下 fn 结果为真的元素,过滤
  • sorted(可迭代, key=fn):按 fn 的返回值排序,返回新列表,原列表不动。

⚠️ 常见错误

  • map/filter 忘了转 list:直接打印出来是 <map object at 0x...>,不是 [2,4,6,8]
  • lambda 里写多条语句:lambda 只能写一个表达式if/else 只能用三元式 a if 条件 else b,不能写普通 if 块。
  • sorted 忘了 reverse:默认升序,要降序加 reverse=True,或给 key 取负数。

5.6 递归 recursion ⭐

递归是函数调用自己,像俄罗斯套娃:打开一个,里面还有更小的一个,直到最小的那个不用再拆。写递归只需两件事:终止条件(最小的娃)+ 递推关系(大娃怎么变成小娃)。

def fact(n):
    if n <= 1:
        return 1            # 终止条件:最小的娃
    return n * fact(n - 1)  # 递推关系:n! = n × (n-1)!

fact(5)   # 120

def fib(n):                 # 斐波那契数列
    if n <= 1:
        return n            # 终止条件
    return fib(n-1) + fib(n-2)   # 递推关系

fib(6)   # 8

脑内走查一遍 fact(3)3*fact(2)3*(2*fact(1))3*(2*1)6。先一路"往下拆",到底后再一路"往回算"。

⚠️ 常见错误

  • 漏掉终止条件 → 无限递归,报 RecursionError: maximum recursion depth exceeded
  • 终止条件写在递归调用之后:还没检查到就被不断调用,照样爆栈。
  • 递归太深:Python 默认递归上限约 1000 层(sys.setrecursionlimit 可调),题目给的数据一般不会触顶。

5.7 闭包:函数"记住"环境 ⭐

闭包 = 内层函数 + 外层函数的变量。外层函数返回内层函数,内层函数即使在外层调用结束后,仍"记得"外层那个局部变量——像带了个私人记事本。

def make_multiplier(n):
    def multiply(x):
        return x * n      # 用到了外层的 n
    return multiply       # 注意:返回的是函数,不是值

triple = make_multiplier(3)
print(triple(4), triple(10))   # 12 30

内层函数要修改外层变量,用 nonlocal(和全局用 global 一个道理)。

def make_counter(start=0):
    count = start
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter(10)
print(c(), c(), c())      # 11 12 13,count 被记住了

⚠️ 常见错误

  • 忘了 return 内层函数def make_multiplier(n): def multiply(x): ... 却没 return multiply,调用返回 None
  • 内层改外层变量没写 nonlocal:Python 当你在新建局部变量,要么报 UnboundLocalError,要么计数不生效。
  • 闭包延迟绑定:循环里造的多个闭包,最后都记住循环变量"最后的值",调用结果一样(需要默认参数或立即绑定来规避)。

5.8 装饰器思想:给函数穿外套 ⭐

装饰器就是接收函数、返回新函数的函数,等于在原函数外面"套一件外套"(加日志、改输出、计时等)。@装饰器名 写在函数定义上方,等价于 原函数 = 装饰器(原函数)

def uppercase(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()   # 先执行原函数,再把结果变大写
    return wrapper

@uppercase              # 等价于 greet = uppercase(greet)
def greet(word):
    return word

print(greet("hi"))      # HI

看懂这个例子就懂装饰器了:@uppercasegreet 不再是原来的 greet,而是被包了外套的"新 greet"——调用它先跑原逻辑,再套上 upper()

⚠️ 常见错误

  • 装饰器没 return 内层函数@uppercasegreet 变成 None,一调用就报 TypeError: 'NoneType' object is not callable
  • wrapper 没透传参数:必须写 wrapper(*args, **kwargs),才能把原函数的参数原样转交。
  • wrapper 里没 return fn(...):原函数的返回值被丢掉,外面拿到的是 None

5.9 文档字符串与类型提示

文档字符串 docstring """...""" 是函数开头的说明文字,用 函数.__doc__help(函数) 能读到——相当于菜谱封面的"菜品说明"。类型提示 type hint: 类型-> 类型 标注参数和返回值类型,是给人和 IDE 看的"说明书",Python 运行时不会强制校验

def add(a: int, b: int) -> int:
    """返回两个整数的和。"""
    return a + b

print(add(3, 4))        # 7
print(add.__doc__)      # 返回两个整数的和。

⚠️ 常见错误

  • docstring 写成普通注释# 说明 只给开发者看,"""说明""" 才是能被程序读到的文档。
  • 以为类型提示会报错add("3", "4") 不会因为类型不对而报错,提示只是"说明书"不是"检查员"。
  • docstring 里嵌套英文双引号:docstring 用 """ 包裹,里面再写英文 " 会让字符串提前结束,报语法错误;要用中文引号「」代替。

💡 记忆口诀

  • 参数四兄弟:位置按顺序,默认放最后,关键字点名,* 收元组 ** 收字典
  • 作用域:要改全局写 global,要改外层写 nonlocal
  • 递归两件套:有终止、有递推,缺一必爆栈
  • lambda 三连:map 映射、filter 过滤、sorted 排序靠 key

📌 双语术语表(本讲)

中文 English
形参 / 实参 parameter / argument
返回值 return value
作用域 scope
局部变量 local variable
全局变量 global variable
默认参数 default argument
位置参数 positional argument
关键字参数 keyword argument
可变参数 variable-length argument (*args / **kwargs)
匿名函数 lambda
高阶函数 higher-order function
闭包 closure
装饰器 decorator
递归 recursion
文档字符串 docstring
类型提示 type hint

⭐ 本讲考点清单

  1. return → 返回 Nonereturn 后的代码不执行
  2. 默认参数必须放最后;默认参数别用可变对象
  3. *args 收成元组、**kwargs 收成字典
  4. 改全局要 global,改外层要 nonlocal
  5. lambda + map / filter / sorted(key=) 三件套
  6. 递归两要素:终止条件 + 递推关系
  7. 多返回值本质是元组,用解包接收
  8. 闭包 = 内层函数 + 外层变量的记忆
  9. 装饰器 = 接收函数返回函数,@ 是语法糖
  10. docstring 与类型提示的作用(不强制校验)