zoukankan      html  css  js  c++  java
  • part5-1 Python 函数(递归函数、参数传递方式、变量作用域、局部函数)

    函数的特点:
    (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)
     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 用于声明访问当前函数所在函数内的局部变量。

  • 相关阅读:
    Guake — 一个新的下拉式终端 — LinuxTOY
    登录时提示出错
    WebOS开发环境搭建
    整理Windows Phone 7教程(很全面)
    如何在 Windows Phone 的代码中创建应用程序栏
    Silverlight for Windows Phone Toolkit升级说明
    WindowsPhone控件详解及引用外部控件Silverlight Toolkit
    WindowsPhone统计图表控件 第三方控件visifire
    WindowsPhone第三方控件Resco MobileForms Toolkit 2012
    Hadoop学习与使用之基本操作命令
  • 原文地址:https://www.cnblogs.com/Micro0623/p/11655636.html
Copyright © 2011-2022 走看看