第3章函数
学习目标
 熟练掌握自定义函数的设计和使用。
 深入理解各类参数,熟悉参数传递过程。
 掌握lambda表达式。
本章先介绍函数的定义与调用,接着介绍形式参数、实际参数、函数的返回、位置参数与关键参数、默认参数、个数可变的参数等相关概念和用法,再介绍参数与返回值类型的注解、lambda表达式和函数式编程中常用的类与内置函数。
3.1函数的定义
函数是为实现一个特定功能而组合在一起并赋予一个名字(函数名)的语句集,可以被别的程序或函数本身通过函数名来引用,也可以用来定义可重用代码,组织和简化代码。
函数定义的语法格式如下: 

def 函数名(形式参数):

函数体

函数通过def关键字定义,包括函数名称、形式参数、函数体。函数名是标识符,命名必须符合Python标识符的规定; 形式参数,简称为形参,写在一对圆括号里面。形参是可选的,即函数可以包含参数,也可以不包含参数,多个形参之间用逗号隔开。即使没有形参,这对圆括号也不能省略。该行以冒号结束。函数体是语句序列,左端必须缩进一些空格。
一些函数可能只完成要求的操作而无返回值,而另一些函数可能需要返回一个计算结果给调用者。如果函数有返回值,则使用关键字return来返回一个值。执行return语句的同时意味着函数的终止。
【例3.1】定义一个函数,其功能是求正整数的阶乘,并利用该函数求解6!、16!和26!的结果。
程序源代码如下: 

#example3_1.py

#coding=utf-8

def jc(n): #函数定义

s=1

for i in range(1,n+1):

s*=i

return s



#主程序

i=6

k=jc(i)

print(str(i)+"!=",k)

i=16

k=jc(i)

print(str(i)+"!=",k)

i=26

k=jc(i)

print(str(i)+"!=",k)

程序example3_1.py的运行结果: 

6!= 720

16!= 20922789888000

26!= 403291461126605635584000000



图3.1jc()函数的定义图解

这里定义了一个名为jc的函数,它有一个形式参数n,函数返回s的值,即n的阶乘值。图3.1解释了这个函数的定义。
函数必须先定义再调用。否则,在调用时会得到函数名没有定义的错误提示。该程序从上往下执行,遇到def定义的内容先跳过,从非def定义的地方开始执行。这里非def定义的部分通常被称为主程序。
3.2函数的调用
函数的定义是通过参数和函数体决定函数能做什么,并没有被执行。而函数一旦被定义,就可以在程序的任何地方被调用。当调用一个函数时,程序控制权就会转移到被调用的函数上,真正执行该函数; 执行完函数后,被调用的函数就会将程序控制权交还给调用者。
下面通过例3.1详细描述函数的调用过程。
在例3.1中,从主程序开始执行。执行主程序中的第一条语句,将6赋值给变量i,然后执行主程序中的第二条语句,调用函数jc(i)。当jc(i)函数被调用时,变量i的值被传递到形参n,程序控制权转移到jc(i)函数,然后就开始执行jc(i)函数。当jc(i)函数的return语句被执行后,jc(i)函数将计算结果返回给调用者,并将程序的控制权转移给调用者主程序。回到主程序后,jc(i)函数的返回值赋值给变量k。接下来执行主程序中的第三条语句,打印出结果。然后继续执行主程序的第四条语句,将16赋值给变量i ……(后续调用与前面一致,不再重复)。图3.2解释了jc(i)函数的调用过程。


图3.2jc(i)函数的调用过程


3.3形参与实参
在函数定义中,函数名后面圆括号中列出的参数称为形式参数,简称形参,如例3.1中jc(n)函数的n。如果形参的个数超过1个,各参数之间用逗号隔开。在定义函数时,函数的形参不代表任何具体的值,只有在函数调用时,才会有具体的值赋给形参。调用函数时传入的参数称为实际参数,简称实参,如例3.1中调用jc(n)函数时使用jc(i)传入的变量i。
3.4函数的返回
函数的执行结果通过返回语句return返回给调用者。函数体中不一定有表示返回的return语句。函数调用时的参数传递实现了从函数外部向函数内部输入数据,而函数的返回则解决了函数向外部输出信息的问题。如果一个函数的定义中没有return语句,系统将自动在函数体的末尾插入return None语句。
Python语言提供了一条return语句用于从函数返回值,格式如下: 

