第5 章 函数 本章介绍函数。函数是带名字的代码块,用于完成特定的任务。要执行函数定义的特 定任务,可调用该函数。使用函数,当需要在程序中多次执行同一任务时,就无须重复编写 完成该任务的代码。通过使用函数,程序的编写、阅读、测试和修改都更容易。 使用PyCharm 创建一个新的项目ch05,用来验证本章示例。 5.1 定义函数 Python的函数有以下3类。 ● 内建函数:Python语言自带了许多内建函数,如之前用过的print()、type()等都是 Python的内建函数,可以直接用。 ● 自定义函数:编程人员可以自己创建函数,叫作自定义函数。设计并实现自定义函 数是程序设计的主要工作。 ● API函数:别人创建的、封装好的函数。使用者并不知道它是怎样实现的,但可以直 接使用,使用方法与前两类函数无本质区别。 本节主要介绍自定义函数。 1.最简单的函数 下面示例用最简单的函数形式,说明函数定义与调用的过程。创建一个Python文件 test_def1.py,代码为 def say_hello(): #① """②显示问候语""" print("Hello!") #③ say_hello() #④ 69 执行结果: Hello! test_def1.py中说明了最简单的函数结构。第①行使用def保留字告诉Python要定义 一个函数,该行称为函数定义。def保留字之后必须跟有函数名和包括形式参数的圆括号。 本例的函数名是say_hello。括号中可以向函数传递参数,也就是告诉函数完成任务需要什 么样的信息;本例不需要参数,括号内为空。该行要以冒号结束。 函数体语句从函数定义的下一行开始,必须是缩进的。第②行的文本是被称为文档字 符串(docstring)的注释,描述了函数是做什么的。文档字符串用三引号括起来,有些工具 通过文档字符串自动生成在线的或可打印的文档;即使不生成文档,文档字符串也会让程 序员之间的交流变得更加顺畅,在代码中包含文档字符串是一个好的习惯。 第③行是本例函数体唯一的一行执行代码,它的功能是调用print()函数显示问候语。 要想使用这个函数,可以调用它,如本例的第④行。函数调用让Python执行函数中的 代码。函数调用需要指明函数名,以及用括号括起来的实际参数。本例不需要参数,因此 括号内为空。 2.向函数传递参数 对test_def1.py进行修改,使其在显示问候语时加上用户名,代码为 def say_hello(username): """显示问候语""" print("Hello, "+username+"!") say_hello("Zhang") 执行结果: Hello, Zhang! 在函数定义的括号内增加了参数username,通过该参数,函数可以接受调用时给出的 任何值。本例在调用时指定了参数值为"Zhang",函数的显示结果就是“Hello,Zhang!”。 如果函数调用改为say_hello("Xu"),函数的显示结果就是“Hello,Xu!”。 3.实参和形参 前面笼统地使用术语“参数”,更深入的讨论需要将参数分为形参和实参。 在函数定义中,括号内要求的参数(如username)被称为形式参数,简称形参,函数体中 根据这些形参进行操作。当调用函数时,需要指定参数的实际值(如say_hello("Zhang")中 的"Zhang"),称为实际参数,简称实参。 5.2 返回值 5.1节示例函数say_hello()的功能只是单纯输出。在很多情况下,函数在经过处理之 后,需要将处理结果返回给调用者,这时候就要用到返回值。 70 5.2.1 return语句 在函数体中,由return语句指定函数的返回值。创建一个Python文件test_return1. py,代码为 def max(x, y): a=x if x<y: a=y return a m=max(6, 9) print(m) 执行结果: 9 函数max接收两个形式参数,使用if语句,将两个参数中值比较大的一个赋值给变量 a,然后用return语句返回a的值。 语句m=max(6,9),先将实际参数6和9传递给函数max,再将函数返回的结果赋值 给变量m。 需要说明的是,即使函数体中没有return语句,函数运行结束后也会隐含地返回一个 None作为返回值,None的说明见2.2.3节。 5.2.2 多分支return Python函数使用return语句返回“返回值”,可以将返回值赋给其他变量作其他的用 途。所有函数都有返回值,即使返回的是None。 一个函数可以有多个return语句,遇到任何一个return语句,函数立即返回,函数中的 其他语句都不再执行,也就是说最多只有一个return语句会被执行。如果执行到函数结束 也没有遇到return语句,就隐式地执行returnNone。如果有必要,也可以显式地执行 returnNone,明确返回一个None作为返回值,returnNone可以简写为return。 对test_return1.py进行修改,将相关代码修改为 def max(x, y): if x<y: return y else: return x 执行结果完全相同。 5.2.3 返回值类型 可以用type()函数查看函数返回值的类型。 71 对test_return1.py进行修改,将调用部分代码改为 m=max(6, 9) print(m) print(type(max(6, 9))) 执行结果: 9< class 'int'> Python的return语句只能返回单值,如果需要返回多个值,需要使用复合数据类型,如 列表、元组等。 创建一个Python文件test_return2.py,代码为 def show_list(): return [1, 2, 3] print(show_list()) print(type(show_list())) 执行结果: [1, 2, 3] <class 'list'> 可以看到返回的是列表类型。 还可以返回其他的复合类型,如元组。对test_return2.py的return语句进行修改,将 代码改为 return (1, 2, 3) 执行结果: (1, 2, 3) <class 'tuple'> 继续对test_return2.py的return语句进行修改,将代码改为 return 1, 2, 3 执行结果仍然为 (1, 2, 3) <class 'tuple'> return1,2,3看似返回多个值,其实隐式地被Python封装成了一个元组返回,本例验 证了Python函数只能返回单值。但利用元组的封装和拆封功能,可以使Python函数达到 返回多个值的效果。例如: def show_list(): return 1, 2, 3 72 a, b, c=show_list() print(a) print(b) print(c) 在本例中,return语句先将多个值封装成元组,“a,b,c=show_list()”一句再将函数 返回的元组拆封成三个变量。 5.3 参数的传递方式 在函数定义中可以包含若干参数,称为形式参数(形参)。调用时将实际的参数值传 入,称为实际参数(实参)。本节讨论实参是如何传递给形参的,也就是参数的传递方式。 要理解Python的参数传递方式,最好与其他语言(如C语言)进行对照,并且借用C语言的 术语。 C语言采用的是值传递方式,即形参和实参分配不同的内存地址,在调用时将实参的值 复制到形参,在这种情况下,在函数内部修改形参的值不会影响到实参。C++ 增加了“引 用”这个概念,即在函数定义时,在形参前加一个& 符号,表示传递参数的引用,实参与形参 指向共同的内存地址,在函数内修改形参值会影响到实参,这种参数传递的方式被称为引 用传递。那么,Python是值传递还是引用传递呢? 接下来看两个例子。 示例一:创建一个Python文件test_tran1.py,代码为 def swap(x, y): """试图交换两个变量的值""" temp=x x=y y=temp print(f"函数内部: x={x},y={y}") a, b=5, 9 print(f"交换之前: a={a},b={b}") swap(a, b) print(f"交换之后: a={a},b={b}") 执行结果: 交换之前: a=5,b=9 函数内部: x=9,y=5 交换之后: a=5,b=9 可以看到,在swap函数内部对形参x和y进行了交换,但在函数的外部,a和b的值仍 然是函数调用之前的值,没有改变。按照这个示例,看起来Python采用的是值传递方式。 示例二:创建一个Python文件test_tran2.py,代码为 def proc_list(m): 73 """对列表进行修改""" m.append(10) print(f"函数内部: {m}") n=list(range(5)) print(f"调用之前: {n}") proc_list(n) print(f"调用之后: {n}") 执行结果: 调用之前: [0, 1, 2, 3, 4] 函数内部: [0, 1, 2, 3, 4, 10] 调用之后: [0, 1, 2, 3, 4, 10] 函数proc_list接收一个列表,并在列表的尾部追加一个值。在函数调用之后,外部传 入的实参值发生了变化,也就是说,函数内外操作的是同一个列表。按照这个示例,看起来 Python采用的是引用传递方式。 Python的参数传递方式与内存使用机制有关,不能确定是值传递还是引用传递,随着 版本的变化还可能改变。回顾4.1.1节关于is操作符及id()函数的使用,可以帮助读者理 解本节内容。如果在程序中适合的位置使用id()函数,可以看到,在示例一中,a和x的id 不同,而在示例二中,n和m 的id相同。通常情况下,简单对象(如int)采用值传递,复杂对 象(如列表)采用引用传递。 5.4 参数类型 在使用Python的内置函数(如print()函数)时,相信读者已经领略到Python参数的灵 活性。与其他编程语言相比,Python的一大优势就是其参数类型及传递形式丰富而灵活。 Python中参数的定义形式有多种,不但包括位置参数、默认值参数、关键字参数等一般形 式,还可以使用元组参数、字典参数的封装与拆封进一步增强参数传递的灵活性。这些形 式可以单独使用也可以混合使用。 5.4.1 位置参数 位置参数是参数定义的最基本形式,本章前面的函数定义采用的都是位置参数。 创建一个Python文件test_para1.py,代码为 def f(a, b, c): print(a, b, c) f(1, 2, 3) 执行结果: 1 2 3 74 位置参数必须以函数定义中的顺序来传递,如函数调用f(1,2,3)中的1、2和3分别 对应函数定义f(a,b,c)中的a、b和c。 5.4.2 默认值参数 可以为一个或多个参数指定默认值,这样,在调用时就可以传入比定义时更少的实际 参数。创建一个Python文件test_para2.py,代码为 def f(a, b=10, c=20): print(a, b, c) f(1, 2, 3) f(1, 2) f(1) f() #调用时会报错 执行结果: Traceback (most recent call last): File "E:/PyCharmProjects/ch04/test_para2.py", line 7, in <module> f() TypeError: f() missing 1 required positional argument: 'a' 1 2 3 1 2 20 1 10 20 参数a是位置参数,调用时必须传入。参数b和c给出了默认值,调用时这些参数如果 不指定实参,将采用默认值。这个函数可以通过以下几种不同的方式调用。 (1)位置参数调用时必须传入: f()#调用时会报错 (2)只给出必要的参数: f(1) (3)给出部分可选的参数: f(1, 2) (4)给出所有的参数: f(1, 2, 3) 需要注意的是,对于引用传递的参数(实参值在函数内部可能改变),即使函数被多次 调用,默认值也只会被赋值一次,参数值的改变可能在多次调用中累积。请看下面的示例, 创建一个Python文件test_para3.py,代码为 def f(a, li=[]): li.append(a) 75 return li print(f(1)) print(f(2)) print(f(3)) 执行结果: [1] [1, 2] [1, 2, 3] 如果不想让默认值在后续调用中累积,可以将函数改为如下形式: def f(a, li=None): if li is None: li=[] li.append(a) return li 这个示例的实质是将参数的引用传递改成值传递,这样,参数值的改变就不会在多次 调用中累积。 5.4.3 关键字参数 函数调用时可以通过关键字参数的形式来传递参数,形如keyword=value。下面示例 说明关键字参数的用法,创建一个Python文件test_para4.py,代码为 def f(a, b=10, c=20): print(a, b, c) #最一般的调用方式 f(1, 2, 3) #其他正确的调用方式 f(a=1, b=2, c=3) f(1, c=2) f(a=1) f(b=2, c=3, a=1) #其他错误的调用方式 f(a=1, 2) #关键字参数之后不能再使用非关键字参数 f(1, a=2) #重复指定了参数a 的值 f(b=2) #参数a 的值未指定 f(1, d=2) #没有名称为d 的参数 函数定义与前例相同,接收一个必选参数(a)和两个可选参数(b和c)。前面是正确的 调用形式,执行结果: 1 2 3 76 1 2 3 1 10 2 1 10 20 1 2 3 正确的调用方式包括如下两种。 (1)位置参数也可以用关键字参数的形式来指定值,f(a=1)是正确的。 (2)指定关键字参数的顺序可以任意,f(b=2,c=3,a=1)是正确的。 错误的调用方式包括如下3种。 (1)在函数调用中,关键字的参数必须跟随在位置参数的后面,所以f(a=1,2)是 错的。 (2)任何参数都不可以多次赋值,所以f(1,a=2)是错的。 (3)传递的所有关键字参数必须与函数接受的某个参数相匹配,所以f(1,d=2)是 错的。关 键字参数是Python程序员非常喜欢的一种参数传递方式,希望读者能够熟练掌握。 5.4.4 元组参数的封装与拆封 3.2节介绍了元组封装与拆封的概念,5.2节介绍了函数返回值如何使用元组的封装与 拆封,本节介绍元组的封装与拆封如何用于参数传递。 在定义函数时,有时候并不知道调用的时候会传入多少个参数。这时候,采用形如 “*name”的形式,可传递可变个数的参数,这些参数被封装进一个元组。 创建一个Python文件test_para5.py,代码为 def func(*name): print(type(name)) print(name) func(1, 2, 3) func(1, 2, 3, 4, 5, 6) 执行结果: <class 'tuple'> (1, 2, 3) <class 'tuple'> (1, 2, 3, 4, 5, 6) 可见,形参可被当成元组来操作,从而可以知道实参的数量及各参数的值。 这样传递的参数元组必须在位置参数和默认参数之后。将文件test_para5.py修改为 def func(a, b=2, *name): print(type(name)) print(name) func(1, 2, 3) 77 func(1, 2, 3, 4, 5, 6) func(1) 执行结果: <class 'tuple'> (3,) <class 'tuple'> (3, 4, 5, 6) <class 'tuple'> () 可以看出,在前两个调用中,前两个实参被传递给了形参a和b,剩余的参数被封装传 递给形参name。第三个调用只有一个实参,该实参传递给形参a,形参b使用默认参数,而 name只能接收到一个空元组。 注意:第一个调用显示的元组为(3,),这是单元素元组的显示形式,在单元素的后面要 加一个逗号。 在函数定义中,任何出现在*name后面的参数都被当成关键字参数。将文件test_ para5.py修改为 def func(a, b=2, *name, last_para): print(type(name)) print(name) print(last_para) func(1, 2, 3, last_para='OK!') func(1, 2, 3, 4, 5, 6, last_para='Yes!') 执行结果: <class 'tuple'> (3,) OK! <class 'tuple'> (3, 4, 5, 6) Yes! 上面示例说明了元组参数的封装,相反的过程就是元组参数的拆封。 创建一个Python文件test_para6.py,代码为 def func(a, b, c): print(a, b, c) args=(1, 2, 3) func(*args) 执行结果: