当前位置 : 主页 > 编程语言 > python >

part5-1 Python 函数(递归函数、参数传递方式、变量作用域、局部函数)

来源:互联网 收集:自由互联 发布时间:2021-06-25
函数的特点: (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 用于声明访问当前函数所在函数内的局部变量。
网友评论