🎯 学习目标
- 理解函数的作用,会用
def定义与调用函数 - 彻底掌握五类参数:位置、关键字、默认、
*args、**kwargs - 了解仅限位置参数
/与仅限关键字参数* - 理解 LEGB 作用域规则与
global/nonlocal - 掌握
lambda及map/filter/sorted高阶函数 - 会写递归、文档字符串与类型提示,避开可变默认参数陷阱
1定义与调用
函数是一段可重复使用、完成特定功能的代码块。使用函数的好处:复用代码、命名语义、隔离复杂度、便于测试。
def greet(name): # def 关键字 + 函数名 + (参数):
"""向某人打招呼。""" # 文档字符串(函数说明书)
return "你好," + name # return 返回结果
msg = greet("小明") # 调用:实参 "小明" 传给形参 name
print(msg) # 你好,小明
print(greet) # <function greet at ...> 函数也是对象
- 形参:定义时括号里的变量(如
name);实参:调用时传入的具体值。 - 没有
return的函数默认返回None。 - 函数必须先定义后调用;函数体在调用时才执行。
# 先定义
def f():
print("定义时不执行,调用时才执行")
f() # 这里才真正执行函数体
2参数体系(本章重点)
2.1 位置参数与关键字参数
def introduce(name, age, city):
return f"{name},{age}岁,来自{city}"
# 位置传参:按顺序对应
introduce("小明", 20, "北京")
# 关键字传参:按名字对应,顺序可随意,可读性好
introduce(age=20, city="北京", name="小明")
# 混用:位置参数必须在关键字参数前面
introduce("小明", city="北京", age=20)
2.2 默认参数
def power(x, n=2): # n 有默认值,调用时可省略
return x ** n
power(3) # 9 n 取默认值 2
power(3, 3) # 27 显式覆盖
power(2, n=4) # 16
# 有默认值的参数必须放在无默认值参数之后
# def f(a=1, b): ... # 语法错误!
2.3 可变参数 *args:接收任意多个位置参数
def my_sum(*nums): # * 把多余的位置参数打包成【元组】
print(nums) # (1, 2, 3, 4)
return sum(nums)
my_sum(1, 2, 3, 4) # 10
my_sum() # () 空元组,也合法
# 反过来:解包调用——把列表/元组拆成多个参数
def add(a, b, c):
return a + b + c
args = [1, 2, 3]
add(*args) # 6
2.4 关键字可变参数 **kwargs
def show_info(name, **extra): # ** 把多余关键字参数打包成【字典】
print(name, extra)
show_info("小明", age=20, city="北京", hobby="读书")
# 小明 {'age': 20, 'city': '北京', 'hobby': '读书'}
# 解包字典调用
opts = {"b": 2, "c": 3}
add(1, **opts) # 6(键名必须与形参对应)
典型的"万能函数签名":
def func(*args, **kwargs):
print("任意位置参数:", args)
print("任意关键字参数:", kwargs)
func(1, 2, x=3, y=4)
2.5 参数顺序与特殊分隔符
# 完整顺序:位置参数 → / → *args → 仅限关键字参数 → **kwargs
def f(a, b, /, c, d, *, e, **kwargs):
# a,b 只能按位置传(/ 之前)
# e 只能按关键字传(* 之后)
return (a, b, c, d, e)
f(1, 2, 3, d=4, e=5) # 合法
# f(a=1, b=2, ...) # 报错:a、b 不允许关键字传
# f(1,2,3,4,5) # 报错:e 必须写 e=5
# 仅关键字参数非常适合布尔开关,强制调用者写清楚含义
def split_text(text, *, strip=True, lower=False):
...
split_text(" Hi ", strip=True) # 一眼看出开关意图
2.6 可变默认参数陷阱(面试必考)
默认值在函数定义时只创建一次!若默认值是可变对象(列表/字典),多次调用会共享同一个对象,造成诡异的状态累积:
# 错误写法
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] ← 不是预期的 ['b']!
# 正确写法:用 None 作哨兵
def add_item(item, bucket=None):
if bucket is None:
bucket = [] # 每次调用都新建
bucket.append(item)
return bucket
3返回值
def stats(data):
return min(data), max(data), sum(data) / len(data)
# 多个值用逗号分隔 → 自动打包成元组
lo, hi, avg = stats([80, 90, 100]) # 解包接收
print(lo, hi, avg) # 80 100 90.0
def early_return(x):
if x < 0:
return None # 卫语句提前返回,减少嵌套
return x * 2
def no_return():
print("只是做点事")
print(no_return()) # None(默认返回值)
4作用域:变量在哪里可见
Python 按 LEGB 顺序查找变量:Local(函数内)→ Enclosing(外层嵌套函数)→ Global(模块级)→ Built-in(内置,如 print/len)。
x = "global" # 全局变量
def outer():
x = "enclosing" # 外层函数局部变量
def inner():
x = "local" # 内层局部变量
print(x) # local:最近优先
inner()
print(x) # enclosing
outer()
print(x) # global
global 与 nonlocal
count = 0
def visit():
global count # 声明:我要修改的是【全局】的 count
count += 1
visit(); visit()
print(count) # 2
def counter_maker():
n = 0
def add():
nonlocal n # 声明:修改【外层函数】的 n(不是新建局部变量)
n += 1
return n
return add
c = counter_maker()
print(c(), c(), c()) # 1 2 3 —— 闭包计数器(第12章详讲)
函数内读取全局变量不需要声明;但只要函数内出现赋值,该变量就被视为局部变量。函数内修改全局/外层变量需 global/nonlocal。工程上应尽量少用 global,优先通过参数传入、return 返回。
5lambda 匿名函数
lambda 用来创建一句话的小型匿名函数,语法:lambda 参数: 表达式,表达式的值就是返回值(不能写语句)。
# 普通函数 vs lambda
def add(a, b):
return a + b
add2 = lambda a, b: a + b
print(add2(2, 3)) # 5
# 最常见用途:作为 sorted/max/min 的 key
students = [("张三", 88), ("李四", 76), ("王五", 95)]
sorted(students, key=lambda s: s[1]) # 按分数排
max(students, key=lambda s: s[1]) # ('王五',95)
words = ["banana", "fig", "apple"]
sorted(words, key=lambda w: (len(w), w)) # 先按长度再按字母
# 字典按值排序
freq = {"a": 5, "b": 2, "c": 8}
sorted(freq.items(), key=lambda kv: kv[1], reverse=True)
lambda 只适合短小、即用即弃的逻辑。超过一个表达式或需要复用时,老老实实写 def,可读性更重要。
6高阶函数:函数作为参数
函数在 Python 中是"一等公民",可以当作参数传递、作为返回值。
nums = [1, 2, 3, 4]
# map:对每个元素做变换(现代 Python 更推荐列表推导式)
list(map(lambda x: x ** 2, nums)) # [1,4,9,16]
[x ** 2 for x in nums] # 等价推导式,更易读
# filter:保留满足条件的元素
list(filter(lambda x: x % 2 == 0, nums)) # [2,4]
[x for x in nums if x % 2 == 0] # 等价推导式
# reduce:把序列"累积"成一个值(需导入)
from functools import reduce
reduce(lambda acc, x: acc + x, nums, 0) # 10 累加
reduce(lambda acc, x: acc * x, nums, 1) # 24 累乘
# 自定义高阶函数:把操作当作参数
def apply_twice(f, x):
return f(f(x))
print(apply_twice(lambda n: n + 1, 10)) # 12
7递归:自己调用自己
递归必须有两个要素:基线条件(何时停止)与递归关系(如何缩小问题)。
# 阶乘:n! = n × (n-1)!,0!=1
def factorial(n):
if n <= 1: # 基线条件
return 1
return n * factorial(n - 1) # 递归调用,规模减1
print(factorial(5)) # 120
# 斐波那契数列(朴素递归有大量重复计算)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print([fib(i) for i in range(10)]) # [0,1,1,2,3,5,8,13,21,34]
# 目录遍历、JSON 解析、树结构是递归的天然场景
Python 默认递归深度限制约 1000(sys.getrecursionlimit()),过深会 RecursionError;朴素 fib 递归时间复杂度 O(2ⁿ),实际项目用循环或缓存(functools.lru_cache,第 14 章讲)。
8文档字符串与类型提示
def divide(a: float, b: float) -> float:
"""计算 a 除以 b。
Args:
a: 被除数
b: 除数,不能为 0
Returns:
商(浮点数)
Raises:
ZeroDivisionError: 当 b 为 0 时
"""
return a / b
# help(divide) 可查看文档;类型提示只是"建议",解释器不强制
divide(10, 3)
# 常用类型提示
from typing import List, Dict, Optional, Union, Callable
def process(items: List[str],
config: Dict[str, int],
sep: Optional[str] = None) -> str:
return (sep or ",").join(items)
# Python 3.10+ 可用内置写法:
# list[str]、dict[str,int]、str | None、X | Y
类型提示不会被运行时强制检查,但配合编辑器(Pylance)能实现自动补全与提前查错,在中大型项目与团队协作中强烈推荐。
9综合案例:文本处理工具函数集
def clean_text(text: str) -> str:
"""去除首尾空白并转小写。"""
return text.strip().lower()
def tokenize(text: str, sep: str = " ") -> list:
"""按分隔符切词并过滤空串。"""
return [w for w in text.split(sep) if w]
def word_frequency(words: list) -> dict:
"""统计词频。"""
freq = {}
for w in words:
freq[w] = freq.get(w, 0) + 1
return freq
raw = " Python is Easy Python is Fun "
words = tokenize(clean_text(raw))
print(words) # ['python','is','easy','python','is','fun']
print(word_frequency(words)) # {'python':2,'is':2,'easy':1,'fun':1}
10本章小结与练习
📌 关键结论
*args收元组、**kwargs收字典;解包方向相反(*列表、**字典)。- 默认参数必须不可变,可变默认值用
None哨兵解决。 - 作用域遵循 LEGB;函数内改全局/外层变量要
global/nonlocal,但应尽量避免。 - lambda + key 是排序/查找的黄金搭档;复杂逻辑用 def。
- 递归要有基线条件,警惕深度与重复计算。
练习1:定义函数 calc(a, b, op="+"),支持 +、-、*、/ 四种运算并返回结果。
用默认参数 + if 分支(或字典映射 op 到 lambda):
ops={'+':lambda a,b:a+b,'-':lambda a,b:a-b,'*':lambda a,b:a*b,'/':lambda a,b:a/b}; return ops[op](a,b),除法前判断 b 是否为 0。练习2:写函数接收任意个数字,返回它们的最大值(不使用 max 函数)。
def my_max(*nums):
m = nums[0]
for n in nums[1:]:
if n > m: m = n
return m 关键是 *nums 收集成元组,再遍历比较。可加 if not nums 处理空参数。练习3:为什么默认参数写成 def f(data=[]) 很危险?正确写法是什么?
默认值在定义时只创建一次,多次调用不传参时共享同一个列表,append 的结果会累积。正确写法 data=None,函数体内 if data is None: data = [],保证每次调用独立新建。