= 榨汁机(苹果) 苹果汁
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 例:求两个数的最大值
思路:
- 如果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.3 例:计算圆的面积
另一个例子,已知:圆的半径是\(r\),我们要计算圆的面积,公式就是\(y = \pi r^2\)。
我们准备写一个函数来做这件事:
- 这个函数会根据我们提供的半径\(r\),返回一个圆的面积给我们。
- 显然,这个函数必然是接受
r
作为参数 - 我们做的,是算出面积,然后返回一下
def calc_area(r):
= 3.14159 * (r ** 2)
area return area
= 5
x = calc_area(x)
area
print(f"半径为{x}的圆,其面积为{area:0.2f}")
半径为5的圆,其面积为78.54
7.1.4 不返回值
若函数不需要返回任何值,不写return
语句,或者return
语句后不返回任何值
#%% 这2种都是可以的
def hello():
print("hello") # 不写return
def hello2():
print("hello")
return # 写return但不包括返回值
hello() hello2()
hello
hello
7.1.5 什么也不做的函数
有时候需要先占用函数名,但是函数的内容想以后再写,此时可以写一个空函数。函数体内只需要使用pass
语句。
特别地,没有return
语句(例如现在这种情况),或者return
语句不返回值的时候,函数会返回一个none
。
def do_noting():
pass
print(do_noting())
None
7.1.6 小练习
结合前面的学习内容:
- 实现一个函数
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 打包(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.6 小练习
- 利用不定数量参数,写一个函数
my_sum()
,可以求任意个变量的和。效果应该如下:
my_sum(1,2,3,4) == 10
my_sum(3,4,5) == 12
7.2.7 拆包(unpacking):把数据结构拆散传给参数
你定义了一个函数,其中每一个参数都有明确的含义,而函数的参数保存在一个List、Tuple里(比如第一个元素对应第一个参数,第二个元素对应第二个参数等等)或者Dict里(key就对应参数名),把这些数据结构中的元素,当参数传给函数。
- tuple或者list前面加
*
- dict前加
**
称之为拆包(unpakcing)
例:
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.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进行的操作,可以是一个函数。
比如,我们要把一个列表中的值进行一个很复杂的运行,可以首先把这个运算写成一个函数。这里以乘以2为例。
def do_double(x):
"""这是一个超级复杂的运行,算法有一万行
"""
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
r red("注:")
:函数內部和外部变量重名的情况,要额外小心。因此第二个例子可能是你“忘记了在函数内部定义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.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_dobule函数,应用到列表a中的每一个元素里。
= map(do_double, a) # 把do_dobule函数,应用到列表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()
-> reuslt
'''
'\na \n-> filter by is_odd()\n-> map by do_double()\n-> reuslt\n'
- 列表
a
首先经过filter函数的加工,然后再经过map函数的加工,最后得到reuslt。 - 比喻:一堆水果(数据)-> 剔除坏的(处理1)-> 清洗(处理2)-> 削皮(处理3)-> 榨汁(处理4) -> 果汁(处理后的数据)
注:
- 多数情况下,列表推导式和map/filter函数几乎可以相互替代
- 列表更加符合Python的“风格”
- 但超级巨大List时,用map/filter性能更好:map/filter是一个lazy(惰性)函数,只有在你引用其中的值时,才会把函数真正应用上去。
- map/filter的结果,最后要转为list
所以
- 清晰性:列表推导式 > map/filter函数 > 循环
- 性能:map/filter函数 > 列表推导式 > 循环
从逻辑上来讲,应该是先有map/filer
:要把一个函数应用到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.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 0x10733ae60>
所以,当你把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_funcion(n):
def func(x):
# return power(x, n) # 也是一样的
return x ** n
return func
make_power_funcion
是一个函数,这个函数会返回另一个函数func
'''
约等于以下代码
def func(x):
n = 2
return x ** n
square = func
'''
= make_power_funcion(2) square
= make_power_funcion(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_funcion(n):
def func(x):
# return power(x, n) # 也是一样的
return x ** n
return func
这个代码里,调用power()
函数,会返回另一个函数fun()
。
这个函数里面调用了变量n
,但n
不在fun()
的函数体内,而在上一层。fun()
函数,会把生产自己的坏境中的变量(本例中即n
),也包括在里面:闭包是一个附带数据的函数。
这段代码发生了什么?
= make_power_funcion(2) square
square
函数,相当于一个附带了n=2
的func()
函数
power(n = 2)
,其中n = 2
- 在
make_power_funcion()
函数的内部,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
用于创建简单的匿名函数。
常见错误 (Common Errors & Pitfalls):
UnboundLocalError: local variable 'var_name' referenced before assignment
- 原因:在函数内部尝试修改一个与全局变量同名的变量,但没有使用
global
关键字声明,Python会认为你试图引用一个尚未赋值的局部变量。 - 示例:
x = 10; def func(): x = x + 1; print(x)
,调用func()
会报错。 - 解决:如果意图是修改全局变量,请在函数内部使用
global var_name
。
- 原因:在函数内部尝试修改一个与全局变量同名的变量,但没有使用
- 忘记
return
或return
位置不当- 原因:函数需要返回一个计算结果,但忘记写
return
语句,或者return
语句在逻辑上没有被执行到(例如在某个if
分支内,但条件未满足)。 - 结果:函数默认返回
None
,可能导致后续代码逻辑错误。 - 解决:仔细检查函数的逻辑,确保在所有期望的路径上都有正确的
return
语句。
- 原因:函数需要返回一个计算结果,但忘记写
- 对可变类型参数副作用的忽视
- 原因:将列表或字典等可变对象传入函数,在函数内部修改了它,导致函数外部的原始对象也被意外改变。
- 解决:如果不想修改原始对象,在函数内部应先创建副本(如使用
.copy()
方法)。