1 *args 与 **kwargs —— 打包与拆包的对称魔法
是什么?为什么学?
*args 把「任意多个位置参数」打包成元组,**kwargs 把「任意多个关键字参数」打包成字典,它们让函数不再被参数个数限制。写工具库、配置类函数、装饰器(Day 20+)时几乎必用。
底层原理
调用 f(1, 2, 3) 时,Python 先把实参收集成「参数列表」再传给函数;def f(*args) 就是告诉 Python「把剩下的所有位置参数收集成一个元组塞给 args」。**kwargs 同理,收集 key=value 形式的实参打包成字典。反过来,调用时在列表前加 *(如 f(*[1,2,3]))、在字典前加 **(如 f(**d))则是拆包——把容器展开成多个独立实参。打包与拆包是一对对称操作。
生活类比
*args 像超市购物袋:你往袋子里装多少东西都行(位置参数),店员装袋后交给你一个袋子(元组),你回家慢慢清点。**kwargs 像填表:每一项都是「项目=内容」(如「姓名=小明」),表格自动把所有项目整理成一本册子(字典)。而拆包是反过来——把袋子里的东西一样样摆到柜台上(*),把册子里的条目一条条念出来(**)。
注意事项总结
1 *args 打包任意多个位置参数:
def my_sum(*args):
print("收到参数:", args) # args 是元组
return sum(args)
print(my_sum(1, 2, 3))
print(my_sum(4, 5, 6, 7, 8))
运行结果:
收到参数: (1, 2, 3)
6
收到参数: (4, 5, 6, 7, 8)
30
传 2 个、5 个、100 个都行——这是固定参数函数做不到的。
2 拆包——把列表 / 字典「展开」成独立实参:
def show(a, b, c):
print(a, b, c)
nums = [1, 2, 3]
show(*nums) # 列表加 *:展开成 show(1, 2, 3)
info = {"a": 10, "b": 20, "c": 30}
show(**info) # 字典加 **:展开成 show(a=10, b=20, c=30)
运行结果:
1 2 3
10 20 30
打包在定义处(def f(*args)),拆包在调用处(f(*lst)),一收一放,正好对称。
3 **普通参数 + *args 混用**:
def greet(title, *names):
print("尊敬的", title)
for name in names:
print("欢迎", name)
greet("嘉宾", "小明", "小红", "小刚")
运行结果:
尊敬的 嘉宾
欢迎 小明
欢迎 小红
欢迎 小刚
易错点
- 错误写法:
def f(*args, x)把普通参数写在 args 后面 → 问题:报SyntaxError,args 之后只能跟关键字参数 → 正确写法:*args写在普通参数后面:def f(x, *args)。 - 错误写法:调用
f(*nums)时列表元素个数和形参个数对不上 → 问题:报TypeError(多了或少了) → 正确写法:拆包前先确认个数匹配。 - 错误写法:
def f(**kwargs, x)→ 问题:报SyntaxError,**kwargs必须是最后一个参数 → 正确写法:**kwargs永远放参数列表最后。
记忆口诀:一颗星打包元组,两颗星打包字典;定义处收,调用处拆,一收一放对称玩。
2 lambda —— 一句话函数与「函数是一等公民」
是什么?为什么学?
lambda 是用一行表达式写完的匿名函数:lambda 参数: 返回值表达式。它适合「逻辑简单、只用一次」的场景,最常见的用法是给 sorted / map / filter 提供排序或处理规则。
底层原理
Python 里函数是一等公民(first-class citizen):函数和数字、字符串一样,可以赋值给变量、放进列表、作为参数传递、作为返回值。lambda 只是「快速创建一个函数对象」的语法糖——lambda x: x * 2 与 def 定义的函数在底层是同一类对象,区别只是 lambda 没有名字、只能写一个表达式、没有独立代码块。
术语小词典:一等公民(first-class citizen)就是「和普通数据平起平坐」:函数也能被存进变量、塞进容器、当参数传,想怎么用就怎么用。
生活类比
lambda 像临时叫的代驾:事情简单(就一句话)、只用这一次,没必要专门请个全职司机(def 定义、起名字、写文档)。工作复杂(多行逻辑、多次复用)时,还是得雇正式的「函数员工」——有名有姓、有独立代码块,可读性更好。
注意事项总结
1 lambda 与 def 的等价写法对比:
def double(x): # 传统写法:def + return
return x * 2
double2 = lambda x: x * 2 # lambda 写法:一行搞定
print(double(5)) # 10
print(double2(5)) # 10
print(type(double2)) # <class 'function'>,和 def 函数是同一类对象
运行结果:
10
10
<class 'function'>
lambda x: x * 2 里,冒号前是参数,冒号后是返回表达式(自带 return,不用写)。
2 函数是一等公民——函数可以放进列表 / 字典:
def add(a, b):
return a + b
def multiply(a, b):
return a * b
tools = [add, multiply] # 函数放进列表(注意:没写括号)
print(tools[0](3, 4)) # 取出 add 再调用
ops = {"+": add, "*": multiply} # 函数放进字典
print(ops["+"](10, 5))
运行结果:
7
15
tools = [add, multiply] 里写的是函数名字(没有括号)——存的是函数对象本身。函数和普通数据一样能被存放、传递,这是理解 map / filter / sorted 的基础。
3 lambda 实际最常配合排序用:
students = ["Tom", "alice", "Bob"]
print(sorted(students)) # 默认按字母排:['Bob', 'Tom', 'alice']
print(sorted(students, key=lambda s: s.lower())) # 忽略大小写排:['alice', 'Bob', 'Tom']
运行结果:
['Bob', 'Tom', 'alice']
['alice', 'Bob', 'Tom']
易错点
- 错误写法:
lambda x: x * 2后面加赋值或 if 语句 → 问题:报SyntaxError,lambda 只能写一个表达式 → 正确写法:复杂逻辑改用def。 - 错误写法:
tools = [add()]误加括号 → 问题:存进去的是调用结果而不是函数本身 → 正确写法:存函数不加括号:[add]。 - 错误写法:
sorted(words, lambda w: w.lower())忘了key=→ 问题:lambda 被当成容器,报TypeError→ 正确写法:必须写成key=lambda ...。
记忆口诀:lambda 一行成函数,冒号前面是参数,冒号后面是结果;复杂逻辑用 def,可读优先记心间。
3 map / filter —— 批量处理数据的迭代器
是什么?为什么学?
map(函数, 容器) 把函数逐个应用到容器的每个元素(转换);filter(函数, 容器) 把「函数返回 True」的元素保留下来(筛选)。它们是「对一堆数据批量操作」的标准工具,代码比 for 循环更短、意图更明确。
底层原理
map / filter 返回的不是列表,而是迭代器(iterator)——一种「按需产出、边取边算」的对象。它们不会一次性算完所有结果,而是每次 next() 时才取一个元素处理一个,所以处理 100 万个元素也只占固定内存。正因如此,print(map(...)) 只能看到 <map object ...>,必须用 list() 把它「拉完」转成列表才能看到全部结果。
术语小词典:迭代器(iterator)就是「按需产出数据的对象」——像自动售货机,按一下(next)出一样,不按不产,所以省内存。
生活类比
map 像流水线传送带:每个零件(元素)经过工位(函数)加工一下再出来——零件数量不变,只是个个都变了样。filter 像安检门:每个乘客(元素)过安检(函数判断),合格(True)放行、不合格(False)拦下——人数变少,但留下来的都达标。
注意事项总结
1 map 批量转换——把每个数字翻倍:
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
print(doubled)
运行结果:
[2, 4, 6, 8, 10]
元素个数不变(5 进 5 出),但每个值都变了——这就是「转换」。
2 filter 批量筛选——只保留偶数:
nums = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)
运行结果:
[2, 4, 6]
元素个数变少(6 进 3 出)——这就是「筛选」。
3 map + filter 搭配使用(先筛后平方):
nums = [1, 2, 3, 4, 5, 6]
result = list(map(lambda x: x * x, filter(lambda x: x % 2 == 0, nums)))
print(result)
运行结果:
[4, 16, 36]
易错点
- 错误写法:直接
print(map(...))→ 问题:打印出<map object at 0x...>,看不到数据 → 正确写法:外面套list():list(map(...))。 - 错误写法:filter 的 lambda 里写
x % 2(忘了 == 0) → 问题:返回的是 0/1 而非 True/False,偶数被错误筛掉 → 正确写法:写完整判断:x % 2 == 0。 - 错误写法:对字典用 map 没注意迭代的是键 → 问题:map 默认遍历的是字典的键 → 正确写法:想处理值:
map(func, d.values())。
记忆口诀:map 逐个换新颜,filter 合格才放行;迭代器按需算,套上 list 才见全。
4 sorted 的 key 参数 —— 自定义排序的灵魂
是什么?为什么学?
sorted(容器, key=函数) 中的 key 参数决定「按什么规则排序」:默认按元素本身的大小排,而 key 函数可以对每个元素「加工出一个比较依据」。按绝对值排、忽略大小写排、按字典某键排——全部靠 key 实现。
底层原理
sorted 的排序分两步:先对每个元素调用一次 key 函数得到「键值」,然后按键值大小对原元素排序(内置 TimSort 算法),最后返回新列表(原列表不被修改)。注意 key 只负责「算出比较依据」,真正排序的还是原元素——所以 sorted(students, key=lambda s: s["score"]) 返回的仍是完整字典,只是顺序变了。
生活类比
学校按成绩排名:默认排序像直接按「姓名的拼音字母」排;key 排序像按「身高」排——人还是那些人(元素不变),只是排队依据换成了别的东西(key 算出的键值)。想按哪个维度排,就告诉教练(key 函数)怎么量。
注意事项总结
1 key 按绝对值排序:
nums = [-5, 3, -1, 2]
print(sorted(nums)) # 默认按数值
print(sorted(nums, key=lambda x: abs(x))) # 按绝对值
运行结果:
[-5, -1, 2, 3]
[-1, 2, 3, -5]
默认排序按数值大小:-5 < -1 < 2 < 3;按绝对值排时,每个数先取绝对值再比较:1、2、3、5,对应原数是 -1、2、3、-5。key 只改变「比较依据」,结果里还是原来的数。
2 key 对字典列表按成绩排序(最常用场景):
students = [
{"name": "小明", "score": 92},
{"name": "小红", "score": 88},
{"name": "小刚", "score": 95},
]
top = sorted(students, key=lambda s: s["score"], reverse=True)
for s in top:
print(s["name"], s["score"])
运行结果:
小刚 95
小明 92
小红 88
key=lambda s: s["score"] 让排序依据是每个字典的 score 值;reverse=True 表示从高到低(默认从小到大)。字典列表排序是真实项目里的高频操作,这一行是标准答案。
易错点
- 错误写法:
sorted(students, key=s["score"])少了 lambda → 问题:直接报NameError,key 要的是一个函数 → 正确写法:key=lambda s: s["score"]。 - 错误写法:忘了
reverse=True想要从大到小 → 问题:得到的是从小到大 → 正确写法:降序加reverse=True。 - 错误写法:以为 sorted 会修改原列表 → 问题:原列表不变,返回新列表 → 正确写法:想原地排序用
列表.sort()。
记忆口诀:sorted 要排序,key 来定依据;函数算键值,元素不变序;降序加 reverse,新表不伤原。
5 递归 —— 自己调用自己的「栈」游戏
是什么?为什么学?
递归(recursion)是函数在函数体里调用自己,适合解决「大问题能拆成同构的小问题」的场景:阶乘、倒计时、目录遍历、树形结构(Day 20+)。理解递归,等于打开了「分而治之」的思维大门。
底层原理
每次函数调用,Python 都会在调用栈(call stack)上压入一个栈帧(记录参数、局部变量、返回位置)。递归调用自己时,栈帧一层层往上叠:factorial(5) → factorial(4) → …… 直到遇到基准条件(base case)不再调用,然后栈帧一层层「弹栈」返回结果。所以递归必须满足两条:①有出口(基准条件);②每次调用让问题变小——参数朝出口方向逼近。缺了出口,栈帧无限叠加,最终触发 RecursionError: maximum recursion depth exceeded(默认约 1000 层)。
生活类比
递归像俄罗斯套娃:打开最外面的大娃,里面是一个小一号的娃,再打开又是更小的……直到最小的那个(基准条件)——里面什么都没有了。整个过程「一层层打开(递),再一层层合上(归)」。如果厂家忘了做最小的娃(没有出口),你就永远拆不完。
注意事项总结
1 递归算阶乘——最经典的递归:
def factorial(n):
if n <= 1: # 出口:n 到 1 就停
return 1
return n * factorial(n - 1) # 递进:问题变小(n-1)
print(factorial(5))
运行结果:
120
执行过程:factorial(5) 算 5 * factorial(4);factorial(4) 又要算 4 * factorial(3)……一直压栈到 factorial(1) 返回 1,再一层层弹栈相乘:5×4×3×2×1 = 120。出口(n <= 1)保证递归能停。
2 递归求斐波那契数列(前 10 项):
def fib(n):
if n <= 1: # 出口:第 0、1 项直接返回
return n
return fib(n - 1) + fib(n - 2) # 递进:拆成两个更小的子问题
result = []
for i in range(10):
result.append(fib(i))
print(" ".join(str(x) for x in result))
运行结果:
0 1 1 2 3 5 8 13 21 34
斐波那契数列第 n 项 = 前两项之和(第 0 项是 0,第 1 项是 1)。fib(5) = fib(4) + fib(3)……这种朴素写法会重复计算很多次(性能差),以后学了「记忆化」再优化。
3 递归必须有两个部分——出口 + 递进(倒计时):
def count_down(n):
if n <= 0: # 1. 出口(基准条件):n 到 0 就停
print("发射!")
return
print(n) # 2. 递进:打印当前值
count_down(n - 1) # 然后调用自己,参数变小
count_down(3)
运行结果:
3
2
1
发射!
4 没有出口的递归(错误示范,千万不要运行):
# def forever():
# return forever() + 1
#
# print(forever())
# 上面这段没有「出口」,函数无限调用自己,栈帧无限叠加,
# 运行会报 RecursionError: maximum recursion depth exceeded。
# 不小心运行了无限递归,在终端按 Ctrl + C 强制终止即可。
易错点
- 错误写法:递归函数没有基准条件 → 问题:无限调用自己,报
RecursionError→ 正确写法:先写出口:if 条件: return 直接结果。 - 错误写法:基准条件写了但永远达不到(参数没变小) → 问题:同样 RecursionError → 正确写法:检查每次递归调用时参数是否朝出口逼近。
- 错误写法:用递归解决简单循环问题 → 问题:性能差、代码绕 → 正确写法:能用循环就用循环(更简单、更快);树形结构等场景才用递归。
记忆口诀:递归两件套:出口保停,变小逼近;去程递、回程归,栈满报错 RecursionError。
6 动手实践:排序与过滤工具
把今天学的知识组合起来,做一个工具库:能对数字列表排序(支持自定义规则)、筛选出符合条件的数、批量转换数据。
# 排序与过滤工具
import math
def sort_by_abs(nums):
"""按绝对值从小到大排序"""
return sorted(nums, key=lambda x: abs(x))
def sort_by_len(words):
"""按字符串长度排序"""
return sorted(words, key=lambda x: len(x))
def filter_by_condition(nums, func):
"""按传入的函数筛选(func 返回 True 的保留)"""
return list(filter(func, nums))
def double_all(nums):
"""把所有数字翻倍"""
return list(map(lambda x: x * 2, nums))
def sum_all(*args):
"""任意个数求和"""
return sum(args)
# 调用测试
print(sort_by_abs([-5, 3, -1, 2])) # [-1, 2, 3, -5]
print(sort_by_len(["abc", "a", "abcd", "ab"])) # ['a', 'ab', 'abc', 'abcd']
print(filter_by_condition(range(1, 11), lambda x: x % 3 == 0)) # [3, 6, 9]
print(double_all([1, 2, 3])) # [2, 4, 6]
print(sum_all(1, 2, 3, 4)) # 10
完成标准:每个函数输出与注释一致。进阶:用 lambda 给一组学生成绩字典按分数排序,如 students = [{"name": "小明", "score": 92}, {"name": "小红", "score": 88}],按 score 从高到低排。
今日总结
*args 打包位置参数成元组,**kwargs 打包关键字参数成字典;调用时 * / ** 还可以拆包展开
函数是一等公民:可以赋值、放进容器、作为参数传递——这是 map / filter / sorted 能工作的基础
lambda 适合一行简单函数:lambda 参数: 表达式;逻辑复杂时用 def 更清晰
map 逐个转换、filter 按条件筛选,返回迭代器,记得套 list() 查看结果
sorted 的 key 参数决定排序依据,reverse=True 降序;原列表不被修改
递归必须有出口(基准条件),且每次调用要让问题变小;递归有「去程」和「回程」两个阶段
能用循环优先用循环,递归留给树形遍历等天然递归的场景
完成排序与过滤工具,并尝试用 lambda 给字典列表按成绩排序
| 报错 | 原因 | 修复 |
|---|---|---|
TypeError: 'map' object is not subscriptable | 忘了把 map/filter 结果转成列表 | 外面套 list():list(map(...)) |
RecursionError: maximum recursion depth exceeded | 递归没有出口,无限调用自己 | 检查递归函数是否缺少基准条件,或条件永远不满足 |
TypeError: <lambda>() missing 1 required positional argument | lambda 参数个数和调用处不匹配 | 检查 lambda 写了几个参数、传了几个参数 |
SyntaxError: invalid syntax | lambda 后面写了多行语句或用了赋值 | lambda 只能写一个表达式,复杂逻辑改用 def |
TypeError: 'int' object is not iterable | 把单个数字传给了期待容器的函数 | 检查实参是列表/元组,而不是单个值 |