Python学习笔记 - 函数和lambda表达式 (1)

前言

        函数是执行特定任务的一段代码,程序通过将一段代码定义成函数,并为该函数指定一个函数名,这样即可在需要的时候多次调用这段代码。因此,函数是代码复用的重要方式。学习函数需要重点掌握函数定义、函数调用的方法。

        与函数紧密相关的另一个知识点是 lambda 表达式。lambda 表达式可作为表达式、函数参数或函数返回值,因此使用 lambda 表达式可以使程序更加简洁。

一、函数入门

函数是 Python 程序的重要组成单位,一个 Python 程序可以由多个函数组成。

在之前的博文中,我们使用到了一些函数,如 len()、min() 和 max()等,这些函数都是由 Python 官方提供的,称为内置函数(Build-in Functions, BIF)。使用函数是真正开始编程的第一步。

1.1 理解函数

        比如在程序中定义了一段代码,这段代码用于实现一个特定的功能。问题来了:如果下次需要实现同样的功能,难道要把前面定义的代码重写一次?如果这样做实在是太傻了,这意味着:每次当程序需要实现该功能时,都要将前面定义的代码复制一次。

        正确的做法是:将实现特定功能的代码定义成一个函数,每次当程序需要实现该功能时,只要执行(调用)函数即可。

        通俗来讲,所谓函数,就是指为一段实现特定功能的代码 “取” 一个名字,以后只需通过该名字来执行(调用)这段代码。

        通常,函数可以接收零个或多个参数,也可以返回零个或多个值。从函数使用者的角度来看,函数就像一个“黑匣子”,程序将零个或多个参数传入这个“黑匣子”,该“黑匣子”经过一番计算即可返回零个或多个值。

        对于“黑匣子”的内部细节(就是函数的内部实现细节),函数的使用者并不需要关心。就像前面在调用 len()、max()、min() 等函数时,我们只负责传入参数、接收返回值,至于函数内部的实现细节,我们并不关心。如下图 1 所示为函数调用示意图。

图1  函数调用示意图

从函数定义者(实现函数的人)的角度来看,其至少需要考虑清楚一下3点:

  • 函数需要几个关键的需要动态变化的数据,这些数据应该被定义成函数的参数。
  • 函数需要传输几个重要的数据(就是调用者希望得到的数据),这些数据应该被定义成返回值。
  • 函数的内部实现过程。

从上面的介绍不难看出,定义函数比调用函数要难得多,对于实现过程复杂的函数,定义函数本身就很费力,需要多实践和多总结,才能写成高效实用的函数。

1.2 定义函数和调用函数

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

def 函数名(形参列表):
    // 由零条或多条语句组成的函数体
    ...
    [return [返回值]]

Python作为解释型编程语言,其函数必须先定义后调用,也就是定义函数必须在调用函数之前,否则会发生错误。

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

  • 函数名:从语法角度来看,函数名只要是一个合法的标识符即可;从程序的可读性角度来看,函数名应该由一个或多个有意义的单词连缀而成,每个单词的字母全部小写,单词与单词之间使用下划线分隔。
  • 形参列表:用于定义该函数可以接收的参数。形参列表有多个形参名组成,多个形参名之间用英文逗号隔开。一旦在定义函数时指定了形参列表,调用该函数时就必须传入对应的参数值——谁调用函数,谁负责为形参赋值。当然,函数也可以没有参数。
  • 函数返回值:如果函数有返回数据,就需要在函数体最后使用 return 语句将数据返回;如果没有返回数据,则函数体中可以使用 return None 或 省略 return 语句。

在函数体中,多条可执行语句之间有严格的执行顺序,排在函数体前面的语句总是先执行,排在函数体后面的语句总是后执行。

示例程序:function_test.py

# coding=utf-8
# 代码文件: 函数/function_test.py
# Python函数的运用示例

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

# 定义一个函数,声明一个形参
def say_hi(name):
    print("=== 正在执行say_hi()函数 ===")
    return name + ", 您好!"

a = 6
b = 9
# 调用my_max()函数,将函数返回值赋值给result变量
result = my_max(a, b)     # 调用函数
print("result:", result)

# 调用say_hi()函数,直接输出函数的返回值
print(say_hi("Godlike"))  # 调用函数

运行结果:

>python function_test.py
result: 9
=== 正在执行say_hi()函数 ===
Godlike, 您好!

分析:从上面的运行结果可以看出,当程序调用一个函数时,既可以把调用函数的返回值赋值给指定的变量,也可以将函数的返回值直接传给另一个函数,作为另一个函数的实参。

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

例如上面的 my_max() 函数,实际上可以简写为如下形式:

def my_max(x, y):
    # return返回一个if-elset条件表达式
    return (x if x > y else y)

1.3 为函数提供文档

        可以使用 Python 内置的 help() 函数查看其他函数的帮助文档,通过查看指定函数的帮助信息,对于 Python 开发者来说非常重要。

        我们还可以为自定义函数编写说明文档——只要把一段字符串放在函数声明之后、函数体之前,这段字符串将被作为函数的一部分,这个文档就是函数的说明文档。

        程序既可通过 help() 函数查看函数的说明文档,也可通过函数的 __doc__ 属性访问函数的说明文档。

下面示例程序示范了为函数编写说明文档的用法。

代码清单:function_doc.py

# coding=utf-8
# 代码文件: 函数/function_doc.py
# Python为函数提供文档的示例程序

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

# 使用help()函数查看my_max()的帮助文档
help(my_max)
print('-----------------------------------')
print(my_max.__doc__)

运行结果:

F:\python_work\函数和lambda表达式>python function_doc.py
Help on function my_max in module __main__:

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

-----------------------------------

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

