提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 函数
-
-
- 场景一:传"不可变对象"(类似值传递的错觉)
- 场景二:传"可变对象"(类似引用传递的错觉)
- [💥 终极测验:打破"引用传递"的错觉!](#💥 终极测验:打破“引用传递”的错觉!)
- 总结(背诵口诀)
-
- 输出结果:
-
-
- 防止函数修改列表
- 函数可使用的参数形式
- 第一个问题:为什么非默认参数必须放在前面?
- [第二个问题:为什么 `**` 参数是以字典导入,且必须放在最后?](#第二个问题:为什么
**参数是以字典导入,且必须放在最后?) -
- [1. 为什么它是以字典(Dictionary)的形式导入的?](#1. 为什么它是以字典(Dictionary)的形式导入的?)
- [2. 为什么它后面绝对不能再有其他参数了?](#2. 为什么它后面绝对不能再有其他参数了?)
- [🌟 终极总结:Python 函数参数的"黄金顺位"](#🌟 终极总结:Python 函数参数的“黄金顺位”)
- 解包传参
- 强制使用位置参数或关键字参数
- 变量的作用域
- 递归
-
- 匿名函数
- 第一层:如何读懂这段代码的执行流程?
- [第二层:为什么我们需要它?(干嘛不老老实实用 def?)](#第二层:为什么我们需要它?(干嘛不老老实实用 def?))
- [第三层:实战中的 3 大黄金使用场景](#第三层:实战中的 3 大黄金使用场景)
-
- [场景 1:配合排序函数 `sort()` 或 `sorted()`](#场景 1:配合排序函数
sort()或sorted()) - [场景 2:配合映射函数 `map()`](#场景 2:配合映射函数
map()) - [场景 3:配合过滤函数 `filter()`](#场景 3:配合过滤函数
filter())
- [场景 1:配合排序函数 `sort()` 或 `sorted()`](#场景 1:配合排序函数
-
- 总结
-
-
- [🌟 核心知识点高度总结(四大支柱)](#🌟 核心知识点高度总结(四大支柱))
- [⚠️ 危险区:极其容易犯错的 6 大暗坑(避坑指南)](#⚠️ 危险区:极其容易犯错的 6 大暗坑(避坑指南))
-
- [💣 暗坑 1:【全网最易错】使用"可变类型"作为默认参数](#💣 暗坑 1:【全网最易错】使用“可变类型”作为默认参数)
- [💣 暗坑 2:忘了 `map` 和 `filter` 是"惰性"的(生成器)](#💣 暗坑 2:忘了
map和filter是“惰性”的(生成器)) - [💣 暗坑 3:`+=` 与 `=` 的微妙区别(UnboundLocalError)](#💣 暗坑 3:
+=与=的微妙区别(UnboundLocalError)) - [💣 暗坑 4:作用域泄漏(分支和循环不产生作用域)](#💣 暗坑 4:作用域泄漏(分支和循环不产生作用域))
- [💣 暗坑 5:解包传参时字典的区别(`*` vs `**`)](#💣 暗坑 5:解包传参时字典的区别(
*vs**)) - [💣 暗坑 6:滥用 Lambda 函数](#💣 暗坑 6:滥用 Lambda 函数)
-
函数
Python 定义函数使用 def 关键字,一般格式如下:
python
def 函数名 (参数列表) :
函数体
[return]
函数规则
函数代码块以def关键词开头,后接函数标识符名称和圆括号 ()。
任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
函数的第一行语句可以选择性地使用文档字符串---用于存放函数说明。用三个引号括起来,单引号和双引号都可以。
函数参数后面以冒号结束。
函数体开始缩进。
return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。
注意:
函数必须先定义再调用
函数在定义的时候只是告诉解释器我定义了一个这样的函数,可以完成某些功能,但是这个时候函数还没有执行,需要调用函数后,才会执行。
形参和实参
在定义函数时,指定的参数称为形式参数,简称为形参(函数的提供者)
在调用函数时,给函数传递的参数称为实际参数,简称为实参(函数的调用者)
在定义函数时,形参没有分配存储空间,也没有值,相当于一个占位符;
在调用函数时, 会在栈区中给函数分配存储空间, 然后给形参/局部变量分配存储空间,传递的是实际的数据
当函数执行结束,函数所占的栈空间会被释放,函数的形参/局部变量也会被释放
Python函数的参数传递
传递的时候,个人认为就是传递一个地址过去,若是一个不可变类型,函数中对其进行了重新赋值,也就是这个变量重新指向了其他的位置,但是外面的a依然地址没有改变,若是一个可变类型,若是通过append等方式,传入的地址依然是不变的,依然是指向同一个区域,自然外面的也会同样的改变,但是若是使用的是"="也就是内部的可变类型的地址也会改变,自然不影响外面的哪个地址的内容了
场景一:传"不可变对象"(类似值传递的错觉)
不可变对象(如整数、字符串): 它们是"实心铁疙瘩",一旦造出来,绝对不允许内部发生改变。
【代码演示】
python
def change_num(n):
# 3. 试图修改 n 的值
n = 100
print(f"函数内部的 n: {n}")
# 1. 在内存造一个铁疙瘩 10,贴上标签 'a'
a = 10
# 2. 把 'a' 传给函数。此时,函数内部多了一张标签 'n',它也贴在铁疙瘩 10 上。
change_num(a)
# 4. 打印外面的 a
print(f"函数外部的 a: {a}")
【运行结果】
text
函数内部的 n: 100
函数外部的 a: 10
【底层画面拆解】:
为什么外面的 a 没变?
当在函数内部执行 n = 100 时,因为 10 是铁疙瘩(不可变),Python 无法把它敲成 100。
Python 的做法是:在内存里重新造一个铁疙瘩 100,然后把标签 n 从 10 上撕下来,贴到新造的 100 上。
而外面的标签 a 依然安安静静地贴在原来的 10 上。
👉 这就是为什么它"看起来"像 C++ 的值传递。
场景二:传"可变对象"(类似引用传递的错觉)
可变对象(如列表、字典): 它们是"敞口的纸箱子",你可以随时往里面扔东西或者拿东西,但箱子本身没换。
【代码演示】
python
def change_list(la):
# 3. 往箱子里扔新东西
la.append(99)
print(f"函数内部的 la: {la}")
# 1. 在内存造一个箱子 [1, 2],贴上标签 'my_list'
my_list = [1, 2]
# 2. 把标签传给函数。此时,标签 'la' 也贴在了同一个箱子上!
change_list(my_list)
# 4. 打印外面的 my_list
print(f"函数外部的 my_list: {my_list}")
【运行结果】
text
函数内部的 la: [1, 2, 99]
函数外部的 my_list: [1, 2, 99]
【底层画面拆解】:
为什么外面的 my_list 变了?
当在函数内部执行 la.append(99) 时,因为列表是纸箱子(可变),Python 直接把手伸进箱子里,扔了一个 99 进去。
注意!这里没有发生等号(=)的重新赋值 ,标签 la 没有被撕下来,它依然贴在这个箱子上。
等函数执行完,外面的人通过 my_list 标签往箱子里一看,发现里面多了一个 99!
👉 这就是为什么它"看起来"像 C++ 的引用传递。
💥 终极测验:打破"引用传递"的错觉!
既然你总结的最后一句说"严格意义上不能叫引用传递",那我们怎么证明它不是真正的引用传递呢?
看下面这个"偷天换日"的例子:
python
def reassign_list(la):
# 关键点:我们使用了等号 (=),对它进行了【重新赋值】,而不是 append 修改!
la = ["被", "掉", "包", "了"]
my_list = [1, 2]
reassign_list(my_list)
print(my_list) # 你猜打印什么?
【运行结果】
text
[1, 2]
【震撼原理解析】:
如果是真正的 C++ 引用传递,外面的 my_list 应该变成 ["被", "掉", "包", "了"] 才对!为什么没变?
因为在 Python 中,等号 = 永远意味着"撕下标签,贴到新对象上" 。
当执行 la = [...] 时,Python 造了一个新箱子,把 la 标签撕下来贴到了新箱子上。外面的 my_list 依然死死地贴在原来的 [1, 2] 箱子上。
总结(背诵口诀)
要彻底理解 Python 的传参,记住这两句话就够了:
- 传参就是"发一张一模一样的副卡(标签)"给函数。
- 函数里怎么用这张副卡决定了外面的结果:
- 如果函数用
=给副卡绑定了新对象(重新赋值),主卡不受任何影响。 - 如果函数用
.append()或[0]=等方法修改了副卡指向的对象内部(原地修改可变对象),主卡看到的内容也会同步改变,因为它们指的是同一个对象!
- 如果函数用
def multiply2(var1):
print("函数内var1 id:", id(var1))
var1 *= 2
print("var1 *= 2后,函数内var1 id:", id(var1))
var1 = var1 * 2
print("var1 = var1 * 2后,函数内var1 id:", id(var1))
list1 = [1, 2, 3]
print("list1 id:", id(list1))
multiply2(list1)
输出结果:
list1 id: 2302584035712
函数内var1 id: 2302584035712
var1 *= 2后,函数内var1 id: 2302584035712
var1 = var1 * 2后,函数内var1 id: 2302584033664
防止函数修改列表
有时要函数对列表进行处理,又不希望函数修改原列表,可以使用 copy.deepcopy()。
python
import copy
def multiply2(var1):
var1[3].append(400)
print("函数内处理后:", var1)
list1 = [1, 2, 3, [100, 200, 300]]
print("函数外处理前:", list1)
multiply2(copy.deepcopy(list1))
print("函数外处理后:", list1)
**var1 = 2与var1 = var1 * 2的区别:
var1 = 2使用原地址。
var1 = var1 * 2开辟了新的空间。
函数可使用的参数形式
必须参数
调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。为此,最简单的关联方式是基于位置把每个相应位置的实参和形参相关联,调用时的数量必须和声明时的一样。
python
'''
该案例演示了函数位置实参
'''
def func(a, b, c):
print(a, b, c)
func(1, 2, 3) # 1 2 3
关键字参数
函数调用使用关键字参数来确定每个变量传入的参数值,使用关键字参数允许函数调用时参数的顺序与声明时不一致。
python
'''
该案例演示了函数调用时的关键字参数
'''
def printInfo(name,age) :
print("姓名:",name)
print("年龄:",age)
# Python解释器可以通过age和name这样的关键字去和形参进行匹配
printInfo(name = "zhangsan",age = 18)
printInfo(age = 18,name = "zhangsan")
默认值参数
定义函数时,可给每个形参指定默认值。在调用函数时,给形参提供了实参则使用指定的实参值,否则使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。注意:非默认参数必须放在默认参数之前。
第一个问题:为什么非默认参数必须放在前面?
规则重温:
def func(a, b=1):👉 合法 (非默认的a在前)def func(a=1, b):👉 报错 SyntaxError (默认的a在前)
【原理解析:解决"分赃不均"的歧义】
在 Python 中,当我们调用函数传入参数时,最基本的方式是**"按位置顺序(从左到右)"**对号入座。
假设 Python 允许你写 def func(a=1, b):,现在你调用了这个函数,只传了一个数字:
python
func(10) # 致命问题来了:这个 10 到底该给谁?
此时,Python 解释器的大脑短路了,它面临两个无法抉择的选项:
- 选项 A(按顺序塞): 把
10塞给第一个参数a。那么a变成了10。但是等一下!b是没有默认值的,b现在空着,程序没法运行啊! - 选项 B(跳过默认值): 既然
a已经有默认值1了,那我把10直接跳过去塞给b吧。这样a=1,b=10。
Python 设计者龟叔(Guido)认为:计算机绝对不能去"猜"程序员的意图。一旦有歧义,就必须禁止这种语法!
【正确的逻辑】
如果你强制规定:没有默认值的必须排在前面 (def func(a, b=1):)。
当你调用 func(10) 时,逻辑瞬间变得无比清晰:
- Python 从左往右发牌。第一个牌
10毫无争议地发给没有默认值的a。 - 手里的牌发完了,去看后面的参数。发现
b没分到牌,但b自己带有默认值1,那就用1。 - 完美闭环,没有任何歧义!
python
'''
该案例演示了函数调用时的默认参数
'''
def printInfo(name,age = 20) :
print("姓名:",name)
print("年龄:",age)
printInfo("zhangsan")
printInfo("lisi",30)
printInfo(age = 40,name = "wangwu")
不定长参数
参数的个数是不确定的。
python
def 函数名([普通参数,] *var_args_tuple ):
函数体
python
'''
该案例演示了函数调用时的不定长参数
'''
def printInfo(num,*vartuple):
print(num)#70
print(vartuple)#(60, 50)
printInfo(70,60,50)
print("-" * 20)
如果不定长的参数后面还有参数,必须通过关键字参数传参
python
def printInfo1(num1,*vartuple,num) :
print(num)
print(num1)
print(vartuple)
printInfo1(10,20,num = 40)
print("-" * 20)
# 如果没有给不定长的参数传参,那么得到的是空元组
printInfo1(70,num = 60)

加了星号 * 的参数会以元组(tuple)的形式导入,存放所有未命名的变量参数。
如果形参中出现了不定长参数,那么在调用函数的时候,先通过位置进行必须参数的匹配,然后不定长参数后面的参数必须通过关键字参数匹配
如果不定长的参数后面还有参数,必须通过关键字参数传参
还有一种就是参数带两个星号 **的可变长参数,基本语法如下:
python
def 函数名([普通参数,] **var_args_dict ):
函数体
加了两个星号 ** 的参数会以字典的形式导入,后面就不能再有其他参数了
python
'''
该案例演示了函数调用时的不定长参数
'''
def printInfo(num,**vardict):
print(num)
print(vardict)
# return
printInfo(10,key1 = 20,key2 = 30)
printInfo(10,a = 20,b = 30)
第二个问题:为什么 ** 参数是以字典导入,且必须放在最后?
这里的 ** 参数,在 Python 界有一个约定俗成的名字叫 **kwargs (Keyword Arguments,关键字参数)。
1. 为什么它是以字典(Dictionary)的形式导入的?
当你使用 ** 时,你接收的不是单个的值,而是带有名字的键值对 (形如 key=value 的形式)。
【代码演示】
python
def print_info(**kwargs):
print(kwargs)
# 调用时,你必须指定参数的名字
print_info(name="Tom", age=18, city="Beijing")
在这个调用中,name、age、city 就是键(Key) ,而 "Tom"、18、"Beijing" 就是值(Value) 。
在 Python 所有的基本数据结构(列表、元组、集合、字典)中,只有字典天生就是用来存储这种"键值对映射"的 。所以,Python 顺理成章地把收集到的所有关键字参数,打包成了一个极其方便查询的字典 {'name': 'Tom', 'age': 18, 'city': 'Beijing'} 递给函数内部去用。
2. 为什么它后面绝对不能再有其他参数了?
把 **kwargs 想象成一个**"贪婪的黑洞"或者"超级吸尘器"**。
它的职责是:把调用函数时,所有没有被前面参数认领的 key=value 格式的参数,统统吸进自己的字典里。
假设 Python 允许 **kwargs 后面还有参数:
python
# 假设这是允许的(实际上会报错)
def test_func(**kwargs, last_arg):
pass
# 现在你调用它:
test_func(a=1, b=2, last_arg=99)
【逻辑灾难再次降临】:
当 Python 看到 a=1 和 b=2 时,**kwargs 黑洞毫不客气地把它们吸进去了。
接着,Python 看到了 last_arg=99。此时黑洞 **kwargs 会说:"这也是一个 key=value 格式的参数,按照我的职责,我必须把它也吸进去!"
如果黑洞把它吸走了,那么后面的 last_arg 参数就永远拿不到值了。
为了防止这种"抢劫"行为导致后面的参数饿死,Python 立下了不可逾越的铁律:
既然 **kwargs 会无条件吞噬剩下所有的关键字参数,那它就必须作为参数列表的"终结者",站在全场的最后一位!
🌟 终极总结:Python 函数参数的"黄金顺位"
为了保证这套"发牌逻辑"完美运行不冲突,Python 规定了所有参数类型必须严格遵守以下排队顺序(就像国王巡游的仪仗队):
1. 必选参数 (必须给值的,如 a)
2. 默认参数 (有备胎的,如 b=1)
3. *args (收集无家可归的位置参数 ,打包成元组 )
4. **kwargs (收集无家可归的关键字参数 ,打包成字典,全场最后压轴!)
完美合体代码展示:
python
# 严格按照 1 -> 2 -> 3 -> 4 的顺序排队
def master_func(a, b=1, *args, **kwargs):
print(f"a = {a}")
print(f"b = {b}")
print(f"args = {args}") # 元组
print(f"kwargs = {kwargs}") # 字典
# 疯狂调用测试
master_func(10, 20, 30, 40, x=99, y=100)
Python 的发牌过程(毫无歧义):
10发给a。20发给b(覆盖了默认值 1)。- 剩下的
30, 40是纯位置参数,被*args打包成元组(30, 40)。 - 最后的
x=99, y=100是关键字参数,被黑洞**kwargs吸走,打包成字典{'x': 99, 'y': 100}。
这就是 Python 语法设计中最令人拍案叫绝的严谨之美!
解包传参
若函数的形参是定长参数,可以通过 * 和 ** 对列表、元组、字典等解包传参。
python
def func(a, b, c):
return a + b + c
tuple11 = (1, 2, 3)
print(func(*tuple11))
# 字典中key的名称和参数名必须一致
dict1 = {"a": 1, "b": 2, "c": 3}
print(func(**dict1))
强制使用位置参数或关键字参数
/ 前的参数必须使用位置传参,* 后的参数必须用关键字传参。
python
def f(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)
f(1, 2, 3, d=4, e=5, f=6)
return 语句可以返回多个值,多个值会放在一个元组中。
python
def f(a, b, c):
return a, b, c, [a, b, c]
print(f(1, 2, 3)) # (1, 2, 3, [1, 2, 3])
变量的作用域
Python中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。变量的作用域决定了哪一部分程序可以访问哪个变量,Python的作用域一共有4种,分别是:
L (Local) 局部作用域
E (Enclosing)嵌套作用域 闭包函数外的函数中
G (Global) 全局作用域
B (Built-in) 内建作用域
以 L --> E --> G -->B 的规则查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。以下案例演示各种作用域类型。
python
'''
该案例演示了变量的作用域,下面这个就是要给简化的闭包
'''
a = int(2.9) # 内建作用域 (Python本身提供的,在所有位置都可以访问)
b = 0 # 全局作用域
def outer():
c = 1 # 嵌套作用域
def inner():
d = 2 #局部作用域
print(d,c,b,a)
return inner
in_func=outer()
in_func()
Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问,如下代码:
python
# 分支,循环不会引入新的作用域
num = 2
if num > 1:
msg = "helloWorld"
print(msg)
def test():
msg_test = "welcome"
print(msg_test)
实例中 msg 变量定义在 if 语句块中,但外部还是可以访问的。如果将 msg 定义在函数中,则它就是局部变量,外部不能访问:\从报错的信息上看,说明了 msg_inner 未定义,无法使用,因为它是局部变量,只有在函数内可以使用。
全局变量和局部变量
定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。
python
'''
该案例演示了全局变量和局部变量
'''
sum = 0 # 这是一个全局变量
def add(num1,num2) :
sum = num1 + num2 # 这是一个局部变量
print("函数内局部变量的值:",sum,id(sum))
return sum
add(10,20)
print(num1) # num1访问不到
print("函数外全局变量:",sum,id(sum))
global关键字
使用global修改全局变量
定义了一个全局变量,如何在函数内对其进行修改?
直接在函数内修改,通过var1 += 200修改(依然是原位置)。会报错
当你在函数内部对一个变量进行赋值操作时,Python 默认会把这个变量当作局部变量,即便全局作用域中已经存在同名变量。
python
var1 = 100
def function_a():
var1 += 200 # 将var1当做局部变量处理,+=得先定义变量
function_a() # 报错
通过var1 = 200修改。全局变量var1的值并没被修改,仍是100。我们只是在function_a函数中新定义了一个局部变量var1并将其赋值为200。
python
var1 = 100
def function_a():
var1 = 200
print("var1:", var1)
print(var1) # 100
function_a() # var1: 200
print(var1) # 100
在函数内使用 global 声明全局变量,函数内使用 global 声明全局变量后,可以修改全局变量。
python
def function_a():
global var1
var1 = 200
print("var1:", var1)
var1 = 100
print(var1) # 100
function_a() # var1: 200
print(var1) # 200
修改可变类型的全局变量
当全局变量为可变类型时,函数内不使用 global 声明,也可以对其进行修改。
python
def function_a():
list1[0] = -1000
print("list1:", list1)
list1 = [1, 2, 3]
print(list1) # [1, 2, 3]
function_a() # list1: [-1000, 2, 3]
print(list1) # [-1000, 2, 3]
在函数中不使用 global 声明全局变量时不能修改全局变量的本质是不能修改全局变量的指向,即不能将全局变量指向新的数据。
不可变类型的全局变量其指向的数据不能修改,所以不使用 global 无法修改全局变量。
可变类型的全局变量其指向的数据可以修改,所以不使用 global 也可修改全局变量。
nonlocal关键字
nonlocal 也用作内部作用域修改外部作用域的变量的场景,不过此时外部作用域不是全局作用域而是嵌套作用域。
python
def function_outer():
var1 = 1
print(var1)
def function_inner():
nonlocal var1
var1 = 200
function_inner()
print(var1)
function_outer() # var1: 1 -> 200
递归
递归一种是逻辑思想,将一个大工作分为逐渐减小的小工作,比如说一个和尚要搬50块石头,他想,只要先搬走49块,那剩下的一块就能搬完了,然后考虑那49块,只要先搬走48块,那剩下的一块就能搬完了......,递归是一种思想,只不过在程序中,就是依靠函数嵌套这个特性来实现了,递归调用就是在函数体中又调用了函数本身.,在定义递归函数的时候,主要确定两点,确定它们之间的规律,确定递归结束的条件,
python
'''
该案例演示了求整数的阶乘
5! = 5 * 4 * 3 * 2 *1
'''
# 不使用递归的方式
def get_factorial(num):
res = 1 # 用于存放积
for n in range(1,num+1):
res *= n
return res
print(get_factorial(5))
print("-"*20)
def get_factorial2(n):
return n * get_factorial2(n - 1) if n > 1 else 1
print(get_factorial2(5)) # 120
匿名函数
Python使用 lambda 来定义匿名函数,所谓匿名,指其不用 def 的标准形式定义函数。
lambda 参数列表: 表达式
lambda 只是一个表达式,函数体比def简单很多。
lambda的主体是一个表达式,而不是一个代码块,所以仅仅能在lambda表达式中封装有限的逻辑进去。
lambda函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
使用普通函数传参
python
def operator(a, b):
return a + b
def function(a, b, operator):
return operator(a, b)
print(function(1, 2, operator))
使用匿名函数传参
python
def function(a, b, operator):
return operator(a, b)
print(function(1, 2, lambda x, y: x + y))
你的笔记对 lambda (匿名函数)的总结非常到位,尤其是这句核心定义:"lambda 只是一个表达式,不是代码块,只能封装有限的逻辑"。
你给出的这两个例子(普通函数传参 vs 匿名函数传参)简直是教科书级别的对比!
针对你的问题:"如何来使用这个匿名函数?"
其实你已经在第二个例子中展示了它最核心、最优雅的使用方式。但为了让你彻底掌握它的实战威力,我将从**"为什么需要它"、 "它的执行流程",以及"实战中的 3 大黄金场景"**来为你彻底拆解。
第一层:如何读懂这段代码的执行流程?
在你的例子中:
python
def function(a, b, operator):
return operator(a, b)
# 魔法发生在这里:
print(function(1, 2, lambda x, y: x + y))
【执行动作拆解】:
- Python 执行
function(1, 2, ...)时,准备把三个参数传递进去。 - 第一个参数
a接收到了数字1。 - 第二个参数
b接收到了数字2。 - 第三个参数
operator接收到了一段用lambda现写的"微型机器"(函数对象):lambda x, y: x + y。- 你可以把它翻译为:"我是一个没有名字的函数,给我两个东西(命名为
x和y),我会把它们加起来并还给你。"
- 你可以把它翻译为:"我是一个没有名字的函数,给我两个东西(命名为
- 在
function内部,执行return operator(a, b):- 此时的
operator就是刚才传进来的那个"微型机器"。 - 把
a(即 1) 和b(即 2) 塞进这台机器。 - 机器内部执行
1 + 2,算出3。 function把这个3最终return出来,被外面的print打印。
- 此时的
第二层:为什么我们需要它?(干嘛不老老实实用 def?)
你对比一下你的第一段代码(用 def 定义 operator)和第二段代码(用 lambda)。
def 的痛点:
很多时候,我们需要的这个操作(比如两数相加)极其简单,而且可能这辈子在整个项目里只会用这一次 。
如果你用 def,你需要:
- 在外面专门找个地方写三行代码定义它。
- 还得绞尽脑汁给它起个名字(起名字是程序员最头疼的事)。
- 代码被割裂了,看
function调用的地方,还要往回翻去找operator到底干了什么。
lambda 的爽点:即用即抛,代码连贯!
我直接在调用 function 的小括号里,随手捏一个没有名字的函数塞进去。它就像一块"一次性创可贴",用完就扔,既不用起名,也不会污染全局环境。
第三层:实战中的 3 大黄金使用场景
在真实的 Python 开发中,你绝大多数情况下会在以下三个场景看到 lambda 大显神威:
场景 1:配合排序函数 sort() 或 sorted()
sorted(iterable, key=None, reverse=False)
iterable:可迭代对象(列表、元组、字符串、字典等)
key:排序依据的函数(可选)
reverse:是否降序排序,默认 False(升序)
当你有一个字典列表,你想按照里面每个人的"年龄"来排序。普通的 sort() 会罢工,因为它不知道比较字典的哪个键。
python
students = [
{"name": "Tom", "age": 20},
{"name": "Jerry", "age": 18},
{"name": "Alice", "age": 22}
]
# 告诉 sorted 函数:请用每个人字典里的 "age" 键对应的值作为排序依据!
# lambda stu: stu["age"] 意思是:给我一个学生(stu),我揪出他的年龄(stu["age"])给你比大小。
sorted_students = sorted(students, key=lambda stu: stu["age"])
print(sorted_students)
# 输出: [{'name': 'Jerry', 'age': 18}, {'name': 'Tom', 'age': 20}, {'name': 'Alice', 'age': 22}]
场景 2:配合映射函数 map()
map(function, iterable, ...)
function:对每个元素执行的函数
iterable:一个或多个可迭代对象
返回值: 返回一个迭代器,包含对每个元素应用 function 后的结果
python
nums = [1, 2, 3, 4]
# map(函数, 列表) 会把列表里的东西挨个塞进函数处理
# lambda x: x ** 2 意思是:给我一个数字 x,我还你它的平方。
squared_nums = list(map(lambda x: x ** 2, nums))
print(squared_nums)
# 输出: [1, 4, 9, 16]
场景 3:配合过滤函数 filter()
filter() - 过滤函数
filter(function, iterable)
function:判断函数,返回 True 或 False
iterable:可迭代对象
返回值: 返回一个迭代器,包含所有使 function 返回 True 的元素
python
nums = [1, 2, 3, 4, 5, 6]
# filter(函数, 列表) 会留下那些让函数返回 True 的元素
# lambda x: x % 2 == 0 意思是:给我一个数 x,如果是偶数我还你 True,否则还 False。
even_nums = list(filter(lambda x: x % 2 == 0, nums))
print(even_nums)
# 输出: [2, 4, 6]
总结
🌟 核心知识点高度总结(四大支柱)
这篇笔记其实就是撑起 Python 函数体系的四大支柱:
- 参数传递规则: 规定了函数如何接收外界的数据。核心铁律是参数排队顺序 :
必选 -> 默认 -> *args -> **kwargs。 - 底层内存机制(传值 vs 传引用): Python 只有"传对象引用"。核心区别在于传进来的对象是可变(列表/字典)还是不可变(数字/字符串) ,以及你在函数里用的是
=(换标签) 还是.append()(改内容)。 - 作用域(LEGB 规则): 规定了变量的"生存范围"。核心法则是:局部可以读 全局,但要想**修改(重新赋值)**全局,必须用
global(或用nonlocal修改嵌套作用域)。 - 函数式编程:
lambda提供了一次性微型函数,配合map(转换)、filter(筛选)、sorted(排序)三剑客,能写出极其优雅且高效的代码。
⚠️ 危险区:极其容易犯错的 6 大暗坑(避坑指南)
💣 暗坑 1:【全网最易错】使用"可变类型"作为默认参数
这是这篇笔记里唯一漏掉、也是面试100%会考的终极陷阱!
千万、千万不要把列表或字典作为函数的默认参数!
❌ 错误代码:
python
def add_item(item, my_list=[]):
my_list.append(item)
return my_list
print(add_item(1)) # 预期: [1], 实际: [1]
print(add_item(2)) # 预期: [2], 实际: [1, 2] <-- 见鬼了!1是从哪来的?
原理解析: 默认参数在函数定义的那一刻 就已经在内存中被创建了。因为列表是可变的,第二次调用时,用的是上次残留的同一个列表!
✅ 正确写法:
python
def add_item(item, my_list=None):
if my_list is None:
my_list = [] # 每次调用时才创建新列表
my_list.append(item)
return my_list
💣 暗坑 2:忘了 map 和 filter 是"惰性"的(生成器)
在 Python 3 中,map 和 filter 返回的不是列表 ,而是迭代器。
❌ 错误代码:
python
nums = [1, 2, 3]
res = map(lambda x: x*2, nums)
print(res) # 实际输出: <map object at 0x000002A...> (看不到数据!)
✅ 正确写法:
一定要记得在外面套一层 list(),把它"榨干"!
res = list(map(lambda x: x*2, nums))
💣 暗坑 3:+= 与 = 的微妙区别(UnboundLocalError)
在函数内部,如果你没有声明 global 就想修改全局变量:
❌ 错误代码:
python
count = 10
def add():
count += 1 # 报错:局部变量 count 在赋值前被引用
原理解析: count += 1 等价于 count = count + 1。只要 Python 看到 = 号在左边,就会把 count 当作局部变量,但等号右边的 count 却还没被赋值,所以死循环报错。
✅ 解决办法: 加上 global count。
💣 暗坑 4:作用域泄漏(分支和循环不产生作用域)
如果你习惯了 C++ 或 Java,你会认为在 if 或 for 里面定义的变量,外面是拿不到的。但 Python 会"泄漏"。
⚠️ 注意代码:
python
for i in range(5):
pass
# 循环结束后,i 并没死!
print(i) # 输出 4,依然可以访问!
这其实不算错,但极其容易导致命名冲突。要牢记:Python 只有模块、类和函数才会产生新的作用域(房间)。
💣 暗坑 5:解包传参时字典的区别(* vs **)
在使用解包传参给函数时,传字典极其容易搞错。
python
def func(a, b):
print(a, b)
my_dict = {"a": 1, "b": 2}
# ❌ 错觉:以为传的是值
func(*my_dict) # 输出: a b (只传进去了字典的 Key!)
# ✅ 正确:传键值对必须用双星号
func(**my_dict) # 输出: 1 2 (传进去了字典的 Value)
💣 暗坑 6:滥用 Lambda 函数
新手学了 lambda 觉得很酷,什么都想用它写,导致代码极其难以阅读。
❌ 糟糕的代码(虽然能跑,但会被同事打):
python
# 判断奇偶并返回不同字符串
res = (lambda x: "偶数" if x % 2 == 0 else "奇数")(10)
✅ 铁律: lambda 只适合写**"一行就能看懂的极其简单的逻辑"**。如果里面出现了嵌套、或者复杂的 if-else,请立刻换回原生的 def 定义普通函数。