def 函数名(形式参数):

…

return <表达式1>,…,<表达式n>

当一个函数需要返回多个值时,在return语句之后跟上多个需要返回的表达式或变量,这些表达式的值和变量将共同构成一个元组返回给调用者,所以返回的始终是一个对象。
3.5位置参数与关键参数
当调用函数时,需要将实参传递给形参。参数传递时有两种方式: 以位置参数形式赋值和以关键参数形式赋值。以位置参数形式赋值是指按照函数定义中形参的排列顺序来传递,以关键参数形式赋值是指按照参数赋值的形式来传递。
当使用位置参数时,实参和形参在顺序、个数和类型上必须一一匹配。例3.1中,调用带参数的函数时使用位置参数。
在函数调用中,也可以通过“变量名=值”的“键值”形式将实参传递给形参,使得参数可以不按顺序来传递,让函数参数的传递更加清晰、易用。采用这种方式传递的参数称为关键参数(也称关键字参数)。
【例3.2】 编写一个函数,有三个形参,其中两个传递字符分别作为开始字符和结束字符,打印出两个字符之间的所有字符,每行打印的字符个数由第三个形参指定。
程序源代码如下: 

#example3_2.py

#coding=utf-8

def printChars(ch1, ch2, number):

count = 0

for i in range(ord(ch1), ord(ch2) + 1):

count+=1

if count % number!=0:

print("%4s"%chr(i),end=' ')

else:

print("%4s"%chr(i))



#主程序

printChars("!","9",10)#以位置参数形式传递

print()

printChars(number=10,ch2="9",ch1="!")#以关键参数形式传递

print()

printChars("!",number=10,ch2="9") #位置参数和关键参数混合使用

程序example3_2.py的运行结果如下: 

!"#$%&'()*

+,-./01234

56789 

!"#$%&'()*

+,-./01234

56789 

!"#$%&'()*

+,-./01234

56789

在printChars()函数中,ch1、ch2表示两个字符,number表示每行打印字符的个数。在主程序中,使用printChars("!","9",10)表示输出字符!到字符9之间的字符,每行打印10个字符。在该语句中,按照参数位置顺序将字符"!"传递给ch1,将字符"9"传递给ch2,将10传递给number。函数调用printChars(number=10,ch2="9",ch1="!")中,采用关键参数形式,将实参10传递给形参number,将实参字符“9”传递给形参ch2,将实参字符串“!”传递给形参ch1。函数调用printChars("!",number=10,ch2="9")中,第一个参数采用位置参数形式进行传递,后两个参数采用关键参数形式进行传递。
3.6默认参数
函数定义时,形参可以设置默认值,这种形参通常称为默认参数。如果在调用函数时不为这些参数提供值,这些参数就使用默认值; 如果在调用时有实参,则将实参的值传递给形参,形参定义的默认值将被忽略。具有默认参数值的函数定义格式如下: 

def 函数名(非默认参数, 形参名=默认值, …):

函数体

函数定义时,形式参数中非默认参数与默认参数可以并存,但非默认参数之前不能有默认参数。
【例3.3】默认参数应用实例。分析函数调用及程序的运行结果。
程序源代码如下: 

#example3_3.py

#coding=utf-8

#函数定义

def sayHello(s="Hello!",n=2,m=1): 

for i in range(n):

print(s*m) 



#主程序

#形参没有赋予新值,均取默认值

sayHello()

print()



#按照顺序依次赋值给形参

sayHello("Ha!",3,4)

print()



#按照顺序,形参中s赋予新值"Ha!"

#n没有赋新值,取默认值

#通过关键参数形式,为形参m赋予新值3

sayHello("Ha!",m=3)

程序example3_3.py的运行结果如下: 

>>> 

 RESTART: d:\test\example3_3.py 

Hello!

Hello!



Ha!Ha!Ha!Ha!

Ha!Ha!Ha!Ha!

Ha!Ha!Ha!Ha!



Ha!Ha!Ha!

Ha!Ha!Ha!

>>>

在该函数的定义中有三个参数——s、n和m,s的默认值是字符串“Hello!”,n的默认值是2,m的默认值是1。
在主程序中,第1个sayHello()调用语句没有提供实参值,所以程序就将默认值“Hello!”赋给s,将默认值2赋给n,将默认值1赋给m,运行结果就是打印出两行字符串“Hello!”。
调用sayHello("Ha!",3,4)时,这三个参数均是按位置赋值的,字符串“Ha!”赋给s,3赋给n,4赋给m,运行结果就是打印出三行字符串“Ha! Ha! Ha! Ha!”,行数由n决定,字符串“Ha!”的重复次数由m决定。
调用sayHello("Ha!",m=3)时,以位置参数形式将字符串“Ha!”赋给形参s; 没有提供实参值赋给n,则将默认值赋给n; 以关键参数形式将整数3赋值给形参m。打印出两行字符串“Ha!Ha!Ha!”。采用关键参数形式来传递实参可以跳过一些默认参数的赋值。这里采用关键参数形式为形参m重新赋予新值,直接跳过了默认参数n的赋值,此时n取默认值。
3.7个数可变的参数
当需要接收不定个数参数时,形参以元组或字典等组合对象形式收集不定个数的实参。实参也可以以序列、字典等组合对象形式,为形参中的多个参数分配值。实参和形参也可以均为组合对象,从而可以实现不定个数参数的传递。


扫码观看


3.7.1以组合对象为形参接收多个实参
在前面的函数介绍中,我们知道一个形参只能接收一个实参的值。其实在Python中,函数可以接收不定个数的参数,即用户可以给函数提供可变个数的实参。这可以通过在形参前面添加标识符(一个星号*或两个星号**)来实现。
1. 将多个以位置参数形式传递的实参收集为形参中的元组
在函数定义的形参前面加一个星号*,则该参数将接收不定个数的、以位置参数传递的实参,构成一个元组。
【例3.4】编写一个函数,接收任意个数的参数并打印出来。
程序源代码如下: 

#example3_4.py

#coding=utf-8

#函数定义

def all_1(*args): 

print(args)



#主程序

all_1()

all_1("a")

all_1("a",2)

all_1("a",2,"b")

#all_1(x="a",y=2)#这里不能以关键参数的形参传递

程序example 3_4.py的运行结果: 

()

('a',)

('a', 2)

('a', 2, 'b')

在函数all_1()的定义中,形参args前面有一个星号标识符*,表明形参args可以接收不定个数的、以位置参数形式传递的实参。主程序中调用all_1(),没有传递实参,形参args得到一个空的元组; 主程序中调用all_1("a"),传递一个参数给args,结果以元组的形式输出('a',); 主程序中调用all_1("a",2),传递两个参数给args,结果也是以元组的形式输出('a',2); 主程序中调用all_1("a",2,"b"),传递三个参数给args,结果还是以元组的形式输出('a',2,'b')。从这个示例中可以看出,不管传递几个参数到args,都是将接收的所有参数按照次序组合到一个元组上。
example3_4.py主程序的最后一行被注释掉了,否则将出现以下错误信息: 

Traceback (most recent call last):

File "D:\example3_4.py", line 12, in <module>

all_1(x="a",y=2)

TypeError: all_1() got an unexpected keyword argument 'x'

因为加一个星号*的形参不接收以关键参数形式传递的实参。不定个数的以关键参数形式传递的实参可以被以两个星号**为前缀的形参接收,并收集为字典形式。
以一个星号*为前缀的形参可以和其他普通形参联合使用,这时一般将以星号*为标识符的形参放在形参列表的后面,普通形参放在前面。
2. 将多个以关键参数形式传递的实参收集为形参中的字典
前面已经提到,以一个星号*为前缀的形式参数不接收以关键参数形式传递的实参值。在Python的函数形参中还提供了一种在参数名前面加两个星号**的方式。这时,函数调用者须以关键参数的形式为其赋值,以两个星号**为前缀的形参得到一个以关键参数中变量名为key,右边表达式值为value的字典。
【例3.5】以“**”为前缀的可变长度参数使用案例。
程序源代码如下: 

#example3_5.py

#coding=utf-8

#函数定义

def all_4(**args): 

print(args)



#主程序

all_4(x="a",y="b",z=2)

all_4(m=3,n=4)

all_4()

程序example3_5.py的运行结果如下: 

{'x': 'a', 'y': 'b', 'z': 2}

{'m': 3, 'n': 4}

{}

在函数all_4()的定义中,参数args前面有两个星号**,表明该形参args可以将不定个数的、以关键参数形式给出的实参收集起来转换为一个字典。在主程序中第一次调用该函数时,以关键参数形式将三个参数传递给args,输出的结果是一个字典; 第二次调用该函数时,以关键参数形式将两个参数传递给args,输出的结果还是一个字典; 第三次调用时,没有传递实参,形参得到一个空字典。
以两个星号**为前缀的不定个数参数、以一个星号*为前缀的不定个数参数和普通参数在函数定义中可以混合使用。这时,普通参数放在最前面,其次是以一个星号*为前缀的不定个数参数,最后是以两个星号**为前缀的不定个数参数。


扫码观看


3.7.2以组合对象为实参给多个形参分配参数
函数定义时的形参为单变量时,实参可以是以一个星号*为前缀的序列变量,将此序列中的元素分配给相应的形参; 实参也可以是以两个星号**为前缀的字典变量,根据字典中的key和形参变量名的对应关系,将字典中的value传递给相应的形参。
【例3.6】本例为以单变量为形参、以序列和字典为实参传递参数的案例,试分析程序输出结果并解释原因。
程序源代码如下: 

#example3_6.py

#coding=utf-8

#函数定义,形参为单变量参数

def snn3(x,y,z): 

return x+y+z 



#主程序

aa=[1,2,3]#列表

print(snn3(*aa)) #实参为列表变量(以"*"为前缀)

bb=(6,2,3)#元组

print(snn3(*bb)) #实参为元组变量(以"*"为前缀)

ss="abc"

print(snn3(*ss)) #实参为字符串变量(以"*"为前缀)

cc=[8,9]

print(snn3(7,*cc)) #实参为单变量+序列(以*为前缀)

print(snn3(*cc,20))

d1={"x":1,"y":2,"z":3}

print(snn3(**d1))#实参为字典变量(以**为前缀)

d2={"y":2,"z":3}

print(snn3(1,**d2))

d3={"x":1,"y":2}

#以**为前缀的实参之后的普通参数以关键参数的形式传递

print(snn3(**d3,z=3))

程序example3_6.py的运行结果如下: 

6

11

abc

24

37

6

6

6

在如上示例中,函数snn3()中的形参是三个单变量,返回值为这三个变量的和,而在主程序中调用时,aa是一个列表,也就是说用序列作实参时,要在序列前加*,而且序列的元素个数与snn3()中的形参个数相同,aa中的元素正好也是3个,这样调用时就写成snn3(*aa),输出结果6就是aa列表中三个元素的和。如果主程序中写成snn3(aa),则程序会出现这样的错误: snn3() takes exactly 3 arguments (1 given),因为这时调用时将列表aa作为一个整体传递给形参x,而形参y和z没有值,因此出错。而用*aa,则能把实参的元素分解给各个形参,把列表aa中的元素1分解给x、2分解给y、3分解给z,snn3()接收了三个参数。 
bb是一个元组,ss是一个字符串。这两个变量与aa一样,也是序列作实参,调用时要在序列前加*。
cc也是一个列表,但是只有两个元素,主程序中通过snn3(7,*cc)或snn3(*cc,20)均可调用。按照位置分别进行参数分配。
d1、d2和d3均为字典。作为实参为单变量形参传递并分配值时,实参变量名前面加两个星号**。如果这类实参变量名后面还有普通参数需要传递,则须采用关键参数形式。


扫码观看


3.7.3形参和实参均为组合类型
当形参和实参均为序列时,可以通过在形参和实参前均添加一个星号*来实现参数传递。当形参和实参均为字典时,可以通过在形参和实参前均添加两个星号**来实现参数传递。
【例3.7】形参和实参均为序列或形参和实参均为字典,分别添加*和**作为形参和实参的前缀来实现参数传递。
程序源代码如下: 

#example3_7.py

#coding=utf-8

#序列作为参数的函数定义

def snn1(*args): 

print(args)

for i in args:

print(i,end=' ')

print()



#字典作为参数的函数定义

def snn2(**args): 

print(args)

s=0

for i in args.keys():

s+=args[i]

return s



#主程序

print("snn1:")

aa=[1,2,3] #列表(序列)

snn1(*aa)

snn1(*[4,5])#列表(序列)

bb=(6,2,3,1)#元组(序列)

snn1(*bb)

snn1(*'abc')#字符串(序列)

print()

print("snn2:")

cc={'x': 1, 'y': 2, 'c': 3}#字典

print(snn2(**cc))

print(snn2(**{'aa': 1, 'bb': 2 ,'cc': 4, 'dd': 5, 'ee': 6})) #字典

程序example3_7.py的运行结果如下: 

snn1:

(1, 2, 3)

1 2 3 

(4, 5)

4 5 

(6, 2, 3, 1)

6 2 3 1 

('a', 'b', 'c')

a b c 



snn2:

{'x': 1, 'y': 2, 'c': 3}

6

{'aa': 1, 'bb': 2, 'cc': 4, 'dd': 5, 'ee': 6}

18

实际上,当实参与形参类型相同时,参数可以直接传递,实参和形参均不需要添加任何星号为前缀。例3.7的程序可以改为以下实现形式: 

#example3_7_another.py

#coding=utf-8

#序列作为参数的函数定义

def snn1(args): 

print(args)

for i in args:

print(i,end=' ')

print()



#字典作为参数的函数定义

def snn2(args): 

print(args)

s=0

for i in args.keys():

s+=args[i]

return s



#主程序

print("snn1:")

aa=[1,2,3]#列表(序列)

snn1(aa)

snn1([4,5])#列表(序列)

bb=(6,2,3,1)#元组(序列)

snn1(bb)

snn1('abc')#字符串(序列)

print()

print("snn2:")

cc={'x': 1, 'y': 2, 'c': 3}#字典

print(snn2(cc))

print(snn2({'aa': 1, 'bb': 2 ,'cc': 4, 'dd': 5, 'ee': 6})) #字典

程序example3_7_another.py运行结果: 

snn1:

[1, 2, 3]

1 2 3 

[4, 5]

4 5 

(6, 2, 3, 1)

6 2 3 1 

abc

a b c 



snn2:

{'x': 1, 'y': 2, 'c': 3}

6

{'aa': 1, 'bb': 2, 'cc': 4, 'dd': 5, 'ee': 6}

18

3.8参数与返回值类型注解
Python是一种动态语言,在声明一个参数时不需要指定类型,并且函数的返回值也没有类型的定义。在编写程序时,参数可以接收任意值,运行时可能就会因为类型不匹配而出现错误。函数没有指明返回值类型,有时只有运行后才能确定类型,这可能导致调用程序运行时出错。为了使函数调用者明确函数的参数类型和返回值类型,引入了类型注解的概念,可以为函数参数和函数返回值标注类型。这种类型的注解不是强制执行的,在定义函数时也可以没有。本章前面自定义的函数中没有使用类型注解。类型注解的引入便于一些集成开发环境(Integrated Development Environment,IDE)可以在程序运行前进行校验,发现一些隐藏的类型不匹配错误。即使使用了类型注解,目前大部分编辑器仍然不做类型检查。
在参数名后面添加一个英文冒号,并在冒号后面写出类型名称,即可实现参数类型注解。如果该参数有默认值,则默认值的赋值号放在参数类型名称后面。如果要为函数注解返回值类型,则在参数列表的右侧圆括号后面添加“>类型”。
以下代码定义函数f(x),用类型注解指明参数x的类型为int,用函数返回值类型注解指明函数返回值类型为int。另外指明参数x的默认值为5。

def f(x : int=5) -> int :

return x+1

在一些IDE中,要求实参必须是形参指定的类型,并且返回值必须与指定的类型一致。但目前大部分IDE并不做检查,因此实参不是整数也可以调用f(x),如f(1.1)。
3.9lambda表达式
lambda表达式又称lambda函数,是一个匿名函数,比def格式的函数定义简单很多。lambda表达式可以接收任意多个参数,但只返回一个表达式的值。lambda中不能包含多个表达式。
lambda表达式的定义格式为: 