分析:上面程序使用多行字符串注释的语法为 my_max() 函数编写了说明文档,接下来即可通过 help() 函数查看该函数的说明文档,也可通过函数对象的 __doc__  属性访问该函数的说明文档。

1.4 递归函数

在一个函数体内调用它自身,称为函数递归。函数递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

例如有如下一道数学题。已知有一个数列:f(0) = 1, f(1)=4, f(n+2)=2 * f(n+1) + f(n),其中 n 是大于 0 的整数,求 f(10) 的值。这道题可以使用递归来求解。下面程序将定义一个 fn(n) 函数,用于计算 f(10) 的值。

# coding=utf-8
# 代码文件: 函数/recursive_test.py
# 递归函数的用法

# 定义一个递归函数
def fn(n):
    if n == 0:
        return 1  # --1
    if n == 1:
        return 4  # --2
    else:
        # 在函数体中调用自身,就是递归函数
        return 2 * fn(n - 1) + fn(n - 2)   # --3

# 输出fn(10)的结果
print("fn(10)=", fn(10))  # 输出 fn(10)= 10497

分析:在上面的 fn() 函数体中再次调用了 fn() 函数,这就是递归函数。注意在 fn() 函数体中调用 fn 的形式:

return 2 * fn(n - 1) + fn(n - 2)

        对于 f(10),即等于 2 * f(9) + f(8),其中 f(9) 又等于 2 * f(8) + f(7) ...... 以此类推,最终会计算到 fn(2) = 2 * f(1) + f(0),即 fn(2) 是可计算得到的,这样递归带来的隐式循环就有结束的时候,然后一路反算回去,最后就可以得到 fn(10) 的值。

        仔细看上面递归的过程,当一个函数不断地调用它自身时,必须在某个时刻函数的返回值是确定的,即不再调用它自身;否则,这种递归就变成无穷递归了,类似于死循环。因此,在定义递归函数时有一条最重要的规定:递归一定要向已知方向进行

        例如,如果把上面数学题改为这样的条件:已知有一个数列,f(20) = 1, f(21) = 4, f(n+2) = 2 * f(n+1) + f(n),其中 n 是大于 0 的整数,求 f(10) 的值。 那么 fn() 的函数体就应该改为如下形式:

def fn(n):
    if n == 20:
        return 1
    if n == 21:
        return 4
    else:
        # 在函数体中调用自身,就是递归函数
        return fn(n + 2) - 2 * fn(n + 1)

分析:从上面的 fn() 函数来看,当程序要计算 fn(10) 等于 fn(12) - 2 * fn(11),而 fn(11) = fn(13) - 2 * fn(12) ...... 以此类推,直到 fn(19) 等于 fn(21) - 2 * fn(20),此时就可以得到 fn(19) 的值,然后依次反算得到 fn(10) 的值。这就是递归的主要规则:对于求 fn(10) 的话,如果 fn(0) 和 fn(1) 是已知的,则应该采用 fn(n) = 2 * fn(n - 1) + fn(n - 2) 的形式递归,因为小的一端是已知的;如果 fn(20) 和 fn(21) 是已知的,则应该采用 fn(n) = fn(n+2) - 2 * fn(n + 1) 的形式递归,因为大的一端是已知的。

        递归是非常有用的,例如程序希望遍历某个路径下的所有文件,但这个路径下的目录深度是未知的,那么就可以使用递归来实现这个需求。系统可定义一个函数,该函数接收一个文件路径作为参数,该函数可遍历出当前路径下的所有文件和文件路径——在该函数的函数体中再次调用函数自身来处理该路径下的所有文件路径。

        总之,只要在一个函数的函数体中调用了函数自身,就是递归函数。递归一定要向已知方向进行

二、函数参数

        Python 中的函数参数很灵活,具体体现在传递参数有多个形式上。在定义函数时定义的参数,称为形式参数(简称形参),这些形参的值要等到调用时才能确定下来,由函数的调用者负责为形参传入参数值。简单来说就是,谁调用函数,谁负责传入参数值

2.1 使用关键字参数调用函数

        Python 的参数名不是无意义的,Python 允许在调用函数时通过名字来传入参数值。因此,Python 函数的参数名应该具有更好的语义——程序可以立刻明确传入函数的每个参数的含义。

        按照形参位置传入的参数被称为位置参数。如果使用位置参数的方式来传入参数值,则必须严格按照定义函数时指定的顺序来传入参数值;如果根据参数名来传入参数值,则无需遵守定义形参的顺序,这种方式被称为关键字(keyword)参数

例如如下程序:named_param_test.py

# coding=utf-8
# 代码文件: 函数/named_param_test.py
# 使用关键字参数形式传入函数参数值

# 定义一个计算周长的函数
def girth(width, height):                # --1
    print("width:", width)
    print("height :", height )
    return 2 * (width + height )

# 传统调用函数的方式,根据位置传入参数值
print(girth(3.5, 4.8))                   # --2
# 根据关键字参数来传入参数值
print(girth(width = 3.5, height = 4.8))  # --3
# 在使用关键字参数时可调换参数位置
print(girth(height = 4.8, width = 3.5))  # --4
# 部分使用关键字参数,部分使用位置参数
print(girth(3.5, height = 4.8))          # --5

运行结果:

F:\python_work\函数和lambda表达式>python named_param_test.py
width: 3.5
height : 4.8
16.6
width: 3.5
height : 4.8
16.6
width: 3.5
height : 4.8
16.6
width: 3.5
height : 4.8
16.6

(1)上述代码第1处定义了一个简单的 girth() 函数,该函数包含 width、height 两个参数,该函数与前面定义的函数并没有什么区别。

(2)代码第2处根据位置参数的方式来传入参数,调用函数的实参与定义函数的形参根据位置的先后顺序依次对应。

(3)代码第3处根据位置参数的方式传入参数,根据 “形参名=实参值” 的方式传入参数。

(4)在使用关键字参数调用时,可交换参数的位置,见代码第4处。

(5)还可混合使用位置参数和关键字参数,见代码第5处。

注意需要说明的是,如果希望在调用函数时混合使用位置参数和关键字参数,则关键字参数必须位于位置参数之后,并且其后的所有参数都必须采用关键字参数形式传递。换句话说,在关键字参数之后的传参方式只能是关键字参数

例如如下代码是错误的:

# 必须将位置参数放在关键字参数之前,下面代码错误
print(girth(width = 3.5, 4.8))

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

SyntaxError: positional argument follows keyword argument
语法错误: 位置参数位于关键字参数之后

2.2 函数参数默认值

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

        为形参指定默认值的语法格式如下:

形参名 = 默认值

从上面的语法格式可以看出,形参的默认值紧跟在形参之后,中间以英文 “=” 隔开。

例如,如下程序为 name、message 函数形参指定了默认值。

程序清单:default_param_test.py

# coding=utf-8
# 代码文件: 函数/default_param_test.py
# 带有默认值参数的函数用法

# 定义一个函数,该函数有两个参数,并为这两个参数指定默认值
def say_hi(name = "孙悟空", message = "欢迎来到软件世界"):  # --1
    print(name, ", 您好")
    print("消息是:", message)

# 调用函数时,全部使用默认值
say_hi()                                 # --2
# 调用函数时,只有message参数使用默认值
say_hi("白骨精")                          # --3
# 调用函数时,两个参数都不使用默认值
say_hi("白骨精", "欢迎学习Python")         # --4
# 调用函数时,只有name参数使用默认值,而message参数使用关键字参数形式
say_hi(message = "欢迎学习Python")        # --5

运行结果:

F:\python_work\函数和lambda表达式>python default_param_test.py
孙悟空 , 您好
消息是: 欢迎来到软件世界
白骨精 , 您好
消息是: 欢迎来到软件世界
白骨精 , 您好
消息是: 欢迎学习Python
孙悟空 , 您好
消息是: 欢迎学习Python

(1)上述程序中在定义 say_hi() 函数时为 name、message 形参指定了默认值,见代码第 1 处。因此程序中代码第 2 处调用 say_hi() 函数时没有为 name、message 参数指定实参值,此时 name、message 参数将会使用其默认值。

(2)代码第 3 处调用 say_hi() 函数时,为 name 参数(使用位置参数)指定了实参值,此时 message 参数将会使用默认值。

(3)代码第 4 处调用 say_hi() 函数时,为 name、message 参数(使用位置参数)都指定了实参值,因此这两个参数都使用调用者传入的实参值。

(4)代码第 5 处调用 say_hi() 函数时,只为 message 参数(使用关键字参数)传入了实参值,此时 name 参数将使用默认值。

从上面程序可以看出,如果只传入一个位置参数,由于该参数位于第一位,系统会将该参数值传给 name 参数。因此,我们不能按如下方式调用 say_hi() 函数:

say_hi("欢迎学习Python")

上面调用时传入的实参值 "欢迎学习Python" 字符串将传给 name 参数,而不是 message 参数。

我们也不能按如下方式来调用 say_hi() 函数:

say_hi(name = "白骨精", "欢迎学习Python")

因为Python规定:关键字参数必须位于位置参数之后。因此提示错误信息:SyntaxError: positional argument follows keyword argument

那么,我们能不能单纯地将上面两个参数交换位置呢?

say_hi("欢迎学习Python", name = "白骨精")

        上面调用依然是错误的,因为第一个字符串没有指定关键字参数,因此将使用位置参数为 name 参数传入参数值,第二个参数使用关键字参数的形式再次为 name 参数传入参数值,这意味着两个参数值都会传给 name 参数,程序为 name 参数传入了多个参数值。因此提示错误信息:TypeError: say_hi() got multiple values for argument 'name'

        将函数调用改为如下两种形式是正确的:

say_hi("白骨精", message = "欢迎学习Python")         # --1
say_hi(name = "白骨精", message = "欢迎学习Python")  # --2

(1)上述代码第 1 处先使用位置参数为 name 参数传入参数值,再使用关键字参数为 message 参数传入参数值。

(2)代码第 2 处中的 name、message 参数都使用关键字参数传入参数值。

        由于 Python 要求在调用函数时,关键字参数必须位于位置参数之后,因此在定义函数时指定了默认值的参数(关键字参数)必须放在不是默认值的参数之后

例如如下示例代码:default_param_test2.py

# coding=utf-8
# 代码文件: 函数/default_param_test2.py
# 带有默认值参数的函数用法

# 定义一个打印三角形的函数,有默认值的参数必须放在后面
def printTriangle(char, height = 5):
    for i in range(height + 1):
        # 先打印一排空格
        for j in range(height - 1):
            print(' ', end = '')
        # 再打印一排特殊字符
        for j in range(2 * i - 1):
            print(char, end = '')
        print()

printTriangle('@', 6)          # --1
printTriangle('#', height = 7) # --2
printTriangle(char = '*')      # --3

(1)上述程序定义了一个 printTriangle() 函数,该函数的第一个 char 参数没有默认值,第二个 height 参数有默认值。

(2)代码第 1 处调用 printTriangle() 函数时,程序使用两个位置参数分别为 char、height 传入参数值,这当然是允许的。

(3)代码第 2 处调用 printTriangle() 函数时,第一个参数使用位置参数,那么该参数值将传给 char 参数,第二个参数使用关键字参数为 height 参数传入参数值,这也是允许的,遵循关键字参数位于位置参数之后的规则。

(4)代码 3 处调用 printTriangle() 函数时,只使用关键字参数为 char 参数传入参数值,此时 height 参数将使用默认值,这也是符合语法的。

注意Python 要求将带默认值的参数定义在形参列表的最后

2.3 可变参数

        很多编程语言定义参数个数可变的函数,这样可以在调用函数时传入任意多个参数。Python 可变参数有两种,允许在形参前面添加一个星号(*)或两个星号(**)的形式。一个星号(*)的可变参数在函数中被组装成一个元组,两个星号(**)的可变参数在函数中被组装成一个字典

1.  * 可变参数

示例:下面程序定义了一个参数可变的函数。程序清单:var_args.py

# coding=utf-8
# 代码文件: 函数/var_args.py
# 形参个数可变的函数用法

# 定义了支持参数可变的函数
def test(a, *books):
    print(books)
    # books 被当成元组处理
    for b in books:
        print(b)
    # 输出形参a的值
    print(a)

# 调用test()函数
test(5, "Python", "Java", "C/C++")

运行结果:

> python var_args.py
('Python', 'Java', 'C/C++')
Python
Java
C/C++
5

(1)从上面的运行结果可以看出,当调用 test() 函数时,books 参数可以传入多个字符串作为参数值。从 test() 函数体代码来看,可变参数的本质是一个元组:Python 会将传给 books 参数的多个值组装成一个元组。

        Python 允许个数可变的形参可以处于形参列表的任意位置(不要求是形参列表的最后一个参数),但 Python 要求一个函数最多只能带一个 “普通” 可变参数的形参,并且当可变参数不是最后一个参数时,后面的参数需要采用关键字参数形式传递;否则,程序会把所传入的多个值都当成是传给可变参数的实参值了。

例如,如下代码:var_args2.py

# coding=utf-8
# 代码文件: 函数/var_args2.py
# 形参个数可变的函数用法——可变参数的位置不是形参列表的最后一个参数时的情况

# 定义了支持参数可变的函数
def test(*books, num):
    print(books)
    # books 被当成元组处理
    for b in books:
        print(b)
    # 输出形参num的值
    print(num)

# 调用test()函数
test("Python", "Java", "C/C++", num = 20)  # --1

运行结果:

> python var_args2.py
('Python', 'Java', 'C/C++')
Python
Java
C/C++
20

如果将代码第1处的语句修改为如下:

test("Python", "Java", "C/C++", 20)

运行时,将会报如下错误:

TypeError: test() missing 1 required keyword-only argument: 'num'

test() 函数缺少一个需要关键字参数: 'num'

2.  ** 可变参数

        Python 还支持关键字参数形式的可变参数,此时,Python 会将这种关键字参数组装成一个字典(dict)。在定义函数时,需要在形参名前面添加两个星号(**)。

例如如下代码:var_args3.py

# coding=utf-8
# 代码文件: 函数/var_args3.py
# 可变参数被组装成字典的函数用法

# 定义了支持参数可变的函数
def test(x, y, z, **score):
    print(x, y, z)
    print(score)

# 调用test()函数
test(1, 2, 3, 语文=89, 数学=98, 英语=96)  # --1

运行结果:

> python var_args3.py
1 2 3
{'语文': 89, '数学': 98, '英语': 96}

(1)从运行结果来看,上述代码第 1 处调用 test() 函数的语句中,最后三个关键字参数被组装成了一个字典。

注意在定义函数时,** 可变参数必须放在常规参数之后,否则会发生错误。本例中如果将函数定义改为:

def test(**score, x, y, z) 形式,运行时会报语法错误:SyntaxError: invalid syntax。

在定义一个函数时,可同时包含一个支持“普通”形式的可变参数和一个支持关键字参数形式的可变参数

例如如下代码:var_args4.py

# coding=utf-8
# 代码文件: 函数/var_args4.py
# 同时支持普通形式的可变参数和关键字参数形式的可变参数的函数用法

# 定义了支持参数可变的函数
def test(x, y, z, *books, **scores):
    print("type(books): %s, type(socre): %s" %(type(books), type(scores)))
    print(x, y, z)
    print(books)
    print(scores)

# 调用test()函数
test(1, 2, 3, "Python", "Java", "C/C++", 语文=89, 数学=98, 英语=96)  # --1

运行结果:

> python var_args4.py
type(books): <class 'tuple'>, type(socre): <class 'dict'>
1 2 3
('Python', 'Java', 'C/C++')
{'语文': 89, '数学': 98, '英语': 96}

(1)从运行结果可以看出,形参 books 的数据类型为元组(tuple),形参 score 的数据类型为字典(dict)。

(2)上述代码第 1 处在调用 test() 函数的语句中,实参 "Python", "Java", "C/C++" 被组装成了一个元组传递给形参books,而实参 语文=89, 数学=98, 英语=96 被组装成了一个字典传递给形参scores。因此,上述代码第 1 处中调用 test() 函数的语句还可以改写为如下形式:

# 定义一个元组对象 books
books = ("Python", "Java", "C/C++")
# 定义一个字典对象 scores
scores = {'语文': 89, '数学': 98, '英语': 96}
# 调用 test() 函数
test(1, 2, 3, *books, **scores)  # --2

(3)上述代码第 2 处的 *books 表示将元组对象 books 拆包为 "Python", "Java", "C/C++" 字符串的形式;**score 表示将字典对象 scores 拆包为 key=value 键值对的形式。修改后的程序的运行结果同上面一样。

2.4 逆向参数收集

所谓逆向参数收集,指的是在程序已有列表、元组、字典等对象的前提下,把它们的元素“拆开”后传递给函数的形参

逆向参数收集需要在传入的列表、元组参数之前添加一个星号(*),在字典参数之前添加两个星号(**)。

