= 榨汁机(苹果) 苹果汁
7 函数
- 递归和函数式编程是进阶内容,看进度。
- 匿名函数可以介绍。
在本章中,你将学习到:
- 什么是函数? 理解函数的概念、作用以及它如何帮助我们编写更高效、更易读的代码。
- 如何定义和调用函数? 掌握
def
关键字、函数名、参数、函数体和return
语句的基本语法。 - 参数的多种玩法: 学习位置参数、关键字参数、默认参数,以及如何处理不定数量的参数 (
*args
和**kwargs
)。 - 变量在函数中的“地盘”——作用域: 理解局部变量、全局变量以及LEGB规则。
- 函数与数据类型的互动: 了解当可变与不可变类型作为参数传递给函数时会发生什么。
- (进阶) 递归的初步认识: 函数如何调用自身来解决特定问题。
- (进阶) 函数式编程的魅力: 初探
map
、filter
、lambda
表达式等强大的工具,以及高阶函数和闭包的概念。
通过学习本章,你将能够将复杂的任务分解为一个个小巧而强大的函数,让你的代码更具结构化、模块化,也为后续更复杂的数据分析任务打下坚实的基础。让我们开始探索函数的奇妙世界吧!
首先,Python中的函数基本上可以类比我们熟悉的数学函数,比如正弦函数:
\[ sin(\pi) = 0 \]
但更广泛地理解,可以认为:把任何东西(参数
)扔给函数,函数返回某个结果(返回值
)给你:
例如:我们把 \(\pi\) 扔给正弦函数 \(sin\) ,会得到 \(0\) 。
例如:把榨汁机看成一个函数,那我们把苹果扔进榨汁机,就能得到苹果汁。
其中
- 榨汁机:函数
- 苹果:参数(你要丢给函数的东西)
- 苹果汁:返回值(函数丢给你的东西)
- (进阶概念)副作用:除了返回值,函数还影响了一些其他东西。(后面会提到)
写成代码,大概是:
现实的例子,我们面前已经用过的,比如求List的长度:
= [1,2,3,4,5]
a = len(a)
a_size print(a_size)
5
看a_size = len(a)
这个代码
len()
:函数名len(a)
:其中a
是参数,即“你提供给这个函数的东西”a_size = len(a)
:会返回a
的长度(元素的数量),你把这个结果放在变量a_size
之中
一般而言,写函数我们往往希望达到:
- 代码复用 (Reusability):避免重复编写相同的代码块。
- 模块化 (Modularity):将复杂问题分解为更小、更易于管理的部分。
- 可读性 (Readability):结构清晰,易于理解代码的逻辑和意图。
- 可维护性 (Maintainability):修改或调试时,只需关注特定函数。
比如在数据分析中,你可能需要多次对不同的数据集进行同样的清洗操作(如去除缺失值、转换数据类型),这时把清洗步骤封装成一个函数,就可以反复使用。
7.1 定义一个函数
在Python中
- 定义一个函数的关键字是
def
, def
后,是函数名,小括号内是参数(这个函数要接收的东西),最后是冒号- 函数体,要有“1个缩进”
- 返回关键词是
return
。如果函数执行完都没有遇到return
,则返回None
- 注意:这个截图中的代码有一个错误。
函数命名的约定:
- 推荐使用
snake_case
(小写字母和下划线) 命名风格 - 避免和内置的名称重名(如前面截图中定义的
max()
函数)。回忆我们前面说的,变量名(和函数名)只是一个标签,避免你定义的函数顶替掉内置的函数。
7.1.1 例:加法函数
举一个最简单的例子,我们要写一个加法函数,其作用就是把2个变量相加。或者说,你给这个函数“传递2个参数”,这个函数会返回他们的和。
#%% 定义一个加法函数
def add(x, y):
= x + y
z return z
逐行解释一下
第一行: def add(x, y):
- 定义一个函数,由
def
开头 - 接着是你给这个函数起的名字
add
- 名字后面是小括号,里面包含了这个函数的“参数”。这里是2个参数,命名为
x
和y
。显然,这就是你调用这个函数的时候,要给函数的东西。 - 和
if
、for
语言等一样,最后是冒号,不要忘记。
注意:关于参数,2个参数的名字你可以自己定义,不一定是x
和y
。这2个参数的名字,如同一切变量名一样,最终会”指向”2个值,所以你在函数的内部,就可以用这2个名字,来引用这2个值
第二行:z = x + y
- 注意,前面有“1个”缩进(Tab),指示这个语句比函数本身低一级
z = x + y
,一目了然。需要说明的是,函数体内的2个参数,在函数体内部已经有所指代,所以不会出现x和y不存在之类的错误。
第三行:return z
- 前面还是“1个”缩进,这是和
z = x + y
同一个层级的代码。 - 返回使用
return
语句。这里把和z
,返回给上一层。
定义好之后,我们可以像使用任何函数一样调用它
我们来一个很熟悉的代码。
= 1
a = 2
b = add(a, b)
c print(c)
3
7.1.2 Docstring 与类型标注(推荐)
良好的文档字符串与类型标注有助于阅读、IDE 辅助与静态检查。
def add(x: int, y: int) -> int:
"""返回两个整数的和。
参数
x, y: 两个整数
返回
int: x + y
"""
return x + y
print(add(1, 2))
help(add)
3
Help on function add in module __main__:
add(x: int, y: int) -> int
返回两个整数的和。
参数
x, y: 两个整数
返回
int: x + y
7.1.3 例:求两个数的最大值
思路:
- 如果a > b,则返回a。
- 否则返回b。(此时a <= b,显然两者相等时,返回b也没错)
#%% 定义一个函数,接收两个参数,返回其最大值
def my_max(a, b):
if a > b:
return a # 返回a
else:
return b # 返回b
print(my_max(3,5))
5
7.1.4 例:计算圆的面积
另一个例子,已知:圆的半径是\(r\),我们要计算圆的面积,公式就是\(y = \pi r^2\)。
我们准备写一个函数来做这件事:
- 这个函数会根据我们提供的半径\(r\),返回一个圆的面积给我们。
- 显然,这个函数必然是接受
r
作为参数 - 我们做的,是算出面积,然后返回一下
def calc_area(r):
return 3.14159 * (r ** 2)
= 5
x = calc_area(x)
area
print(f"半径为{x}的圆,其面积为{area:0.2f}")
半径为5的圆,其面积为78.54
7.1.5 不返回值
若函数不需要返回任何值,不写return
语句,或者return
语句后不返回任何值
#%% 这2种都是可以的
def hello():
print("hello") # 不写return
def hello2():
print("hello")
return # 写return但不包括返回值
hello() hello2()
hello
hello
7.1.6 什么也不做的函数
有时候需要先占用函数名,但是函数的内容想以后再写,此时可以写一个空函数。函数体内只需要使用pass
语句。
特别地,没有return
语句(例如现在这种情况),或者return
语句不返回值的时候,函数会返回一个None
。
def do_nothing():
pass
print(do_nothing())
None
7.1.7 小练习
结合前面的学习内容:
- 实现一个函数
my_count()
,计算并返回一个列表的元素数量,但不要使用自带的len()
函数。
print(my_count([2,3,4,5])) # 应该输出 4
- 实现一个函数
my_max_len()
,找到一个字符串列表中最长的字符串,但不要使用自带的max()
函数。
print(my_max_len(['a','bbb','cc'])) # 应该输出 'bbb'
- 实现一个函数
find_min_max()
,一个查找列表中最大和最小值,并返回一个元组.
print(find_min_max([1, 5, 3, 9, 2, 7])) # 应该输出 (1, 9)
print(find_min_max([-5, 0, 10, -10, 5])) # 应该输出 (-10, 10)
7.2 参数
7.2.1 使用参数名
把参数传递给函数,可以按参数的顺序,也可以使用参数的名称
7.2.2 例:使用参数名称传递参数
def print_info(name, age):
print("姓名: ", name)
print("年龄: ", age)
'Alex',20) # 按参数顺序:姓名,年龄
print_info(print ("------------------------")
= 21, name = "Bob") # 按参数名,此时不用考虑参数的顺序 print_info(age
姓名: Alex
年龄: 20
------------------------
姓名: Bob
年龄: 21
7.2.3 默认参数
定义函数的时候,可以给某些参数定义一个默认值。
默认参数必须定义在最后。
7.2.4 例:乘方函数(n次方)
def power(x, n=2): # n有默认值
return x ** n
print(power(5)) # 调用函数不传递n的值,使用默认值,结果为25
print(power(3, 3)) # 调用函数传递n的值,使用传递值,结果为27
25
27
7.2.5 参数进阶:位置专用与关键字专用
可以通过在函数签名中使用 /
与 *
控制调用方式:
def f(a, /, b, *, c):
# a 只能用位置传参;c 只能用关键字传参
return a + b + c
1, 2, c=3) # 合法
f(# f(a=1, b=2, c=3) # TypeError: a 不能用关键字传参
# f(1, 2, 3) # TypeError: c 必须使用关键字传参
6
7.2.5.1 可变默认参数陷阱
不要把列表、字典等可变对象作为默认参数,它们会在多次调用间共享状态:
def append_bad(x, acc=[]): # 不推荐
acc.append(x)return acc
print(append_bad(1)) # [1]
print(append_bad(2)) # [1, 2] 共享了上次的结果
[1]
[1, 2]
正确写法:用 None
作为占位,在函数内创建新对象。
def append_ok(x, acc=None):
= [] if acc is None else acc
acc
acc.append(x)return acc
7.2.6 打包(packing):处理不定数量的参数
打包位置参数
函数可以接受任意多个参数,例如求最大值的函数max()
print(max(1,6,2))
print(max(4,1,5,2))
6
5
你要写一个这样的函数,那么可创建一个新的参数,前面带一个*
号。那么不在参数列表里的参数,会组成一个tuple,并且绑定带这个带*
号的变量名。
# 第一个参数是class_id,从第二个参数起,不定数量个参数,都会组成一个tuple,并命名为`students`(没有*)
def print_students(class_id, *students):
"""打印班级号,和同学的姓名"""
print("班级:",class_id)
print("学生包括:", students)
# 也可以逐一打印
print("学生包括:")
for s in students:
print (s)
5 ,"Alex","Bob","clare") print_students(
班级: 5
学生包括: ('Alex', 'Bob', 'clare')
学生包括:
Alex
Bob
clare
实际上,如果你有可变参数的函数,如print_students
,但是你拿到的数据是一个List,怎么办?
土办法,把列表元素一个个拿出来,传给函数
= ["Alex","Bob","clare"] # 这已经是一个list
students_list
5, students_list[0],students_list[1],students_list[2]) print_students(
班级: 5
学生包括: ('Alex', 'Bob', 'clare')
学生包括:
Alex
Bob
clare
当然,也简单的办法,变量在传递给函数时,前面加个*,python会自动帮你拆开
= ["Alex","Bob","Clare"] # 这已经是一个list
students_list
5, *students_list) # 自动把students_list这个List,拆成多个元素 print_students(
班级: 5
学生包括: ('Alex', 'Bob', 'Clare')
学生包括:
Alex
Bob
Clare
打包关键字参数
类似地,也允许函数接受任意数量的关键字参数(按参数名称传递),这些参数会被收集到一个字典中。
def print_info_flexible(name, **other_info):
print(f"姓名: {name}")
for key, value in other_info.items():
print(f"{key}: {value}")
"Alice", age=30, city="New York", occupation="Engineer")
print_info_flexible(# 姓名: Alice
# age: 30
# city: New York
# occupation: Engineer
姓名: Alice
age: 30
city: New York
occupation: Engineer
7.2.7 小练习
- 利用不定数量参数,写一个函数
my_sum()
,可以求任意个变量的和。效果应该如下:
my_sum(1,2,3,4) == 10
my_sum(3,4,5) == 12
7.2.8 拆包(unpacking):把数据结构拆散传给参数
你定义了一个函数,其中每一个参数都有明确的含义,而函数的参数保存在一个List、Tuple里(比如第一个元素对应第一个参数,第二个元素对应第二个参数等等)或者Dict里(key就对应参数名),把这些数据结构中的元素,当参数传给函数。
- tuple或者list前面加
*
- dict前加
**
称之为拆包(unpacking)
例:
def add(x, y):
print(f'x是{x},y是{y}')
return x + y
# 土办法
= dict(x=1,y=2)
params print(add(params['x'],params['y']))
# 拆包
= dict(y=2,x=1) # 注:参数名一一对应,而非顺序。
params print(add(**params))
#
= [1,2] # 第一个元素对应第一个参数
params print(add(*params))
= (2,1) # 第一个元素对应第一个参数
params print(add(*params))
# 当然,这样也是一样的
print(add(*[1,2]))
x是1,y是2
3
x是1,y是2
3
x是1,y是2
3
x是2,y是1
3
x是1,y是2
3
7.2.9 多返回值与解包
Python 函数可以一次返回多个值(本质是返回一个元组),可在调用处解包:
def split_pair(x: int) -> tuple[int, int]:
return x, x**2
= split_pair(3)
a, b print(a, b)
# 解包长度不匹配会抛出 ValueError
# a, b, c = split_pair(3) # ValueError: not enough values to unpack
3 9
- 注意和上一节的不同:定义函数的时候参数名是否带有星号?
7.3 参数作为可变类型和不可变类型
前面提到,多个变量名指向同一个可变对象(例如List,Dict),针对其中一个变量的修改,会引起所有指向同一个数据的变量都发生改变。
把List等,作为参数传递给一个函数,也有类似的后果。因此:
- 当不可变类型(如数字、字符串、元组)作为参数传递时,函数内部对参数的重新赋值不会影响到函数外部的原始变量,因为这实际上是在函数内部创建了一个新的局部变量。
- 当可变类型(如列表、字典)作为参数传递时,函数内部对该参数内容的修改(例如 list.append() 或修改字典的键值对)会影响到函数外部的原始变量,因为函数内外的变量名指向的是内存中的同一个对象。如果想避免这种情况,应在函数内部创建该对象的副本(如使用 list.copy() 或 dict.copy())再进行操作。
下例中,
- 把a_list作为参数x,转递给函数
do_change(x)
,那么在函数内部,变量x和外部a_list指向的是同一个数据,或者说,x成了a_list的别名。 - 把x作为函数的返回值,返回到外层,并赋予b_list,那么b_list, x, a_list,三个名字,实际上都指向了内存中的同一块数据。
- 当然,x只在函数体内可见。
= list('apple') # a_list -> ['a', 'p', 'p', 'l', 'e']
a_list
def do_change(x):
0] = 'B'
x[return x
= do_change(a_list) # 把a_list作为参数,则函数的参数 x -> a_list
b_list
print(b_list)
print(a_list)
['B', 'p', 'p', 'l', 'e']
['B', 'p', 'p', 'l', 'e']
同意,如果你的设计意图并非“原地修改”,原变量必须保持不变,则可以把x拷贝一份。
= list('apple') # a_list -> ['a', 'p', 'p', 'l', 'e']
a_list
def do_change(x):
= x.copy() # 把x拷贝一份,对y进行修改,那么x就保持不变了
y 0] = 'B'
y[return y
= do_change(a_list) # 把a_list作为参数,则函数的参数 x -> a_list
b_list
print(b_list)
print(a_list)
['B', 'p', 'p', 'l', 'e']
['a', 'p', 'p', 'l', 'e']
若传递的参数是不可变类型,如int,str等,则对参数的修改会自动引发拷贝,没有上述问题。前面已经说过了。
7.4 在推导式中使用函数
回忆列表推导式的一般形式:选出符合条件codition的i,然后对i进行某个操作do_something(i),最后把结果集合成一个列表
[do_something(i) for i in a_list if condition(i)]
其中我们要对i进行的操作,可以是一个函数(推荐写出清晰的文档字符串与类型标注,便于后续维护与 IDE 辅助)。
比如,我们要把一个列表中的值进行一个很复杂的运行,可以首先把这个运算写成一个函数。这里以乘以2为例。
def do_double(x: int) -> int:
"""这是一个超级复杂的运算,算法有一万行(示例)"""
return x * 2
3) do_double(
6
那么,用这个函数来处理列表中的每一个元素,列表推导式会每一个i交给函数,然后把函数的返回值组合成新的列表。
= [1,2,3,4,5]
a
for i in a] [do_double(i)
[2, 4, 6, 8, 10]
7.4.1 小练习
- 写一个函数
work()
,要求接受一个字符串的List作为参数,把List中的每一个字符串,去掉第一个字符和最后一个字符,然后转为大写。
应该的效果:
work(['apple','banana','orange']) == ['PPL','ANAN','RANG']
提示:
- 写一个函数,用于处理1个字符串
- 用列表推导式处理整个列表
- 把上述代码包装在
work()
中
7.5 变量作用域
你在函数里调用了一个变量名,python会“从里到外”找这个变量,顺序是LEGB
LEGB含义解释:(暂时不用管)
- L-Local(function)即局部名称;函数内的名字空间
- E-Enclosing function locals即函数中嵌套函数的外部;外部嵌套函数的名字空间(例如closure)
- G-Global(module)即全局名称;函数定义所在模块(文件)的名字空间
- B-Builtin(Python)即内置名称;Python内置模块的名字空间
7.5.1 局部作用域
= 10 # 全局变量a
a def func():
= 20 # 局部变量a。在定义了局部变量a之后。后面使用a这个变量名,将会首先找到这个变量(即从里到外找)
a print(a) # 应该是20
func()
20
= 10 # 全局变量a
a def func():
print(a) # 在函数内部,没有变量a,那么就往外一层找,因此会找到全局变量`a`,应该等于10
func()
10
注意: 函数內部和外部变量重名的情况,要额外小心。因此第二个例子可能是你“忘记了在函数内部定义 a”,而不是你想要“引用全局变量 a”,但是因为有一个全局的变量 a,所以忘记定义 a
这个问题可能会被隐藏。
7.5.2 全局作用域
特别地,函数内部的赋值语句,其中的变量会被python看作一个local变量,如果内部未定义,就可能出错。
因为Python在编译函数的字节码时,只要发现函数内部有对变量的赋值操作(如 a = ...
),就会默认该变量是局部变量,除非显式声明为 global
或 nonlocal
。
= 100 # 全局变量a
a def func():
= a + 1 # a + 1这个a系统认为是局部变量,但这个函数没有局部的a
a print(a) # 全局变量a
func()
UnboundLocalError: local variable 'a' referenced before assignment
所以,如果你明确地要用一个函数外的变量,那么可以使用global
关键字
= 100 # 全局变量a
a def func():
global a # 说明:a这个变量名,指向的是全局变量a
= a + 1
a print(a) # 全局变量a
func()
101
7.5.3 nonlocal(修改外层局部变量)
nonlocal
用于修改“外层函数的局部变量”。它不同于 global
(修改模块级全局)。
def outer():
= 0
x def inner():
nonlocal x
+= 1
x return x
return inner
= outer()
counter print(counter()) # 1
print(counter()) # 2
1
2
7.6 递归简介(进阶)
递归:函数自己调用自己调用自己调用自己调用自己 …
例子,阶乘\(n! = n \times (n-1) \times (n-2) ... 2 \times 1\)
如 \(5! = 5 \times 4 \times 3 \times 2 \times 1\)
普通写法
def f(n):
= n
result
for i in range(1, n): # 注意,range(1,n),只到 n-1
*= i
result return result
= f(5)
result print(result)
120
递归的写法
\(f(n) = n * f(n-1)\)
\(f(n - 1) = (n - 1) * f(n - 2)\)
\(f(n) = n * (n - 1) * f(n - 2)\)
def f(n):
if n == 1:
return 1 # 递归终止条件
else:
return n * f(n-1) # 递归:自己调用了自己
= f(5)
result print(result)
120
7.7 函数式编程(进阶)
在我们已经熟练掌握了如何定义和使用函数之后,是时候接触一种更抽象、更数学化的编程范式了——函数式编程 (Functional Programming)。
函数式编程强调:
- 纯函数 (Pure Functions):函数的输出完全由其输入决定,且没有副作用。
- 不可变性 (Immutability):数据一旦创建,就不应被修改。
- 高阶函数 (Higher-Order Functions):函数可以接受其他函数作为参数,或者返回一个函数。
在本节中,我们将初步探索Python中函数式编程的一些核心工具和思想,包括:
map()
和filter()
:高效处理序列数据的利器。- 匿名函数
lambda
:快速创建简单的一次性函数。 - 高阶函数的概念:理解函数如何能被灵活地传递和创建。
- (选学)
reduce()
:对序列进行累积计算。 - (选学) 闭包 (Closures):理解函数如何“记住”其创建时的环境。
7.8 map和filter
7.8.1 map
有一个List,a = [1,2,3,4,5]
,你现在要把其中的每一个元素都乘以2,并保存在一个新的List中。用现有的知识,可以这样
- 建立一个空的List用来保存结果
result = []
- 循环
a
中的所有元素,并且乘以2,添加到result
的末端
= [1,2,3,4,5]
a print(a)
[1, 2, 3, 4, 5]
= []
result for i in a:
*2)
result.append(i
print(result)
[2, 4, 6, 8, 10]
map
函数,可以把一个函数,应用到List中的所有元素上。
def do_double(x):
"""翻倍函数"""
return x * 2
print(do_double(3))
6
把 do_double 函数,应用到列表 a 中的每一个元素里。
= map(do_double, a) # 把 do_double 函数,应用到列表 a 中的每一个元素里。
result
print(list(result))
[2, 4, 6, 8, 10]
也可用列表推导式
= [i * 2 for i in a]
result print(result)
= [do_double(i) for i in a]
result print(result)
[2, 4, 6, 8, 10]
[2, 4, 6, 8, 10]
把a里的元素,全部转换为str
= map(str, a) # 把str函数,应用到列表a中的每一个元素里。
result
print(list(result))
['1', '2', '3', '4', '5']
7.8.2 filter
有一个List,a = [1,2,3,4,5]
,你现在要选出符合特定条件的元素,例如选出其中的奇数,组成一个新的List。用循环做:
= [1,2,3,4,5]
a
= []
result
for i in a:
if i % 2 == 1:
result.append(i)
print(result)
[1, 3, 5]
和map一样,定义一个is_odd函数,作为filter的过滤条件,应该返回布尔值(True/False
),作为是否符合条件的结果。
def is_odd(x):
return x % 2 == 1
print(is_odd(3))
print(is_odd(6))
True
False
filter(判断函数, List)
= filter(is_odd, a)
result print(list(result))
[1, 3, 5]
当然,也可以用列表推导式
for i in a if is_odd(i)] [i
[1, 3, 5]
7.8.3 混合map和filter
选出a中的奇数,并乘以2
map 和 filter的做法
= list(map(do_double, filter(is_odd, a)))
result print(result)
[2, 6, 10]
列表推导式的做法
= [do_double(i) for i in a if is_odd(i)]
result print(result)
[2, 6, 10]
这其实可以理解为一个“数据流的概念”:
'''
a
-> filter by is_odd()
-> map by do_double()
-> result
'''
'\na \n-> filter by is_odd()\n-> map by do_double()\n-> result\n'
- 列表
a
首先经过filter函数的加工,然后再经过map函数的加工,最后得到result。 - 比喻:一堆水果(数据)-> 剔除坏的(处理1)-> 清洗(处理2)-> 削皮(处理3)-> 榨汁(处理4) -> 果汁(处理后的数据)
注:
- 多数情况下,列表推导式和map/filter函数几乎可以相互替代
- 列表更加符合Python的“风格”
- 但超级巨大List时,用map/filter性能更好:map/filter是一个lazy(惰性)函数,只有在你引用其中的值时,才会把函数真正应用上去。
- map/filter的结果,最后要转为list
所以
- 清晰性:列表推导式 ≥ map/filter 函数 > 循环(通常更符合 Python 风格)。
- 性能:内置函数/向量化(如
map
搭配内建或 NumPy/Pandas)通常更快;列表推导式与map
接近;纯 Python 循环最慢(但最直观)。以可读性为先,在有瓶颈时再做基准测试。
从逻辑上来讲,应该是先有map/filter
:要把一个函数应用到List中的所有元素/过滤元素,这是个基础性的需求,大多数编程语言都有做这件事的办法。我们一般把这种操作统称为map/filter
。Python的列表推导式,可以看成是一个Python优化版的map/filter
。
7.8.4 匿名函数 lambda
在我们使用map
、filter
,或者列表推导式的时候,如果要对列表做的操作非常简单、一目了然,并且可以写进一行(比如乘以二,或者取最后一个字母等等),那么我们可以不用专门写一个函数,而直接放一个匿名函数进去。
使用lambda
关键字,一个表达式,直接返回这个表达式的结果,不用写return
。
写法一般是:
lambda <参数列表>: <对参数的操作>
例如一个匿名版本的add(a,b)
函数:
lambda a,b : a + b
或者一个匿名的翻倍函数:
lambda x: x * 2
以把一个列表所以元素翻倍为例:
用普通函数:事先定义了一个do_double函数,这个函数返回参数x2
= map(do_double, a)
result print(list(result))
[2, 4, 6, 8, 10]
用匿名函数:乘以2这类简单操作,用匿名函数即可,可以省下预先定义do_double
函数。
= map(lambda x : x * 2,a)
result print(list(result))
[2, 4, 6, 8, 10]
7.8.4.1 key
函数与排序/选最值
在排序或选最值时,常通过 key=
指定“比较的依据”。
= [("alice", 3), ("bob", 5), ("carol", 2)]
items print(sorted(items, key=lambda x: x[1], reverse=True)) # 按第二列降序
= {"alice": 92, "bob": 85, "carol": 97}
scores print(max(scores, key=scores.get)) # 选出分数最高的键
[('bob', 5), ('alice', 3), ('carol', 2)]
carol
注:
对于初学者,如果不是一个超级简单的操作,建议还是应该用一个可以“顾名思义”的函数。
7.8.5 map和filter的结果:惰性
如果我们直接打印map和filter的结果,我们会发现,它们的返回值不是一个List,而是一个map object
或者filter object
。
这是因为map
和filter
,不会在你调用map()/filter()
函数的时候马上进行计算,而是在你读取其中的值的时候,才进行具体计算。我们称之为“惰性求值”。
例如,用前面的List翻倍的例子。
= [1,2,3,4,5]
a = map(lambda x : x * 2,a) # 当你调用map的时候,并未真正地进行计算
result print(result)
<map object at 0x107fd8b50>
所以,当你把map()/filter()
函数的结果打印出来看的时候,python会告诉你,这是个map/filter object,即map/filter函数的结果。但是,里面具体的数据还没计算出来,所以没法直接使用
要用,很简单,用list()
函数转换为一个列表即可。
print(list(result))
[2, 4, 6, 8, 10]
当然,如果你使用列表推导式,那么结果直接就是一个List。
= [i * 2 for i in [1,2,3,4,5]]
result print(result)
[2, 4, 6, 8, 10]
对于filter函数也一样:返回的不是列表,需要你手动转换。
def is_even(x):
return x % 2 == 0
# 过滤出偶数
print(list(filter(is_even, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])))
[2, 4, 6, 8, 10]
7.9 高阶函数
在Python中,函数是一等公民(头等函数,first-class function)。
一个函数,可以和任何变量或者对象一样,绑定一个名字(函数名),也可以换一个名字(重绑定),也可以作为参数传递给另一个函数(函数作为参数传另一个函数),也可以作为一个函数的返回值(函数返回另一个函数)。
7.9.1 函数是一等公民
def add(x,y):
return x + y
print(add(1,2))
3
函数和变量一样,其名字也可以重新绑定.
dda = add
之后,dda
和add
指向同一个函数(加法函数),dda()
也可以正常调用。
= add
dda
print(dda(1,2))
3
7.9.2 函数作为参数
例如map,可以把一个指定的函数,应用到List的每一个元素上。
map()
函数的第一参数,就是你要应用到List元素上的函数, 比如前例中乘以2do_double
,这里是把函数do_double
作为参数传递给了map
。第二个参数,就是你要处理的List。
= map(do_double, [1,2,3,4,5])
result print(list(result))
[2, 4, 6, 8, 10]
7.9.3 函数生成器
(进阶)
用参数来生成不同的函数。
例如,我们写一个函数,用于求一个数x的n次方。显然我们可以写一个这样的函数。
def power(x,n):
return x ** n
例如,3的平方:
print(power(3,2))
9
例如,2的3次方:
print(power(2,3))
8
平方、三次方等等,非常的常用,我们想写个专门的函数,可以少输入一个参数
def square(x):
return power(x,2)
print(square(3))
9
同样,三次方也是
def cube(x):
return power(x,3)
print(cube(2))
8
我们看到,这2个函数,函数体几乎完全一样,只有1个地方不同:2和3。
- 如果某些场合,4次方也很常用,我们可以原样建立一个4次方函数。
- 但是,这么多的函数,只有1个地方不同,必定是可以再往上抽象一层。
这个函数,可以根据你给的参数n
,生成另一个函数:n次方函数生成器
def make_power_function(n):
def func(x):
# return power(x, n) # 也是一样的
return x ** n
return func
make_power_function
是一个函数,这个函数会返回另一个函数func
'''
约等于以下代码
def func(x):
n = 2
return x ** n
square = func
'''
= make_power_function(2) square
= make_power_function(3) cube
print(square(3))
print(cube(2))
9
8
7.9.4 reduce (进阶)
reduce
和map
一样,不单是一个具体的函数,同时也是“一种常用的操作”,所以放在大部分编程语言中,都有相同的含义。因此一般会与map
并列,常称之为map/reduce
。
和map
一样,reduce
同样可以把一个函数应用到一个列表上,区别在于,你使用reduce
的时候,你要运用的函数,例如func(x,y)
,有2个参数。
reduce会这么做:
- 取出你要处理的列表的头2个元素(
a[0]
和a[1]
),传进func(a[0], a[1])
中,并得到一个返回值,如z1
- 取出列表的第3个元素
a[2]
,和前一个结果,一起传入func(z1,a[2])
,得到一个返回值,如z2
- 取出列表的第4个元素
a[3]
,和前一个结果,一起传入func(z2,a[3])
,得到一个返回值,如z3
- …
例如,我们对一个列表a = [1,3,5,7,9]
,reduce
一下应用我们前面写的add()
函数。
显然,这个操作大概是这个过程。
reduce
函数在functools
模块中,模块我们后面会讲。要引入reduce
函数,使用这行代码:
from functools import reduce
from functools import reduce
= [1,3,5,7,9]
a
reduce(add,a)
25
7.9.5 闭包Closure(进阶)
一个闭包closure,可以认为是一个函数,但除了函数体本身,包含了定义闭包时的数据。
例如
def make_power_function(n):
def func(x):
# return power(x, n) # 也是一样的
return x ** n
return func
这个代码里,调用make_power_function()
函数,会返回另一个函数func()
。
这个函数里面调用了变量n
,但n
不在func()
的函数体内,而在上一层。func()
函数,会把生成自己的环境中的变量(本例中即n
),也包括在里面:闭包是一个附带数据的函数。
这段代码发生了什么?
= make_power_function(2) square
square
函数,相当于一个附带了n=2
的func()
函数
power(n = 2)
,其中n = 2
- 在
make_power_function()
函数的内部,func(x)
是可以看见这个n=2
的 return func
的时候,会把n=2
,一并返回到外层square = power(2)
,此时我们把square
这个名字,绑定给func
这个函数(闭包),而后者同时还附带了n=2
前面讲变量作用域,Python找一个名称,是从内到外
- L局部作用域:即函数体内。例如本例中
func
函数的x
- Enclosing function locals:函数中嵌套函数的外部:例如本例中
func
函数的n
- G全局:代码的最外层
- B内置名称:python的一些内置的名称,如
max
函数
现在可知,python查找变量名,先找局部作用域(函数内部),再找闭包的附带数据,再找全局变量,再找python的内置名称。
7.10 本章小结
本章核心回顾:
- 函数的定义:使用
def
关键字,包含函数名、参数列表、冒号和缩进的函数体。 - 参数传递:理解位置参数、关键字参数、默认值、
*args
和**kwargs
的用法。 - 返回值:使用
return
语句从函数中返回结果;若无return
或return
后无值,则返回None
。 - 作用域:LEGB规则决定了变量的查找顺序,
global
和nonlocal
(进阶) 关键字用于修改外部作用域的变量。 - 可变/不可变类型作参数:注意可变类型(如列表)作参数时,在函数内部的修改会影响外部原对象。
- 函数式编程初步:
map
、filter
用于对序列进行批量操作,lambda
用于创建简单的匿名函数。
7.11 函数 vs 向量化(数据分析实务)
在数据分析中,优先考虑 NumPy/Pandas 的向量化、聚合与布尔索引,通常比编写 Python 层的 for
循环更简洁且高效:
import numpy as np
import pandas as pd
= pd.DataFrame({
df 'a': [1, 2, 3, 4],
'b': [10, 20, 30, 40]
})
# 列运算(向量化)
'c'] = df['a'] + df['b']
df[
# 条件列
'flag'] = np.where(df['a'] > 2, 1, 0)
df[
# 映射
= {1: 'X', 2: 'Y', 3: 'Z', 4: 'Z'}
code_to_cat 'cat'] = df['a'].map(code_to_cat)
df[
# 分组聚合
= df.groupby('cat')['b'].agg(['mean', 'sum']) g
提示: - np.vectorize
只是语法糖,不会带来显著加速。 - DataFrame.apply(axis=1, ...)
逐行运算通常较慢,应优先列运算或矢量化方案。
7.12 常见错误速查
- UnboundLocalError:在函数内赋值导致变量被视为局部,引用前未赋值;用
global
/nonlocal
明确意图。 - TypeError(参数不匹配):缺少必需位置参数、传多了参数,或对同一参数传了多个值(既位置又关键字)。
- 可变默认值陷阱:默认列表/字典在多次调用间共享;使用
None
作为占位并在函数内创建新对象。 - 忘记调用函数:写成
func
而非func()
,得到函数对象而不是返回值。 - 返回值为 None:遗漏
return
或分支未覆盖,后续参与计算报错。 - 解包长度不匹配:返回数量与左侧变量数不一致,抛
ValueError
。 - lambda 晚绑定:循环内创建的 lambda 捕获同一变量,执行时取最终值;可传默认值参数规避。
- map/filter 惰性:未消费或未
list(...)
转换,看不到结果。 - 关键字专用/位置专用:错误传参导致
TypeError
,注意def f(a, /, b, *, c)
的规则。