lambda 形式参数 :  表达式

其中形式参数可以有多个,它们之间用逗号隔开。表达式只有一个。返回表达式的计算结果。
以下例子中赋值号左边的变量相当于给lambda函数定义了一个函数名。可以将此变量名作为函数名来调用该lambda表达式。

>>> f=lambda x,y : x+y

>>> f(5,10)

15

>>>

3.10函数式编程的常用类与函数
map、reduce()和filter是函数式编程中常用的类与函数。这里先将利用初始化参数创建类的对象看作函数的调用,但两者有本质的区别。请读者在学完第4章后再来区分两者的区别。
1.  filter类
创建对象的格式: filter(function or None,iterable)
功能: 把一个带有一个参数的函数function作用到一个可迭代(iterable)对象上,返回一个filter对象,filter对象中的元素由可迭代(iterable)对象中使得函数function返回值为True的那些元素组成; 如果指定函数为None,则返回可迭代(iterable)对象中等价于True的元素。filter对象是一个迭代器(iterator)对象。

>>> aa=[5,6,-9,-56,-309,206]

>>> def func(x):#定义函数,x为奇数时返回True,为偶数时返回False

return x%2!=0



>>> bb=filter(func,aa)

>>> type(bb)#bb是一个filter对象

<class 'filter'>

>>> list(bb)

[5, -9, -309]

>>> dd=[6,True,1,0,False]

>>> ee=filter(None,dd)#指定函数为None

>>> list(ee)

[6, True, 1]

2.  reduce()函数
函数调用格式: reduce(function,iterable[,initializer])

其中,参数function函数是有两个参数的函数,iterable是需要迭代计算的可迭代对象,initializer是计算的初始化参数,是可选参数。
reduce()函数对一个可迭代对象中的所有数据进行如下操作: 用作为reduce参数的两参数函数function先对初始值initializer和可迭代对象iterable中的第1个元素进行function函数定义的相关计算; 然后用得到的结果再与可迭代对象iterable中的第2个元素进行相关计算; 直到遍历计算完可迭代对象iterable中的所有元素,最后得到一个结果。如果参数中没有初始值initializer,则直接从可迭代对象iterable中的第1个和第2个元素开始计算,然后依次将结果和下一个对象进行计算,直到遍历计算完可迭代对象iterable中的所有元素,最后得到一个结果。 
从python 3开始,reduce()函数移到了functools模块,使用之前需要先导入。例如: 

>>> def add(x,y):

 return x+y



>>> from functools import reduce

>>> reduce(add,(1,2,3,4))#使用两参数函数add()

10

>>> reduce(lambda x,y:x+y,(1,2,3,4))#使用两参数的lambda表达式

10

>>> reduce(lambda x,y:x+y,(1,2,3,4),5)#初始值为5

15

>>>

3.  map类
创建对象的格式: map(func,*iterables)
功能: 把一个函数func依次作用到可迭代(iterable)对象的每个元素上,返回一个map对象。参数*iterables前的*表示iterables接收不定个数的可迭代对象,其个数由函数func中的参数个数决定。map对象是一个迭代器(iterator)对象。例如: 

>>> aa=['1','5.6','7.8','9']

>>> bb1=map(float,aa)

>>> bb1

<map object at 0x0000000002F76400>

>>> list(bb1)

[1.0, 5.6, 7.8, 9.0]

>>> list(map(str,range(5)))

['0', '1', '2', '3', '4']

习题3
1. 编写一个可判断一个数是否为素数的函数,然后调用该函数来判断从键盘输入的数是否为素数。素数也称质数,是指只能被1和它本身整除的自然数。
2. 编写一个可求出一个数除了1和自身以外的因子的函数。从键盘输入一个数,调用该函数输出除了1和它自身以外的所有因子。
3. 编写一个可判断一个数是否为水仙花数的函数。然后调用该函数打印出1000以内的所有水仙花数。水仙花数是指一个n位自然数(n≥3),它的每个位上的数字的n次幂之和等于它本身。例如: 13 + 53+ 33 = 153,则153是水仙花数。