6. 函数

函数是执行特定任务的一段代码,程序通过将一段代码定义成函数,并为该函数指定一个函数名,这样即可在需要的时候多次调用这段代码。因此,函数是代码复用的重要手段。在 Python 中,用 lambda 表达式创建匿名函数能够让程序更加简洁。

6.1. 函数入门

函数就是 Python 程序的重要组成部分,一个 Python 程序可以由很多个函数组成。

6.1.1. 理解函数

通俗来讲,函数就是将一段特定功能的代码放在一起,并为其取了一个名字,即函数名,在后续需要使用这段代码的功能时直接调用这个名字。正如在列表中使用到了取长度函数 len()

通常,函数可以接收零个或多个参数,也可以返回零个或多个值。从函数使用者的角度来看,函数就像一个黑盒,程序将零个或多个参数传入这个黑盒,该黑盒经过一番处理即可返回零个或多个值。具体流程如下图所示:

输入图片说明

6.1.2. 定义函数和调用函数

在使用函数之前必须先定义函数,定义函数的语法格式如下:

def func_name(params)
	statements
	[return [values]]

Python 声明函数时使用 def 关键字,对函数语法格式的详细说明如下:

  • func_name 称为函数名:函数名由一个或多个有意义的单词连缀而成,每个单词的字母全部小写,单词与单词之间使用下画线分隔
  • params 称为形参列表:用于定义函数可以接收的参数。形参列表由多个形参名组成,多个形参名之间以英文逗号 , 隔开

在函数体中多条可执行语句之间有严格的执行顺序,排在函数体前面的语句总是先执行,排在函数体后面的语句总是后执行。使用方法如下代码所示:

# 定义一个函数,声明两个形参
def my_max(x, y):
	# 定义一个变量 z , 该变量等于 x、y 中较大的值
	z = x if x > y else y
	# 返回变量 z 的值
	return z

# 定义一个函数,声明一个形参
def say_hello(name):
	return "Hello, " + name

# 调用 my_max() 函数,将函数返回值赋值给 result 变量
result = my_max(6, 9) # 9
print(result)
# 调用 say_hello() 函数,直接输出函数的返回值
print(say_hello("felixzzy")) # Hello, felixzzy

如上,在函数体中使用 return 语句显式地返回一个值,return 语句返回的值既可是有值的变量,也可是一个表达式。

6.1.3. 为函数提供文档

函数像一个黑盒,因此一个编写完善的函数需要有详细的说明文档,在我们设计函数时,也需要为函数编写说明文档,只要把一段字符串放在函数声明之后、函数体之前,这段字符串将被作为函数的部分,这个文档就是函数的说明文档。

程序既可通过 help() 函数查看函数的说明文档,也可通过函数的 __doc__ 属性访问函数的说明文挡。详细使用方法如下代码所示:

def my_max(x, y):
	'''
	获取两个数值之间较大数的函数
	
	my_max(x, y)
		返回 x、y 两个参数之间较大的那个数
	'''
	# 定义一个变量 z,该变量等于 x、y 中较大的值
	z = x if x > y else y
	# 返回变量 z 的值
	return z
# 使用 help() 函数查看 my_max 的帮助文档
help(my_max)
print(my_max.__doc__)

上面程序使用多行字符串的语法为 my_max() 函数编写了说明文档,既可以通过 help() 函数查看该函数的说明文档,也可通过 __doc__ 属性访问该函数的说明文档。

运行上面代码,可以看到如下运行结果。

Help on function my_max in module __main__ :
my_max(x, y)
	获取两个数值之间较大数的函数
	
	my_max(x, y)
		返回 x、y 两个参数之间较大的那个数

	获取两个数值之间较大数的函数
	
	my_max(x, y)
		返回 x、y 两个参数之间较大的那个数

6.1.4. 多个返回值

如果程序需要有多个返回值,则既可将多个值包装成列表之后返回,也可直接返回多个值。 如果 Python 函数直接返回多个值, Python 会自动将多个返回值封装成元组。使用方法如下代码所示:

def sum_and_avg(list):
	sum = 0
	count = 0
	for e in list:
		# 如果元素 e 是数值
		if isinstance(e, int) or isinstance(e, float):
			count += 1
			sum += e
		return sum, sum / count

my_list = [20, 15, 2.8, 'a', 35, 5.9, 1.8]
# 获取 sum_and_avg 函数返回的多个值,多个返回值被封装成元组
tp = sum_and_avg(my_list)
print(type(tp)) # <class 'tuple'>

上面代码中函数的返回值被自动封装成元组。

6.1.5. 递归函数

在一个函数内调用该函数自身,被称为函数递归。函数递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。一个简单的例子是求序列的和:即 f(0) = 0,f(n) = f(n-1)+1,具体的代码如下所示:

def my_sum(n):
	if n == 0:
		return 0
	else:
		return my_sum(n-1) + n

result = my_sum(5)
print(result) # 15

6.2. 函数的参数

在定义 Python 函数时可定义形参,即形式参数,这些形参的值要等到调用时才能确定下来,由函数的调用者负责为形参传入参数值。

6.2.1. 关键字( keyword )参数

Python 允许在调用函数时通过名字来传入参数值,因此, Python 函数的参数名应该具有更好的语义。按照形参位置传入的参数被称为位置参数。如果使用位置参数的方式来传入参数值,则必须严格按照定义函数时指定的顺序来传入参数值,如果根据参数名来传入参数值,则无须遵守定义形参的顺序,这种方式被称为关键字参数。使用方法如下代码所示:

# 计算面积
def area(width, height):
	return width * height

# 根据位置传入参数值
print(area(4, 5))
# 根据关键字参数传入参数值
print(area(width=4, height=5))
# 根据关键字参数传入参数值可以交换位置
print(area(height=5, width=4))
# 部分使用关键字参数,部分使用位置参数
print(area(4, height=5))

如果希望在调用函数时混合使用关键字参数和位置参数,则关键字参数必须位于位置参数之后,否则会出错,如下代码所示:

# 错误代码
print(area(width=4, 5))

运行上面代码,将会提示如下错误 。

SyntaxError: positional argument follows keyword argument

6.2.2. 参数默认值

在某些情况下,需要在定义函数时为一个或多个形参指定默认值,这样在调用函数时就可以省略为该形参传入参数值,而是直接使用该形参的默认值。为形参指定默认值的语法格式如下:

形参名=默认值

仍以上面的计算面积的代码为例,参数默认值的使用方法如下代码所示:

# 计算面积
def area(width=4, height=5):
	return width * height

# 根据关键字参数传入新的参数值
print(area(width=5, height=6)) # 30
# 使用默认值
print(area()) # 20

从上面的例子来看,当传入新的参数值时,使用新的参数值,如果不传入参数值,则使用默认值。

6.2.3. 个数可变的参数

Python 允许定义个数可变的参数,这样就可以在调用函数时传入任意多个参数。只需在形参前添加一个星号 * ,这样该参数就可以接收多个参数值,多个参数值被当成元组传入。使用方法如下代码所示:

def cs_language(*lan):
	print(type(lan)) # <class 'tuple'>
	print(lan) # ('Ruby', 'Go', 'Swift', 'Kotlin', 'Erlang', 'Python')

cs_language('Ruby', 'Go', 'Swift', 'Kotlin', 'Erlang', 'Python')

Python 中一个函数最多只能有一个个数可变的 * 的参数。

当第一个参数是一个个数可变的参数时,后续的参数在传入参数时,需要使用关键字参数,如下代码所示:

def cs_language(*lan, num):
	print(type(lan)) # <class 'tuple'>
	print(lan) # ('Ruby', 'Go', 'Swift', 'Kotlin', 'Erlang', 'Python')
	print(num) # 6

cs_language('Ruby', 'Go', 'Swift', 'Kotlin', 'Erlang', 'Python', num=6)

Python 中的函数还可以使用关键字参数,即在参数前面添加两个星号 ** ,此时 Python 会将这种关键字参数当成字典。如下代码所示:

def cs_language(*lan, num, **test):
	print(type(lan)) # <class 'tuple'>
	print(lan) # ('Ruby', 'Go', 'Swift', 'Kotlin', 'Erlang', 'Python')
	print(num) # 6
	print(type(test)) # <class 'dict'>
	print(test) # {'test1': 1, 'test2': 2}

cs_language('Ruby', 'Go', 'Swift', 'Kotlin', 'Erlang', 'Python', num=6, test1=1, test2=2)

6.2.4. 函数的参数传递机制

前面章节中介绍了 Python 中的基本数据类型,包括整型, String 类型以及 List 类型等等,在 Python 中数据类型又可以分为可变类型和不可变类型:

  • 可变类型:变量赋值后可修改其中的元素,如 List 类型
  • 不可变类型:变量赋值后不可以修改,注意,如整型是重新赋值而不是修改

