函数的特点: (1)、使用 def 关键字定义,有函数名。使用 lambda 定义的函数除外。 (2)、一个函数通常执行一个特定任务,可多次调用,因此函数是代码利用的重要手段。 (3)、函
函数的特点:
(1)、使用 def 关键字定义,有函数名。使用 lambda 定义的函数除外。
(2)、一个函数通常执行一个特定任务,可多次调用,因此函数是代码利用的重要手段。
(3)、函数调用时可以传递0个或多个参数。
(4)、函数有返回值。
一、 函数基础
函数是 Python 程序的重要组成单位,一个 Python 程序可以由多个函数组成。
1、 理解函数
在定义函数时,至少需要清楚下面3点:
(1)、函数有几个需要动态变化的数据,这些数据应该被定义成函数的参数。
(2)、函数需要返回几个重要的数据,这些数据应该被定义成返回值。
(3)、函数的内部实现过程。
函数的定义要比函数的调用难很多。对于实现复杂的函数,定义本身就很费力,所以有时实现不出来也正常。
2、 定义函数和调用函数
函数定义语法格式如下:
1 def function_name(arg1, arg2, ...): 2 # 函数体(由0条或多条代码组成)
3 [return [返回值]]
语法格式说明如下:
(1)、函数声明必须使用 def 关键字。
(2)、函数名是一个合法的标识符,不要使用 Python 保留的关键字;函数名可由一个或多个有意义的单词连接而成,单词之间用下划线分隔,单词的字母全部用小写。
(3)、形参列表,用于定义该函数可以接收的参数。形参列表由多个形参名组成,形参之间用英文逗号分隔。在定义函数指定了形参,调用该函数时必须传入对应的参数值,即谁调用函数,谁负责为形参赋值。
函数被调用时,既可以把调用函数的返回值赋值给指定变量,也可以将函数的返回值传给另一个函数,作为另一个函数的参数。
函数中 return 语句可以显式地返回一个值,return 语句返回的值既可是有值的变量,也可是一个表达式。
3、 为函数提供文档
Python 有内置的 help() 函数可查看其他函数的帮助文档。在编写函数时,把一段字符串放在函数声明之后、函数体之前,这段字符串将被作为函数的一部分,这个文档就是函数的说明文档。
函数可通过 help() 函数查看函数的说明文档,也可通过函数的 __doc__ 属性访问函数的说明文档。示例如下:
1 def max_num(x, y): 2 """ 3 获取两个数值中较大数的函数 4 max_num(x, y) 5 返回x、y两个参数之间较大的那个数 6 """ 7 return x if x > y else y 8 # 使用 help() 函数和 __doc__ 属性查看 max_num 的帮助文档 9 help(max_num) 10 print(max_num.__doc__)
运行这段代码时,可以看到输出的帮助文档信息。
4、 多个返回值
如果函数有多个返回值,可将多个值包装成列表或字典后返回。也可直接返回多个值,这时 Python 会自动将多个返回值封装成元组。
1 # 定义函数
2 def foo(x, y) 3 return x + y, x - y 4 # 调用函数
5 a1 = foo(10, 5) # 函数返回的是一个元组,变量a1 也就是一个元组
也可使用 Python 提供的序列解包功能,直接用多个变量接收函数返回的多个值,例如:
a2, a3 = foo(10, 5)
5、 递归函数
在函数体内调用它自身,被称为函数递归。函数递归包含一种隐式的循环,它会重复执行某段代码,这种重复执行无须循环控制。
现假设有一个数列,f(0)=1, f(1)=4, f(n+2)=2*f(n+1)+f(n),n是大于0的整数,求f(10)的值。这道题用递归函数计算,代码如下:
1 def f(n): 2 if n == 0: 3 return 1
4 elif n == 1: 5 return 4
6 else: 7 # 在函数体内调用其自身,就是递归函数
8 return 2 * f(n - 1) + f(n - 2) 9
10 print(f(10)) # 输出:10497
在这段代码中,f() 函数体中再次调用了 f() 函数,就是递归函数。调用形式是:
return 2 * f(n - 1) + f(n - 2)
对于 f(10)等于 2*f(9)+f(8),其中f(9)又等于2*f(8)+f(7)······,以此类推,最终计算到 f(2)等于2*f(1)+f(0),即f(2)是可计算的,这样递归的隐式循环就有结束的时候,然后一路反算回去最后得到 f(10) 的值。
在上面这个递归函数中,必须在某个时刻函数的返回值是确定的,即不再调用它自身;否则,这种递归就变成了无穷递归,类似于死循环。所以,在定义递归时的一条重要规定是:递归一定要向已知的方向进行。
现在将这个数列的已知条件改为这样,已知:f(20)=1, f(21)=4, f(n+2)=2*f(n+1)+f(n),其中n是大于0的整数,求f(10)的值。此时f()函数体就应该改为如下形式:
1 def f(n): 2 if n == 20: 3 return 1
4 elif n == 21: 5 return 4
6 else: 7 return f(n + 2) - 2 * f(n + 1) 8 print(f(10)) # 输出:-3771
在这次的 f() 函数中,要计算 f(10) 的值时,f(10)等于f(12)-2*f(11),而 f(11)等于f(13)-2*f(12)······,以此类推,直到 f(19)等于f(21)-2*f(20),此时得到 f(19)的值,然后依次反算到f(10)的值。
递归在编程中非常有用,例如程序要遍历某个路径下的所有文件,但这个路径下的文件夹的深度是未知的,此时就可用递归来实现这个需求。
总结:在一个函数的函数体中调用自身,就是递归函数。递归一定要向已知的方向进行。
二、 函数的参数
1、关键字(keyword)参数
按照形参位置传入的参数称为位置参数。如果根据位置参数的方式传入参数值,则必须严格按照定义函数时指定的顺序来传入参数值;也
可根据参数名来传入参数值,此时无须遵守定义形参的顺序,这种方式就是关键字(keyword)参数。示例如下:
1 # 定义一个计算周长的函数 girth,接收两个形参
2 def girth(length, width): 3 print("length:", length) 4 print("width:", width) 5 return 2 * (length + width) 6 # 传统调用函数方式,根据位置传入参数值
7 print(girth(3, 5.5)) 8 # 根据关键字参数传入参数值,使用关键字时,参数位置可以交换
9 print(girth(width=5.5, length=3)) 10 # 部分用关键字,部分用位置参数,位置参数必须在关键字参数前面
11 print(girth(3, width=5.5))
在使用关键字参数调用函数时要注意,在调用函数时同时使用位置参数和关键字参数时,位置参数必须位于关键字参数的前面。也就是
关键字参数后面只能是关键字参数。
2、 参数默认值
在定义函数时,可以为一个或多个形参指定默认值,这样在调用函数时可以省略为该形参传入参数值,直接使用该形参的默认值。示例如下:
1 # 为参数指定默认值
2 def say_hi(name=‘michael‘, message="欢迎学习使用Python!"): 3 print("hello,", name) 4 print("消息是:", message, sep="") 5 # 第一次调用:使用默认参数调用函数
6 say_hi() 7 # 第二次调用:传入1个位置参数时,默认传给第一个参数
8 say_hi(‘jack‘) 9 # 第三次调用:传入2个位置参数
10 say_hi(‘stark‘, ‘欢迎使用 C 语言!‘) 11 # 使用关键字参数指明要传给哪个参数
12 say_hi(message="欢迎使用 Linux 系统!") 13
14 输出如下所示: 15 hello, michael 16 消息是:欢迎学习使用Python! 17 hello, jack 18 消息是:欢迎学习使用Python! 19 hello, stark 20 消息是:欢迎使用 C 语言! 21 hello, michael 22 消息是:欢迎使用 Linux 系统!
从这个程序可知,当只传入一个位置参数时,由于该参数位于第一位,系统会将该参数值传给 name 参数。
在Python 中,关键字参数必须位于位置参数后面,所以下面把关键字参数放在前面是错误的做法:
say_hi(name=‘stark‘, ‘欢迎使用 C 语言!‘)
这样调用函数时,报 “positional argument follows keyword argument” 错误。
在调用函数时,也不能简单的交换两个参数的位置,下面对函数的调用方式同样是错误的:
say_hi(‘欢迎使用 C 语言!‘, name=‘stark‘)
因为第一个字符串没有指定关键字参数,因此使用位置参数为 name 参数传入参数值,第二个参数使用关键字参数的形式再次为 name 参数传入参数值,这样造成两个参数值都会传给 name 参数,程序为 name 参数传入了多个参数值。因此错误提示:say_hi() got multiple values for argument ‘name‘。
Python 要求在调用函数时关键字参数必须位于位置参数后面,因此在定义函数时指定默认值的参数(关键字参数)必须在没有默认值的参数之后。例如:
1 def foo(a, b=10): pass # 有默认值的参数在没有默认值的参数后面
2 下面这些调用函数的方法都是正确的: 3 foo(5) 4 foo(a=5, b=4) 5 foo(5, 4) 6 foo(a=‘python‘)
3、 参数收集(个数可变的参数)
在定义函数时,形参前面加一个星号(*)表示该参数可接收多个参数值,多个参数值被当成元组传入。示例如下:
1 def my_test(a, *args): 2 """测试支持参数收集的函数"""
3 print(args) 4 # args 参数被当成元组处理
5 for s in args: 6 print(s) 7 print(a) 8 my_test(123, ‘python‘, ‘linux‘, ‘C‘) 9
10 输出如下所示: 11 (‘python‘, ‘linux‘, ‘C‘) 12 python 13 linux 14 C 15 123
从输出可知,在调用 my_test() 函数时,args 参数可以传入多个字符串作为参数值,参数收集的本质就是一个元组,将传给 args 的多个值收集成一个元组。
对于个数可变的形参可以处于形参列表的任意位置,但是函数最多只能有一个“普通”参数收集的形参。例如下面这样:
def my_test(*args, a): pass
在定义函数时,把个数可变的形参放在前面。那么在调用该函数时,如果要给后面的参数传入参数值,就必须使用关键字参数;否则,程序会把所传入的多个值都当成是传给 args 参数的。
另外,Python 还可以将关键字参数收集成字典,这时在定义函数时,需要在参数前面加两个星号(**)。一个函数可同时包含一个支持“普通”参数收集的参数和一个支持关键字参数收集的参数。示例如下:
1 def bar(a, b, c=5, *args, **kwargs): pass 2 bar(1, 2, 3, "python", "linux", name=stark, age=30)
在调用 bar() 函数时,前面的1、2、3会传给普通参数a、b、c;后面紧跟的两个参数由 args 收集成元组;最后的两个关键字参数被kwargs 收集成字典。这个 bar() 函数中,c 参数的默认值基本上不能发挥作用。要使 c 参数的默认起作用,可用下面方式调用函数:
bar(1, 2, name=stark, age=30)
4、 逆向参数收集
逆向参数收集指的是在程序中将已有的列表、元组、字典等对象,将其拆分后传给函数的参数。逆向参数收集需要在传入的列表、元组等参数之前添加一个星号,在字典参数之前添加两个星号。示例如下:
1 def bar(s1, s2): 2 print(s1) 3 print(s2) 4 s = ‘ab‘
5 bar(*s) 6 ss = ("python", "linux") 7 bar(*ss) 8 ss_dict = {"s1": "michael", "s2": 25} 9 bar(**ss_dict)
在这个 bar() 函数中,定义时声明了两个形参,在调用函数时,使用一个星号拆分序列(字符串、列表、元组)时,拆分出来的个数也应和形参的个数一样多。使用两个星号传递字典参数时,则要求字典的键与函数的形参名保持一致,并且字典的键值对与函数的形参个数也要一致。
即使支持收集的参数,如果要将一个字符串或元组传给该参数,同样也需要使用逆向收集。示例如下:
1 def bar(s1, *s2): 2 print(s1) 3 print(s2) 4 s = ‘abcd‘
5 bar("py", *s) 6 ss = ("python", "linux") 7 bar("java", *ss)
这次的函数调用中,可以使用一个星号拆分序列,序列的元素个数可以有多个。但是不能使用两个星号拆分字典后向其传参数。实际上,在调用函数时,只传递一个拆分后的序列参数也是可以的,例如:
bar(*s)
这时程序会将 s 进行逆向收集,其中字符串的第一个元素传给 s1 参数,后面的元素传给 s2 参数。如果不使用逆向收集(不在序列参数前使用星号),则会将整个序列作为一个参数,而不是将序列的元素作为多个参数。例如:
bar(s)
这次调用没有使用逆向收集,因此 s 整体作为参数值传给 s1 参数。
字典也支持逆向收集,字典以关键字参数的形式传入。这要求字典的键与函数的形参名一致,并且字典的键值对数量与函数的形参数量一样多。示例在前面已提到。
5、 函数的参数传递机制
Python 中函数的参数传递机制都是“值传递”,就是将实际参数值的副本(复制品)传入函数,而参数本身不会受到任何影响。示例如下:
1 def swap(a, b): 2 a, b = b, a 3 print("在swap函数里,a的值是%s; b 的值是%s" % (a, b)) 4 a = 10
5 b = 20
6 swap(a, b) 7 print("变换结束后,变量 a 的值是%s;变量 b 的值是%s。" % (a, b)) 8
9 运行代码,输出如下: 10 在swap函数里,a的值是20; b 的值是10 11 变换结束后,变量 a 的值是10;变量 b 的值是20。
在这段代码中,swap() 函数中交换了变量a、b 的值,在函数内输出的是交换后的值,在函数外部仍然是未交换时的值。由此可知,程序中实际定义的变量 a 和 b,并不是 swap() 函数里的 a 和 b。在 swap() 函数里的 a 和 b 是主程序中变量 a 和 b 的复制品。
在主程序中调用 swap() 函数时,系统分别为主程序和 swap() 函数分配两块栈区,用于保存它们的局部变量。将主程序中的 a、b 变量作为参数值传入 swap() 函数,实际上是在 swap() 函数栈区中重新产生了两个变量 a、b,并将主程序栈区的 a、b 变量的值分别赋值给 swap() 函数栈区中的 a、b 参数。此时系统存在两个 a 变量、两个 b 变量,只是存在于不同的栈区中。
参数值传递实质:当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋给函数的形参变量,在函数中操作的并不是实际的实参变量。
要注意的是,如果参数本身是可变对象(比如列表、字典等),此时同样也是采用的值传递方式,但是在函数中变量存储的是可变对象的内存地址,因此在函数中对可变对象进行修改时,修改结果会反应到原始的可变对象上。
关于参数传递机制的总结:
(1)、不管什么类型的参数,在 Python 函数中对参数直接使用 “=” 符号赋值是没用的,直接使用 “=” 符号赋值并不能改变参数。
(2)、如果要让函数修改某些数据,则可以通过把这些数据包装成列表、字典等可变对象,然后把列表、字典等可变对象作为参数传入函数,在函数中通过列表、字典的方法修改它们。这样才能改变这些数据。
6、 变量作用域
程序中定义的变量有作用范围,叫做作用域。变量分为两种:
(1)、局部变量:在函数中定义的变量,包括参数,都被称为局部变量。
(2)、全局变量:在函数外面、全局范围内定义的变量,被称为全局变量。
函数在执行时,系统为函数分配一块临时内存空间,所有局部变量被保存在这块临时空间内。函数执行完成后,这块临时内存空间就被释放,同时局部变量就失效。所以离开函数后,就不能访问局部变量。
全局变量可以在所有函数内被访问到。不管是局部变量还是全局变量,变量和它们的值就像一个“看不见”的字典,变量名是 key,变量值是字典的 value。Python 提供下面三个工具函数来获取指定范围内的“变量字典”:
(1)、globals():返回全局范围内所有变量组成的“变量字典”。
(2)、locals():返回当前局部范围内所有变量组成的“变量字典”。
(3)、vars(object):获取在指定对象范围内所有变量组成的“变量字典”。不传入 object 参数,vars() 和 locals() 作用完全相同。
globals()和locals()的区别与联系:
(1)、locals() 总是获取当前局部范围内所有变量组成的“变量字典”。因此,在全局范围内(在函数之外)调用locals() 函数,同样会获取全局范围内所有变量组成的“变量字典”;而globlas() 无论在哪里执行,总是获取全局范围内所有变量组成的“变量字典”。
(2)、通常使用 locals()和globals()获取的“变量字典”只应该被访问,不应该被修改。实际上不管使用globlas()还是locals()获取的全局范围内的“变量字典“,都可以被修改,则这种修改会真正修改全局变量本身;但通过locals()获取的局部范围内的”变量字典“,即使对它修改也不会影响局部变量。
下面用代码理解 locals() 和 globlas() 函数的使用:
1 def test(): 2 name = ‘michael‘
3 print(name) # 输出:michael
4 # 访问函数局部范围内的”变量字典“
5 print(locals()) # 输出:{‘name‘: ‘michael‘}
6 # 通过函数局部范围内的”变量数组“访问 name 变量
7 print(locals()[‘name‘]) # 输出:michael
8 # 通过 locals() 函数修改局部变量的值,即使修改了也不会对局部变量有什么影响
9 locals()[‘name‘] = ‘stark‘
10 # 再次访问 name 变量的值
11 print("modify: ", locals()[‘name‘]) # 输出:modify: michael
12 # 通过 globlas() 函数修改全局变量 x 的值
13 globals()[‘x‘] = 10
14 x = 1
15 y = 2
16 # 在全局范围内调用 globals() 函数和 locals() 函数,访问的是全局变量的”变量字典",两个函数输出的结果一样
17 print(globals()) # 输出:{..., ‘x‘: 1, ‘y‘: 2}
18 print(locals()) # 输出:{..., ‘x‘: 1, ‘y‘: 2}
19 # 在全局范围内直接使用 globlas 和 locals 函数访问全局变量
20 print(globals()[‘x‘]) # 输出:1
21 print(locals()[‘x‘]) # 输出:1
22 # 在全局范围地内使用 globlas 和 locals 函数修改全局变量的值
23 globals()[‘x‘] = 30
24 locals()[‘y‘] = 20
25 # 从输出可知,在全局范围内,使用 globlas 和 locals 函数修改全局变量的值都会修改成功
26 print("modify: ", globals()[‘x‘]) # modify: 30
27 print("modify: ", locals()[‘y‘]) # modify: 20
28
29 test() # 在函数内部使用 globals 函数修改全局变量的值也会修改成功
30 print("modify two: ", globals()[‘x‘]) # modify two: 10
从函数输出可知,locals() 函数用于访问特定范围内的所有变量组成的”变量字典“,但是不能修改特定范围内”变量字典“中变量的值。在全局范围不管使用 locals() 函数还是 globals() 函数都可以修改全局范围内”变量字典“中变量的值。globals() 函数在特定范围内也可以修改全局范围内”变量字典“中变量的值。
全局变量可以在所有函数内被访问,但是在函数内定义了与全局变量同名的变量时,此时全局变量会被局部变量遮蔽。示例如下:
1 name = ‘michael‘ 2 def test(): 3 # 直接访问 name 的全局变量 4 print(name) 5 name = ‘stark‘ 6 test()
运行这段代码,此时程序会报错,错误信息是:UnboundLocalError: local variable ‘name‘ referenced before assignment。错误信息提示在函数内访问的 name 变量还未定义,这是由于在 test() 函数中增加了 ”name=‘start‘“ 这行代码造成的。
Python 语法规定:在函数内部对不存在的变量赋值时,默认就是重新定义新的局部变量。因此这行 ”name=‘start‘“ 相当于重新定义了 name 局部变量,这样 name 全局变量就被遮蔽了,所以代码会报错。为了让程序不报错,可用两种方式修改上面代码。
第一种方式:访问被遮蔽的全局变量
在 test() 函数中希望 print 语句仍然能访问 name 全局变量,并且要在 print 语句之后重新定义 name 局部变量,也就是在函数中可能访问被遮蔽的全局变量,此时可通过 globals() 函数来实现。代码修改如下:
1 name = ‘michael‘ 2 def test(): 3 # 直接访问 name 的全局变量 4 print(globals()[‘name‘]) # 输出:michael 5 name = ‘stark‘ # 定义局部变量 6 test() 7 print(name) # 输出:michael
第二种方式:在函数中声明全局变量
为避免在函数中对全局变量赋值(不是重新定义局部变量),可使用 globals 语句来声明全局变量。代码可修改为如下形式:
1 name = ‘michael‘ 2 def test(): 3 # 先声明 name 是全局变量,后面的赋值语句不会重新定义局部变量,而是直接修改全局变量 4 global name 5 # 直接访问 name 全局变量 6 print(name) # 输出:michael 7 name = ‘stark‘ # 修改全局变量 8 test() 9 print(name) # 输出:stark
在test() 函数中的”global name“ 声明 name 为全局变量,后面对 name 赋值的语句只是对全局变量赋值,不是重新定义局部变量。
三、 局部函数
Python 支持在函数体内定义函数,这种放在函数体内定义的函数称为局部函数。默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭(enclosing)函数内有效,其封闭函数也可以返回局部函数,以便程序在其他作用域中使用局部函数。局部函数示例:
1 def foo(type, nn): 2 """定义一个函数,该函数包含局部函数"""
3 def square(n): 4 """定义一个计算平方的局部函数"""
5 return n * n 6 def cube(n): 7 """定义一个计算立方的局部函数"""
8 return n * n * n 9 def factorial(n): 10 """定义一个计算阶乘的局部函数"""
11 result = 1
12 for i in range(2, n + 1): 13 result *= i 14 return result 15 # 调用局部函数
16 if type == ‘square‘: 17 return square(nn) 18 elif type == ‘cube‘: 19 return cube(nn) 20 else: 21 return factorial(nn) 22 print(foo(‘square‘, 5)) # 输出:25
23 print(foo(‘cube‘, 3)) # 输出:27
24 print(foo(‘‘, 3)) # 输出:6
这里的 foo() 函数体内定义了3个局部函数,foo() 函数根据参数选择调用不同的局部函数。如果封闭函数(foo())没有返回局部函数,那么局部函数只能在封闭函数内部调用。例如上面的 foo() 函数。
另一种情况是,封闭函数将局部函数返回,且程序使用变量保存了封闭函数的返回值,那么这些局部函数的作用域就会被扩大,程序可通过该变量自由的调用它们,就像它们是全局函数一样。
局部函数内的变量也会遮蔽它所在函数内的局部变量。示例如下:
1 def foo(): 2 # 局部变量 name
3 name = ‘michael‘
4 def bar(): 5 # 访问 bar 函数所在 foo 函数内的 name 局部变量
6 print(name) # michael
7 name = ‘stark‘
8 bar() 9 foo()
运行这段代码,出现错误提示“UnboundLocalError: local variable ‘name‘ referenced before assignment”。这错误是由于局部变量遮蔽局部变量导致的,在 bar() 函数中定义的 name 局部变量遮蔽了它所在 foo() 函数内的 name 局部变量,因此导致程序中 print 语句代码报错。
为了使 bar() 函数内的“name = ‘stark‘” 赋值语句不是定义新的局部变量,只是访问它所在 foo() 函数内的 name 局部变量,可使用Python 提供的 nonlocal 关键字,通过 nonlocal 语句即可声明访问赋值语句只是访问该函数所在函数内的局部变量。示例如下:
1 def foo(): 2 # 局部变量 name
3 name = ‘michael‘
4 def bar(): 5 # 访问 bar 函数所在 foo 函数内的 name 局部变量
6 nonlocal name 7 print(name) # michael
8 name = ‘stark‘
9 bar() 10 foo()
在 foo() 函数内增加 “nonlocal name” 语句后,在 bar() 函数中的 “name = ‘stark‘” 就不再是定义新的局部变量,而是访问它所在函数(foo())内的 name 局部变量。
nonlocal 的功能和 global 功能大致相似,区别是 global 用于声明全局变量,而 nonlocal 用于声明访问当前函数所在函数内的局部变量。