例如如下代码:var_args5.py

# coding=utf-8
# 代码文件: 函数/var_args5.py
# 逆向参数收集的函数用法

# 定义一个函数
def test(name, message):
    print("用户是:", name)
    print("欢迎消息:", message)

my_list = ['Godlike', 'Welcome to Python World.'] # --1
test(*my_list)  # 调用test函数 --2

运行结果:

> python var_args5.py
用户是: Godlike
欢迎消息: Welcome to Python World.

(1)上述代码定义了一个包含两个参数的函数,而程序中的 my_list 列表包含了两个元素,为了让程序将 my_list 列表的两个元素传递给 test() 函数,程序在传入的 my_list 参数之前添加了一个信号。

        实际上,即使是一个可变参数,如果程序需要将一个元组传递给该可变参数,同样需要使用逆向参数收集。例如如下代码:var_args6.py

# coding=utf-8
# 代码文件: 函数/var_args6.py
# 逆向参数收集的函数用法,函数中有可变参数

# 定义一个函数
def foo(name, *nums):         # --1
    print("type(nums):", type(nums))
    print("name参数:", name)
    print("nums参数:", nums)

my_tuple = (1, 2, 3)
# 使用逆向收集,将 my_tuple 元组的元素传递给 nums 参数
foo('Godlike', *my_tuple)     # --2

运行结果:

>python var_args6.py
type(nums): <class 'tuple'>
name参数: Godlike
nums参数: (1, 2, 3)

(1)上述代码第1处定义了一个 foo() 函数,该函数有一个可变参数 nums。

(2)在代码第2处,调用 foo() 函数,将 'Godlike' 传递给形参 name,然后使用逆向收集将 my_tuple 包含的所有元素传递给 nums 参数,nums 再将 my_tuple 的所有元素组装成一个元组,此时形参 nums 的数据类型为:<class 'tuple'>。

        此外,还可使用如下方式调用 foo() 函数。

# 使用逆向收集,将 my_tuple 元组的第一个元素传递给 name 参数,剩下元素传递给 nums 参数
foo(*my_tuple)

此时程序会对 my_tuple 元组进行逆向收集,其中第一个元素传递给 name 参数,后面剩下的元素传递给 nums 参数。运行上面代码,输出结果为:

type(nums): <class 'tuple'>
name参数: 1
nums参数: (2, 3)

        如果不使用逆向收集(不在元组实参之前添加星号),整个元组将会作为一个参数,而不是将元组的元素作为多个参数。例如按如下方式调用 foo() 函数。

# 不使用逆向收集,my_tuple 元组作为一个整体传递给 name 参数
foo(my_tuple)

上面的 foo() 函数调用没有使用逆向收集,因此 my_tuple 整体作为一个参数传递给 name 参数。运行上面代码,输出结果为:

type(nums): <class 'tuple'>
name参数: (1, 2, 3)
nums参数: ()

字典也支持逆向参数收集,字典将会以关键字参数的形式传入。例如如下代码:var_args7.py

# coding=utf-8
# 代码文件: 函数/var_args7.py
# 字典类型的逆向参数收集的函数用法

# 定义一个函数
def bar(book, price, desc):
    print("书名:", book)
    print("价格:", price)
    print("描述信息:", desc)

# 定义一个字典对象
my_dict = {'book': 'Python Programming Language', 'price': 89, 'desc': 'a vary good Python learning book.'}
# 按逆向收集的方式将 my_dict 的多个 key-value 对传递给 bar() 函数
bar(**my_dict)

(1)上述代码中定义的 bar() 函数有三个参数。接下来程序定义了一个 my_dict 字典对象,该字典正好包含三个 key-value 对,程序使用逆向收集可将 my_dict 包含的三个 key-value 对以关键字参数的形式传递给 bar() 函数的三个形参。即:

{'book': 'Python Programming Language', 'price': 89, 'desc': 'a vary good Python learning book.'}
将字典键值对元素转换为函数的关键字参数形式:
book = 'Python Programming Language', price = 89, desc = 'a vary good Python learning book.'
然后分别传递给 bar() 函数的 book, price, desc 三个形参

2.5 函数的参数传递机制

        Python 的参数值(实参)是如何传入函数的呢?这是由 Python 函数的参数传递机制来控制的。Python 中函数的参数传递机制都是“值传递”。所谓值传递,就是将实参值的副本(复制品)传入函数,而实参本身不会受任何影响。

下面程序演示了函数参数传递的效果:

# coding=utf-8
# 代码文件: 函数/int_transfer_test.py
# 函数参数传递

def swap(a, b):
	# 下面的代码实现a、b变量的交换
	a, b = b, a
	print("In swap(): a =", a, " b =", b)

a = 6
b = 9
swap(a, b)
print("After swap: a =", a, " b=", b)

运行结果:

> python int_transfer_test.py
In swap(): a = 9  b = 6
After swap: a = 6  b= 9

        从上面的运行结果看,在 swap() 函数里,a 和 b 的值分别是 9、6,交换接收后,变量 a 和 b 的值依然是 6、9。从这个运行结果可以看出,程序中实际定义的变量 a 和 b,并不是 swap() 函数里的 a 和 b。正如前面所讲的,a 和 b 只是主程序中变量 a 和 b 的复制品。下面通过示意图来说明上面程序的执行过程。

        上面程序开始定义了 a、b 两个局部变量,这两个变量在内存中的存储示意图如图 2 所示。

        当执行 swap() 函数时,系统进入 swap() 函数,并将主程序中的 a、b 变量作为参数值传入 swap() 函数,但传入 swap() 函数的只是实参变量 a、b 的副本,而不是 a、b 本身。进入 swap() 函数后,系统中产生了 4 个变量,这 4 个变量在内存中的存储示意图如图 3 所示。

        当在主程序调用 swap() 函数时,系统分别为主程序和 swap() 函数分配两块栈区,用于保存它们的局部变量。将主程序中的 a、b 变量作为参数值传入 swap() 函数,实际上是在 swap() 函数栈区中重新产生了两个变量 a、b,并将主程序栈区中 a、b 变量的值分别赋值给 swap() 函数栈区中的形参变量 a、b (就是对 swap() 函数的形参 a、b两个变量进行初始化操作)。此时,系统存在两个 a 变量、两个 b 变量,只是存在于不同的栈区中而已。

        程序在 swap() 函数中交换 a、b 两个变量的值,实际上是对图 3 中灰色区域的 a、b 变量进行交换。交换结束后,输出 swap() 函数中 a、b 变量的值,可以看到 a 的值为 9,b 的值为 6。

        对比上图 2 和 图 4,可以看到两个示意图中主程序栈区中的 a、b 的值并未有任何改变,程序改变的只是 swap() 函数栈区中的 a、b 的值。这就是值传递的实质当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋值给函数的形参变量,在函数中操作的并不是实际的实参变量

2.6 变量作用域

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

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

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

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

        不管是在函数的局部范围内还是在全局范围内,都可以存在多个变量,每个变量“持有”该变量的值。从这个角度来看,不管是局部范围还是全局范围,这些变量和它们的值就像一个“看不见”的字典(dict),其中变量名就是字典的 key,变量值就是字典的 value。

        实际上,Python 提供了如下三个工具函数来获取指定范围内的 “变量字典”。

  • globals():该函数返回全局范围内所有变量组成的 “变量字典”。
  • locals():该函数返回当前局部范围内所有变量组成的 “变量字典”。
  • vars(object):获取在指定对象范围内所有变量组成的 “变量字典”。如果不传入 object 参数,vars() 和 locals() 的作用完全相同。

globals() 和 locals() 看似完全不同,但它们实际上也是有联系的,关于这两个函数的区别和联系大致有以下两点:

  • locals() 总是获取当前局部范围内所有变量组成的 “变量字典”,因此,如果在全局范围内(在函数之外)调用 locals() 函数,同样会获取全局范围内所有变量组成的 “变量字典”;而 globals() 无论在哪里执行,总是获取全局范围内所有变量组成的 “变量字典”。
  • 一般来说,使用 locals() 和 globals() 获取的 “变量字典” 只应该被访问,不应该被修改。但实际上,不管是使用 globals() 还是使用 locals() 获取的全局范围内的 “变量字典”,都可以被修改,而这种修改会真正改变全局变量本身;但通过 locals() 获取的局部范围内的 “变量字典”,即使对它修改也不会改变局部变量的值。

下面程序示范了如何使用 locals()、globals() 函数访问局部范围和全局范围内的 “变量字典”。

# coding=utf-8
# 代码文件: 函数/locals_test.py
# 变量作用域:locals()函数的使用

def test():
    age = 20    # 定义一个局部变量
    print(age)  # 输出20
    # 访问函数局部范围内的"变量字典"
    print(locals()) # 输出 {'age': 20}
    # 通过函数局部范围内的"变量字典"改变age变量的值
    locals()['age'] = 12
    # 再次访问 age 变量的值
    print('xxx', age) # 依然输出 20
    # 通过 globals 函数修改 x 全局变量
    globals()['x'] = 19

x = 5  # 定义一个全局变量 x
y = 20 # 定义一个全局变量 y

print(globals()) # 输出 {..., 'x': 5, 'y': 20}
# 在全局范围内使用 locals 函数,访问的是全局变量的 "变量数组"
print(locals())  # {..., 'x': 5, 'y': 20}
# 直接访问 x 全局变量
print(x) # 输出 5
test()   # 调用 test() 函数
# 通过全局变量的"变量数组"访问 x 全局变量
print(globals()['x']) # 输出 19 (因为全局变量x的值在test()函数中被修改为19了)
# 通过全局变量的"变量数组"对 x 全局变量赋值
globals()['x'] = 39
print(x) # 输出 39
# 在全局范围内使用 locals 函数对 x 全局变量赋值
locals()['x'] = 99
print(x) # 输出 99

运行结果:

{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000020BA2C34A30>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'F:\\python_work\\函数和lambda
表达式\\locals_test.py', '__cached__': None, 'test': <function test at 0x0000020BA2823E20>, 'x': 5, 'y': 20}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000020BA2C34A30>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'F:\\python_work\\函数和lambda
表达式\\locals_test.py', '__cached__': None, 'test': <function test at 0x0000020BA2823E20>, 'x': 5, 'y': 20}
5
20
{'age': 20}
xxx 20
19
39
99

        从上面程序可以清楚地看出,locals() 函数用于访问特定访问内的所有变量组成的 “变量字典”,而 globals() 函数则用于访问全局范围内的全局变量组成的 “变量字典”。也就是说,locals() 函数在函数外调用时,是访问全局范围内的全局变量组成的 “变量字典”,而在函数内调用时,只是访问函数范围内的局部变量组成的 “变量字典”。

提示】在使用 globals() 或 locals() 函数访问全局变量的 “变量字典”时,将会看到程序输出的 “变量字典”,默认包含了很多变量,这些都是 Python 主程序内置的,我们可以暂时不要理会它们。

        全局变量默认可以在所有函数内被访问,但如果在函数中定义了与全局变量同名的局部变量,此时就会发生局部变量屏蔽(Hide)同名全局变量的情形。例如如下程序:

# coding=utf-8
# 代码文件: 函数/globals_test.py
# 变量作用域:globals()函数的使用

name = 'Charlie'
def test():
    # 直接访问name全局变量
    print(name) # 输出 Charlie --1
    # name = 'Godlike'

test()
print(name) # 输出 Charlie

上面程序第1处中代码直接访问 name 变量,这是允许的,此时程序将会输出 Charlie,如果在上面程序第1处代码之后增加如下一行代码。

name = 'Godlike'

再次运行该程序,将会看到如下错误。

print(name) # 输出 Charlie --1
UnboundLocalError: local variable 'name' referenced before assignment

        该错误提示代码第1处所访问的 name 变量还未定义。这是什么原因呢?这正是由于程序在 test() 函数中增加了 name = 'Godlike' 这一行代码造成的。

        Python语句规定在函数内部对不存在的变量赋值时,默认就是重新定义新的局部变量。因此新增的这行代码相当于重新定义了 name 局部变量,这样 name 全局变量就被遮蔽了,所以上面程序中第1处的代码会报错

        为了避免这个问题,可以通过下面两种方式来修改上面的程序。

1. 访问被遮蔽的全局变量

        如果程序希望第1处代码依然能访问全局变量 name,且在第1处代码之后可重新定义局部变量 name,也就是在函数中可以访问被遮蔽的全局变量,此时可通过 globals() 函数来实现,将上面的程序改为如下形式即可。

# coding=utf-8
# 代码文件: 函数/globals_right1.py
# 变量作用域:访问被遮蔽的全局变量

name = 'Charlie'
def test():
    # 通过globals()函数访问 name 全局变量
    print(globals()['name']) # 输出 Charlie --1
    name = 'Godlike'
    print(name)              # 输出 Godlike --2

test()
print(name) # 输出 Charlie

运行结果:

> python globals_right1.py
Charlie
Godlike
Charlie

2. 在函数中声明全局变量

为了避免在函数中对全局变量赋值(不是重新定义局部变量),可使用 global 语句来声明全局变量。因此,可将程序改为如下形式。

# coding=utf-8
# 代码文件: 函数/globals_right2.py
# 变量作用域:在函数中声明全局变量

name = 'Charlie'
def test():
    # 声明 name 是全局变量,后面的赋值语句不会重新定义局部变量
    global name
    # 直接访问全局变量
    print(name)        # 输出 Charlie --1
    name = 'Godlike'
    print(name)        # 输出 Godlike --2

test()
print(name) # 输出 Godlike

运行结果:

> python globals_right2.py
Charlie
Godlike
Godlike

        增加了 “global name” 声明之后,程序会把 name 变量当成全局变量,这意味着 test() 函数中对 name 赋值语句只是对全局变量的赋值,而不是重新定义局部变量 name。

2.7 函数返回值

Python 函数的返回值是比较灵活的,主要有三种形式:无返回值、单一返回值和多返回值。本节主要将解无返回值和多返回值这两种形式。

1. 无返回值函数

有的函数只是为了处理某个过程,此时可以将函数设计为无返回值的。所谓无返回值,事实上就是返回 NoneNone 表示没有实际意义的数据。

无返回值函数可以省略 return 或使用 return None

无返回值函数示例代码如下:

# coding=utf-8
# 代码文件: 函数/none_return_test.py
# 函数返回值:无返回值函数

def show_info(sep = ':', **info):  # --1
    # 定义 ** 可变参数函数
    print('-----info-----')
    for key, value in info.items():
        print('{0} {1} {2}'.format(key, sep, value))
    return     # return None 或者省略  --2

result = show_info('->', name = 'Tony', age = 18, sex = 'male')
print(result)  # 输出 None
print()        # 输出一个空行

def sum(*numbers, multiple = 1):    # --3
    # 定义 * 可变参数函数
    if len(numbers) == 0:
        return   # return None 或省略 --4
    total = 0.0
    for number in numbers:
        total += number
    return total * multiple

print(sum(30.0, 80.0))   # 输出 110.0
print(sum(multiple = 2)) # 输出 None

运行结果:

> python none_return_test.py
-----info-----
name -> Tony
age -> 18
sex -> male
None

110.0
None

分析:上述代码定义了两个函数 show_info() 和 sum()。其中 show_info() 函数只是输出一些信息,不需要返回数据,因此可以省略 return 语句。如果一定要用 return 语句,见代码第 2 处的写法,在函数结束前使用 returnreturn None 方式。对于本例中的 shoe_info() 函数强加 return 语句显然是多此一举,但是有时使用 return 或 return None 是必要的。代码第 3 处定义了 sum() 函数,如果 numbers 中数据是空的,后面的求和和计算也就没有意义了,可以在函数的开始判断 numbers 中是否与数据,如果没有数据,则使用 returnreturn None 跳出函数,见代码第 4 处。

2. 多返回值函数

        有时需要函数返回多个值,实现返回多个值的方式有很多,简单的方式是使用元组返回多个值,因为元组作为数据结构可以容纳多个数据,另外元组是可不变的,使用起来比较安全。

多返回值函数示例代码如下:

# coding=utf-8
# 代码文件: 函数/multi_return_test.py
# 多返回值函数:函数使用元组返回多个值

def position(dt, speed):   # --1
    pos_x = speed[0] * dt; # --2
    pos_y = speed[1] * dt  # --3
    return (pos_x, pos_y)  # --4

move = position(60.0, (10, -5))  # --5
print('物体位移:({0}, {1})'.format(move[0], move[1])) # --6

运行结果:

> python multi_return_test.py
物体位移:(600.0, -300.0)

分析:(1)这个示例是计算物体在指定时间和速度时的位移。第 1 处代码是定义 position() 函数,其中 dt 参数是时间增量,speed 参数是元组类型,speed 第一个元素是 X 轴上的速度,speed 第二个元素是 Y 轴上的速度。position() 函数的返回值也是元组类型。

(2)函数体中第 2 处的代码是计算X轴方向上的位移,第 3 处代码是计算Y轴方向上的位移。第 4 处的代码是将计算后的结果返回,(pos_x, pos_y) 是元组类型实例。

(3)第  5 处代码是调用 position() 函数,传递的时间是 60.0 秒,速度是 (10, 5)。第 6 处的代码是打印出输出结果。

当然,多函数返回值也可直接返回多个值,不需要将多个返回值结果构造一个元组。

如下程序示范了函数直接返回多个值的情形。

# coding=utf-8
# 代码文件: 函数/multi_return_test2.py
# 多返回值函数:函数直接返回多个值,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  # --1

my_list = [20, 15, 2.8, 'a', 35, 5.9, -1.8]
# 获取 sum_and_avg() 函数返回的多个值,多个返回值被Python自动封装成元组
tp = sum_and_avg(my_list)    # --2
print(tp)

运行结果:

> python multi_return_test2.py
(76.9, 12.816666666666668)

分析:上述程序中的第 1 处代码函数返回值返回了多个值,当在第 2 处代码调用 sum_and_avg() 函数时,该函数返回的多个值将会被自动封装成元组,因此程序看到 tp 是一个包含了两个元素(因为被调用函数返回了两个值)的元组。

此外,也可使用 Python 提供的序列解包功能,直接使用多个变量接收函数的多个返回值。例如上面程序第 2 处的代码可以修改为如下代码:

s, avg = sum_and_avg(my_list)  # --3
print(s, avg)

分析:上述第 3 处代码直接使用两个变量来接收 sum_and_avg() 函数返回的两个值,这就是利用了 Python 提供的序列解包功能。

三、局部函数(嵌套函数)

        前面所看到的函数都是在全局范围内定义的,它们都是全局函数。Python 还支持在函数体内定义函数,这种被放在函数体内定义的函数称为局部函数,也称为嵌套函数。

        在默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭(enclosing)函数内有效,其封闭函数也可以返回局部函数,以便程序在其他作用域中使用局部函数。

# coding=utf-8
# 代码文件: 函数/local_function_test.py
# 局部函数的使用

# 定义函数,该函数会包含局部函数
def get_math_func(type, nn):
	# 定义一个计算平方的局部函数
    def square(n):    # --1
        return n * n
    # 定义一个计算立方的局部函数
    def cube(n):      # --2
        return n * n * n
    # 定义一个计算阶乘的局部函数
    def factorial(n): # --3
        result = 1
        for i in range(2, n + 1):
            result *= i
        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() 函数,接下来程序的第1、2、3 处代码定义了 3 个局部函数,而 get_math_func() 函数则根据参数选择调用不同的局部函数。

        如果封闭函数没有返回局部函数,那么局部函数只能在封闭函数内部调用,如上面程序所示。

        另外,还会出现一种情况,如果封闭函数将局部函数返回,且程序使用变量保存了封闭函数的返回值,那么这些局部函数的作用域就会被扩大。因此程序完全可以自由地调用它们,就像它们都是全局函数一样。后面会讲到函数返回函数的情况。

        局部函数内的局部变量也会遮蔽(或称为屏蔽)它所在函数内的局部变量(这句话有点拗口),请看如下代码。

# coding=utf-8
# 代码文件: 函数/nonlocal_test.py
# 局部函数:局部函数内的局部变量遮蔽它所在函数内的局部变量的情况

def foo():
    # foo函数的局部变量 name
    name = 'Charlie'
    def bar():
        # 访问局部函数bar所在foo函数内的局部变量 name
        print(name)  # --1
        name = 'Godlike'
    bar() # 调用局部函数bar

foo() # 调用foo函数

运行上面代码,会导致如下错误:

print(name)  # --1
UnboundLocalError: local variable 'name' referenced before assignment

分析:该错误是由于局部变量遮蔽局部变量导致的。在 bar() 函数中定义的局部变量 name 遮蔽了它所在 foo() 函数内的 name 局部变量,而在 print(name) 语句之前,bar() 函数中并没有声明 name 局部变量,因此导致程序中第 1 处的代码运行报错。

        为了声明 bar() 函数中的 name = 'Godlike' 赋值语句不是定义新的局部变量,只是访问它所在的 foo() 函数内的 局部变量 name,Python 提供了 nonlocal 关键字,通过 nonlocal 语句即可声明执行 name = 'Godlike' 赋值语句只是访问 bar() 局部函数所在函数(foo 函数)内的局部变量 name。

将上面的程序改为如下形式:

# coding=utf-8
# 代码文件: 函数/nonlocal_test.py
# 局部函数:局部函数内的局部变量遮蔽它所在函数内的局部变量的情况

def foo():
    # foo函数的局部变量 name
    name = 'Charlie'
    def bar():
        nonlocal name  # --1
        # 访问局部函数bar所在foo函数内的局部变量 name
        print(name)  # 输出 Charlie
        name = 'Godlike'
    bar() # 调用局部函数bar

foo() # 调用foo函数

分析:增加上面程序中第 1 处的代码之后,接下来 bar() 函数中的 name = 'Godlike' 就不再是定义新的局部变量,而是访问它所在函数(即 foo() 函数)内的局部变量 name。

提示nonlocal 和 前面介绍的 global 关键字功能大致相同,区别只是 global 用于声明访问全局变量,而 nonlocal 用于声明访问当前函数所在函数内的局部变量。

参考

《Python从小白到大牛(第1版-2018).pdf》第10章 - 函数式编程

《疯狂Python讲义(2018.12).pdf》第5章 - 函数和lambda表达式

《Python编程:从入门到实践(2016.7).pdf》第8章 - 函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值