在 Python 中,函数的参数传递也是根据数据类型的不同而有不同的结果:

  • 可变类型:如列表,字典等,相当于引用传递
  • 不可变类型:如整数、字符串、元组等,相当于值传递

具体使用方法如下代码所示:

def fun1(a):
	# 不可变类型的形参地址
	print("不可变类型形参原地址:", id(a))
	a=10
	print("不可变类型形参修改后地址:", id(a))

def fun2(a):
	# 可变类型的形参地址
	print("可变类型形参原地址:", id(a))
	# 修改部分元素
	a[1] = 10
	print("可变类型形参修改后地址:", id(a))

# 定义不可变类型
a=1
print("不可变类型原地址:", id(a))
fun1(a)
print(a)

b = [1,2,3,4]
print("可变类型原地址:", id(b))
fun2(b)
print(b)

运行上述代码,有如下的输出:

不可变类型原地址: 4369074928
不可变类型形参原地址: 4369074928
不可变类型形参修改后地址: 4369075216
1
可变类型原地址: 4371270976
可变类型形参原地址: 4371270976
可变类型形参修改后地址: 4371270976
[1, 10, 3, 4]

此处 id() 函数的作用是查看变量的地址,从运行结果可以发现在参数传递的过程中,对不可变类型形参修改后,其实是指向了一个新的地址,并不会改变实参的值,而可变类型形参还是指向原先的地址,实际上是在实参上做了修改。

6.2.5. 变量作用域

在程序中定义一个变量时,这个变量是有作用范围的,变量的作用范围被称为它的作用域。根据定义变量的位置不同,变量分为以下两种:

  • 局部变量。在函数中定义的变量,包括参数,都被称为局部变量。
  • 全局变量。在函数外面、全局范围内定义的变量,被称为全局变量。

每个函数在执行时,系统都会为该函数分配一块“临时内存空间”,所有的局部变量都被保存在这块临时内存空间内。当函数执行完成后,这块内存空间就被释放了,这些局部变量也就失效了,因此离开函数之后就不能再访问局部变量了。

全局变量意味着它们可以在所有函数内被访问。

6.3. 局部函数

Python 支持在函数体内定义另一个函数,这种被放在函数体内定义的函数称为局部函数。默认情况下,局部函数对外部是隐藏的,局部函数只能在其父函数内有效。当然,也可以通过其父函数返回局部函数,以便在其他作用域中使用局部函数。使用方法如下代码所示:

# 定义函数,该函数会包含局部函数
def get_math_func(type, nn):
	# 定义一个计算平方的局部函数
	def square(n):
		return n * n
	# 定义一个计算立方的局部函数
	def cube(n):
		return n * n * n
	# 定义一个计算阶乘的局部函数
	def factorial(n):
		result = 1
		for index in range(2, n+1):
			result *= index
		return result
	# 调用局部函数
	if type == "square":
		return square(nn)
	elif type == "cube":
		return cube(nn)
	else:
		return factorial(nn)
print(get_math_func("square", 3)) # 输出 9
print(get_math_func("cube", 3)) # 输出 27
print(get_math_func("", 3)) # 输出 6

如上代码中定义了一个 get_math_func() 函数,并在该函数内定义了三个局部函数,而 get_math_func() 函数根据参数选择调用不同的局部函数。

6.4. 函数的其他用法

在 Python 中,函数本身也是一个对象,函数既可用于赋值,也可用作其他函数的参数,还可作为其他函数的返回值。

6.4.1. 使用函数变量

Python 的函数是一个对象,这样就可以把函数赋值给变量,当把函数赋值给变量之后,程序就可以通过该变量来调用函数。使用方法如下代码所示:

# 定义一个计算面积的函数
def area(width, height):
	return width * height

# 将 area 函数赋值给 my_fun ,则 my_fun 可被当成 area 使用
my_fun = area
print(my_fun(3, 4)) # 输出 12

在上述代码中,将函数 area() 赋值给变量 my_fun,在程序中就可以通过调用变量 my_fun 实现函数 area() 的功能。

6.4.2. 使用函数作为函数形参

既然函数可以赋值给变量,那么函数自然也可以作为另一个函数的形参,这样就可以通过该形参动态改变函数的功能,如下代码通过形参实现不同的计算逻辑:

# 定义函数类型的形参,其中 fn 是一个函数
def map(data, fn):
	result = []
	# 遍历 data 列表中的每个元素,并用 fn 函数对每个元素进行计算
	# 然后将计算结果作为新数组的元素
	for e in data:
		result.append(fn(e))
	return result

# 定义一个计算平方的函数
def square(n):
	return n * n
# 定义一个计算立方的函数
def cube(n):
	return n * n * n
# 定义一个计算阶乘的函数
def factorial(n):
	result = 1
	for index in range(2, n+1):
		result *= index
	return result  
data = [3, 4, 9, 5, 8]
print(data)
# 下面程序代码调用 map() 函数三次,每次调用时传入不同的函数
print(map(data, square)) # [9, 16, 81, 25, 64]
print(map(data, cube)) # [27, 64, 729, 125, 512]
print(map(data, factorial)) # [6, 24, 362880, 120, 40320]
print(type(map)) # <class 'function'>

如上代码中定义了一个 map() 函数,该函数的第二个参数是一个函数类型的参数,这意味着每次调用函数时可以动态传入一个函数,从而实现不同的计算逻辑。

6.4.3. 使用函数作为返回值

Python 还支持使用函数作为其他函数的返回值,如下代码所示:

def get_math_func(type):
	#定义一个计算平方的局部函数
	def square(n):
		return n * n
	# 定义一个计算立方的局部函数
	def cube(n):
		return n * n * n
	# 定义一个计算阶乘的局部函数
	def factorial(n):
		result = 1
		for index in range(2, n+1):
			result *= index
		return result
	
	# 返回局部函数
	if type == "square":
		return square
	elif type == "cube":
		return cube
	else:
		return factorial

# 调用 get_math_func(),程序返回一个嵌套函数
math_func = get_math_func("cube") # 得到 cube 函数
print(math_func(5)) # 输出 125
math_func = get_math_func("square") # 得到 square 函数
print(math_func(5)) # 输出 25
math_func = get_math_func("other") # 得到 factorial 函数
print(math_func(5)) # 输出 120

如上代码中定义了一个 get_math_func() 函数,在该主函数中又定义了三个局部函数 square()cube()factorial(),通过形参 type 确定主函数返回的局部函数。

6.5. 局部函数与 lambda 表达式

lambda 表达式是现代编程语言争相引入的一种语法,如果说函数是命名的、方便复用的代码块,那么 lambda 表达式则是功能更灵活的代码块,它可以在程序中被传递和调用。

6.5.1. 匿名函数

匿名函数是指没有名字的函数,它主要应用在需要一个函数、但是又不想费神去命名这个函数的场合。回顾在局部函数一节中的函数 get_math_func() ,在该函数中定义了三个局部函数 square()cube()factorial() ,由于局部的函数只能在函数 get_math_func() 中调用,因此使用完整的函数定义是不必要的,此时可以直接使用匿名函数。

6.5.2. 使用 lambda 表达式创建匿名函数

在Python中,使用 lambda 表达式创建匿名函数,其语法格式如下:

lambda [parameter_list]:expression

使用 lambda 表达式重新修改 get_math_func() 函数,代码如下所示:

# 定义函数,该函数包含匿名函数
def get_math_func(type):
	# lambda 匿名函数
	if type == "square": # 平方
		return lambda n : n*n
	elif type == "cube": # 立方
		return lambda n : n*n*n
	else: # 累加
		return lambda n : (1+n)*n/2

print(get_math_func("square")(3)) # 输出 9
print(get_math_func("cube")(3)) # 输出 27
print(get_math_func("")(3)) # 输出 6.0

lambda 表达式只能是单行表达式,不允许使用更复杂的函数形式,因此使用 lambda 表达式表示原先的阶乘的操作相对比较困难。

6.6. 本章小结

本章介绍了函数的使用方法和 lambda 表达式,包括定义函数、调用函数的语法。同时,函数也是一个 function 对象,因此函数既可作为其他函数的参数,也可作为其他函数的返回值,通过把函数当成参数传入其他函数,可以让编程变得更加灵活。 Python 的 lambda 表达式只是单行函数的简化版本,因此 lambda 表达式的功能比较简单。

本章需要掌握知识点:

  1. 函数的定义和调用
  2. 形参的各种用法,如关键字参数,个数可变形参等
  3. 理解函数的参数传递机制
  4. 理解局部函数以及 lambda 表达式