函数实质性特定任务的一段代码,程序通过将一段代码定义成函数,并为该函数指定一个函数名,这样即可在需要的时候多次调用这段代码。因此,函数是代码复用的重要手段。
与函数紧密相关的一个知识点就是lambda表达式。lambda表达式可作为表达式、函数参数或函数返回值,因此使用lambda表达式使得程序更加简洁。
5.1 函数入门
函数就是Python程序的重要组成单位,一个Python程序可以由多个函数组成。
5.1.1 理解函数
我们之前也用过大量的函数,如len()、max()等,使用函数是真正开始编程的第一步。
比如在程序中定义了一段代码,这段代码用于实现一个特定的功能。为了使一段代码可以多次使用,我们将实现特定功能的代码定义成一个函数,每次当程序需要实现该功能时,只要执行(调用)该函数。
通俗来讲,所谓函数,就是指为一段实现特定功能的代码“取“一个名字,以后即可通过改名字执行(调用)该函数。通常,函数可以接收零个或多个参数,也可以返回零个或多个值。从函数使用者的角度来看。函数就像一个“黑匣子”,程序将零个或多个参数传入这个“黑匣子”,该“黑匣子”经过一番计算即可返回零个或多个值。
对于“黑匣子”的内部细节(就是函数内部实现细节),函数的使用者并不需要关心。就像前面在调用len()、max()、min()等函数时,我们只负责传入参数、接收返回值,置于函数内部的实现细节,我们并不关心。
图片
从函数定义者(实现函数的人)的角度来看,其至少需要想清楚以下三点:
- 函数需要几个关键的需要动态变化的数据,这些数据应该被定义成函数的参数。
- 函数需要传出几个重要数据(就是调用函数的人希望得到的数据),这些数据应该被定义成返回值。
- 函数的内部实现过程。
从介绍不难看出,定义函数比调用函数难得多,而学习如何实现函数的定义就是需要学习的内容。
5.1.2 定义函数和调用函数
在使用之前必须先定义函数,定义函数的语法格式如下:
def 函数名(形参列表):##由零条到多条可执行语句组成的函数[return [返回值]]
Python声明函数必须使用def关键字,对函数语法格式的详细说明如下:
- 函数名:从语法角度来看,函数名只要是一个合法的标识符即可;从程序的可读性角度来看,函数名应该由一个或多个有意义的单词连缀而成,每个单词的字母全部小写,单词与单词之间使用下划线分隔。
- 形参列表:用于定义该函数可以接收的参数。形参列表由多个形参名组成,多个形参名之间以英文逗号(,)隔开。一旦在定义函数时指定了形参列表,调用该函数时就必须传入对应的参数值——谁调用函数,谁负责为形参赋值。
在函数体中多条可执行语句之间有严格的执行顺序,排在函数体前面的语句总是先执行,排在函数体后面的语句总是后执行。
#定义一个函数,声明两个形参
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("孙悟空"))
"""
result: 9
===正在执行say_hi()函数===
孙悟空,您好!
"""
在函数体中使用return语句可以显式地返回一个值,return语句返回的值既可以是有值的变量,也可以是一个表达式。
5.1.3 为函数提供文档
前面介绍过可以使用Python内置的help()函数来查看其他函数的帮助文档,我们也经常提供help()函数查看指定函数的帮助信息,这对于Python开发者来说非常重要。
我们还可以为函数编写说明文档——只要把一段字符串放在函数声明之后、函数体之前,这段字符串将被作为函数部分,这个文档就是函数的说明文档。
程序即可提供help()函数查看的说明文档,也可以通过函数的__doc__属性访问韩硕的说明文档。
def my_max(x,y):'''获取两个数值之间的较大数的值my_max(x,y)返回x,y两个参数之间较大的那个数'''#定义一个变量z,该变量等于x、y之间的较大值z=x if x>y else y#返回变量z的值return z
#使用help()函数查看my_max的帮助文档
help(my_max)
print(my_max.__doc__)
"""
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__属性访问该函数的说明文档。
5.1.4 多个返回值
如果程序需要有多个返回值,既可以将多个值包装成列表之后返回,也可以直接返回多个值。如果Python函数直接返回多个值,Python会自动将多个返回值封装成元组。
def sum_and_avg(list):sum=0count=0for e in list:#如果元素e是值if isinstance(e,int) or isinstance(e,float):count+=1sum+=ereturn sum,sum/count
my_list=[20,15,2.8,'a',35,5.9,-1.8]
#获取sum_and_avg函数返回的多个值,多个返回值被封装成元组
tp=sum_and_avg(my_list)
print(tp)#(76.9, 12.816666666666668)
此外,也可以使用Python提供的序列解包功能,直接使用多个变量接收函数返回的多个值。
s,avg=sum_and_avg(my_list)
print(s)#76.9
print(avg)#12.816666666666668
5.1.5 递归函数
在一个函数体内调用它自身,被称为函数递归。函数递归包含了一种隐式的循环,他会重复执行某段代码,但这种重复执行无需循环控制。
例如:已知有一个数列:f(0)=1,f(1)=4,f(n+2)=2*f(n+1)+f(n),其中n是大于0的整数,求f(10)的值。这道题可以使用递归来求得。
def fn(n):if n==0:return 1elif n==1:return 4else:#在函数体内调用它自身,就是函数递归return 2*fn(n-1)+fn(n-2)
#输出fn(10)的结果
print(fn(10))#10497
在上面的fn()函数体中再次调用了fn()函数,这就是函数递归。注意fn()函数体中调用fn()的形式:
return 2*fn(n-1)+fn(n-2)
仔细看上面递归的过程,当一个函数不断调用它自身时,必须在某个时刻函数的返回值是确定的,即不再调用它自身;否则,这种递归就变成了无穷递归,类似于死循环。因此,在定义递归函数时有一条最重要的规定:规定一定要像已知的方向进行。
递归是非常有用的,例如程序希望遍历某个路径下的所有文件,但这个路径下的文件路径是未知的,那么就可以使用递归来实现这个需求。系统可以定义一个函数,该函数接收一个文件路径作为参数,该函数可遍历出当前路径下的所有文件和文件路径——在该函数体中再次调用函数自身来处理该路径下的文件路径。
总之,只要在一个函数体中调用函数自身,就是函数递归。递归一定要向已知方向进行。
5.2 函数的参数
在定义Python函数时可定义形参(形式参数),这些形参的值要等调用时才能确定下来,由函数的调用者负责为形参传入参数值。简单来说,就是谁调用函数,谁负责传入参数值。
5.2.1 关键词(keyword)参数
Python函数的参数名不是没有意义的,Python允许在调用函数时通过名字来传入参数值。因此,Python函数的参数名应该具有更好的语义——程序可以立刻明确传入函数的每个参数的含义。
按照形参的位置传入的参数被称为位置参数。如果使用位置参数的方式来传入参数值,则必须严格按照定义函数时指定的顺序来传入参数值;如果根据参数名来传入参数值,则无需遵守定义形参的顺序,这种方式被称为关键字参数。
#定义一个函数
def girth(width,height):print("width:",width)print("heigth:",height)return 2*(width+height)
#传统调用函数的方式,根据位置传入参数值
print(girth(3.5,4.8))
#根据关键字参数来传入参数值
print(girth(width=3.5,height=4.8))
#在使用关键字参数时可交换位置
print(girth(height=4.8,width=3.5))
#部分使用关键字参数,部分使用位置参数
print(girth(3.5,height=4.8))
需要说明的是,如果希望在调用函数时混合使用关键字的参数和位置参数,则关键字参数必须位于位置参数之后。换句话来说,在关键字参数之后的只能是关键字参数。
#必须将位置参数放在关键字参数之前,下面代码错误
print(girth(width=3.5,4.8))
"""File "", line 15print(girth(width=3.5,4.8))^
SyntaxError: positional argument follows keyword argument
"""
5.2.2 参数默认值
在某些情况下,程序需要在定义函数时,为一个或多个元素指定默认值——这样在调用函数时就可省略为该形参传入参数,而是直接使用该形参的默认值。
为形参指定默认值的语法格式如下:
形参名=默认值
从上面的语法来看,形参的默认值紧跟在形参之后,中间以英文“=”隔开。
#为两个参数指定默认值
def say_hi(name="孙悟空",message="欢迎来到极乐净土"):print("name:",name)print("消息是:",message)
#全部使用参数默认值
say_hi()
#只有一个参数使用默认值
say_hi("白骨精")
#两个参数都不使用默认值
say_hi("白骨精","欢迎来到我的世界")
#只有name参数使用默认参数
say_hi(message="欢迎学习Python")
"""
name: 白骨精
消息是: 欢迎来到极乐净土
name: 白骨精
消息是: 欢迎来到我的世界
name: 孙悟空
消息是: 欢迎学习Python
"""
从上面程序可以看出,如果只传入一个位置参数,由于参数位于第一位,系统将该参数值传给name参数。因此,不能按如下方式调用函数:
say_hi("欢迎学习Python")
上面调用时传入的是给name,而不是message。
say_hi(name="白骨精","欢迎学习Python")
因为Python规定:关键字参数必须位于位置参数的后买你。因此提示错误:Positional argument after keyword argument。
那么,能不能单纯地将上面两个参数交换位置呢?
say_hi("欢迎学习Python",name="白骨精")
上面调用依然是错误的,因为第一个字符串没有指定关键字参数,因此将使用位置参数为name参数传入参数值,第二个参数使用关键字参数的从形式再次为name参数传入参数值,这意味着两个参数数值其实都会传入name参数,程序name参数传入了多个参数值。因此提示错误:say_hi() got multiple values for argument 'name'。
将函数调用改为如下两种形式是正确的:
say_hi("白骨精",message="欢迎学习Python")
say_hi(name="白骨精",message="欢迎学习Python")
由于Python要求在调用函数时关键字参数必须位于位置参数的后面,因此在定义函数时指定默认值的参数(关键字参数)必须在没有默认值的参数之后。
#定义一个打印三角形的函数,默认值的参数必须放在后面
def printTriangle(char,height=5):for i in range(1,height+1):#先打印一排空格for j in range(height-i):print(' ',end='')#在打印一排特殊字符for j in range(2*i-1):print(char,end='')print()
printTriangle('@',6)
printTriangle('#',height=7)
printTriangle(char='*')
"""@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@####################################
#############****************
*********
"""
注意:Python要求将带默认值的参数定义在形参列表的最后。
5.2.3 参数列表(个数可变的参数)
很多编程语言都允许定义个数可变的参数,这样可以在调用函数时传入任意多个参数。python当然也不例外,Python允许在形参前面添加以一个星号(*),这样意味着该形参可以接受多个参数值,多个参数被当成元组传入。
#定义了支持参数收集的函数
def text(a,*book):print(book)#books被当成元组处理for b in book:print(b)#输出整数变量a的值print(a)
#调用test()函数
text(5,"疯狂","fkit","Python")
"""
('疯狂', 'fkit', 'Python')
疯狂
fkit
Python
5
"""
从上面的运行结果可以看出,当调用text()函数时,book参数可以传入多个字符串作为参数值。从text()函数体代码来看,参数收集的本质就是一个元组,Python会将传给book参数的多个值收集成一个元组。
Python允许个数可变的形参可以处于形参列表的任意位置(不要求时形参列表的最后一个参数),但Python要求一个函数最多只能带一个支持“普通”参数收集的形参。
#定义了支持参数收集的函数
def text(*book,num):print(book)#book被当成元组处理for b in book:print(b)print(num)
#调用text()函数
text("Python",'fkit','java','C',num=20)
"""
('Python', 'fkit', 'java', 'C')
Python
fkit
java
C
20
"""
Python还可以收集关键字参数,此时Python会将这种关键字参数收集成字典。为了让Python能收集关键字参数,需要在参数前面添加两个星号。在这种情况下,一个函数可同时包含一个支持“普通”参数收集的参数和一个支持关键字收集的参数。
#定义了支持参数收集的函数
def text(x,y,z=3,*books,**scores):print(x,y,z)print(books)print(scores)
text(1,2,3,'Python','java',语文=89,数学=94)
"""
1 2 3
('Python', 'java')
{'语文': 89, '数学': 94}
"""
最后的两个关键字参数将会被收集成字典。
对于上面方式定义的text()函数,参数z的默认值几乎不能发挥作用。
text(1,2,'Python','java',语文=89,数学=94)
"""
1 2 Python
('java',)
{'语文': 89, '数学': 94}
"""
如果希望让z参数的默认值发挥作用,则需要只传入两个位置参数。
text(1,2,语文=89,数学=94)
"""
1 2 3
()
{'语文': 89, '数学': 94}
"""
5.2.4 逆向参数收集
所谓逆向参数收集,指的是在程序已有列表、元组、字典等对象的前提下,把它们的元素“拆开”后传给函数的参数。
逆向参数收集需要在传入的列表、元组参数之前添加一个星号,在字典参数之前添加两个星号。
def text(name,message):print("用户是:",name)print("欢迎消息:",message)
my_list=["孙悟空","欢迎来到西游之旅"]
text(*my_list)
"""
用户是: 孙悟空
欢迎消息: 欢迎来到西游之旅
"""
实际上,即使是支持收集的参数,如果程序需要将一个元组传给该函数,那么同样需要使用逆向收集。
def text(name,*nums):print("用户是:",name)print("nums参数:",nums)
my_list=[1,2,3]
#使用逆向收集,将my_list的元素传给nums参数
text('fkit',*my_list)
"""
用户是: fkit
nums参数: (1, 2, 3)
"""
此外,也可调用如下方式调用text()函数:
#使用逆向收集,将my_list列表的第一个元素传给name参数,剩下元素传给nums参数
text(*my_list)
"""
用户是: 1
nums参数: (2, 3)
"""
如果不使用逆向收集(不在列表参数之前添加*),整个列表将作为一个参数,而不是将元组的元素作为多个参数。
text(*my_list)
#不使用逆向收集,my_list列表整体传给name参数
text(my_list)
"""
用户是: [1, 2, 3]
nums参数: ()
"""
字典也支持逆向收集,字典将会以关键字参数的形式传入。
def bar(book,price,desc):print(book,"这本书的价格是:",price)print("描述信息:",desc)
my_dict={'price':89,'book':'Python','desc':'这是本系统全面的Python学习图书'}
#按逆向收集的方式将my_dict的多个key-value对传给bar()函数
bar(**my_dict)
"""
Python 这本书的价格是: 89
描述信息 这是本系统全面的Python学习图书
"""
5.2.5 函数的参数传递机制
Python的参数值是如何传入函数的呢?这是由Python函数的传递机制来控制的。Python中函数的参数传递机制都是“值传递”。所谓值传递,就是将实际参数数值的副本(复制品)传入函数,而函数本身不会受到任何影响。
def swap(a,b):#下面代码实现a、b变量的值变换a,b=b,aprint("在swap函数里,a的值是",\a,";b的值是",b)
a=6
b=9
swap(a,b)
print("在swap函数里,a的值是", \a, ";b的值是", b)
"""
在swap函数里,a的值是 9 ;b的值是 6
在swap函数里,a的值是 6 ;b的值是 9
"""
上面程序开始定义了a、b两个局部变量,这两个变量在内存中的存储示意图如下:
当程序执行swap()函数时,系统进入swap()函数,并将主程序中的a、b中的a、b变量作为参数值传入swap()函数,但传入swap()函数的只是a、b的副本,而不是a、b本身。进入swap()函数后,系统中产生了4个变量,这4个变量在内存中的存储示意图如下:
当主程序中调用swap()函数时,系统分别为主程序和swap()函数分配两块栈区,用于保存它们的局部变量。将主程序中的a、b变量作为参数值传入swap()函数,实际上在swap()函数栈区中的a、b变量的值分别赋值给swap()函数栈区中的a、b参数(就是对swap()函数的a、b两个变量进行初始化)。此时,系统存在两个a变量、两个b变量,只是存在于不同的栈区中而已。
程序在swap()函数中交换a、b两个变量的值,实际上是上图中的灰色区域的a、b变量进行交换。交换结束后,输出swap()函数中的a、b变量的值,可以看到a是9,b是6。
主程序栈区中的a、b的值并未发生改变,程序改变的只是swap()函数栈区中a、b的值 ,这就是值传递的实质:当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋给函数中形参的变量,在函数中操作的并不是实际的实参变量。
根据Python的参数传递机制,我们知道:传入函数的只是参数的副本,因此程序在函数中对参数赋值并不影响参数本身。如果参数本身是一个可变的对象(比如列表、字典等),此时虽然Python采用的也是值传递方式,但有很多人可能会对这种类型的参数传递产生一些误会。
def swap(dw):#下面代码实现dw的a、b两个元素的值交换dw['a'],dw['b']=dw['b'],dw['a']print("在swap()函数里,a元素的值是",\dw['a'],";b元素的值是",dw['b'])
dw={'a':6,'b':9}
swap(dw)
print("在swap()函数里,a元素的值是", \dw['a'], ";b元素的值是", dw['b'])
"""
在swap()函数里,a元素的值是 9 ;b元素的值是 6
在swap()函数里,a元素的值是 9 ;b元素的值是 6
"""
从上面的运行结果来看,在swap()函数中,dw字典的a、b两个元素的值被成功交换。不仅如此,当swap()函数执行结束后,主程序中dw字典的a、b两个元素的值也被交换了。这很容易造成一种错觉:在调用swap()函数时,传入swap()函数的就是dw字典本身,而不是它的复制品。但只是一种错觉。
程序开始创建了一个字典对象,并定义了一个dw引用变量指向字典都西昂,这意味着此时内存中有两个东西:对象本身和指向该对象的引用变量。此时在系统内存中的存储示意图如图:
接下来主程序开始调用swap()函数,在调用swap()函数时,dw变量作为参数传入swap()函数的dw形参,从而完成swap()函数的dw参数初始化。值得指出的时,主程序中的dw是一个引用变量(也就是一个指针),它保存了字典对象的地址值,当把dw的值赋给swap()函数的dw参数后,就是让swap()函数的dw参数也保存这个地址值,即也会引用到同一个字典对象。
从上图看出,这种参数传递的方式是不折不扣的值传递方式,系统一样赋值了dw的副本传入swap()函数。但由于dw只是一个引用变量,因此系统复制的是dw,并未复制该字典本身。、
当程序在swap()函数中操作dw参数时,由于dw只是一个引用变量,故实际操作的还是字典对象。此时,不管是操作主程序中的dw变量,还是操作swap()函数里的dw参数,其实操作的都是它们共同引用的字典对象,它们引用的是同一个字典对象。因此,当在swap()函数中交换dw参数所引用字典对象的a、b两个元素的值后,可以看到在主程序中dw变量所引用字典对象的a、b两个元素的值也被交换了。
为了更好地证明主程序中的dw和swap()函数中的dw是两个变量,在swap()函数的最后一行增加如下代码:
#把dw直接赋值为None,让它不再指向任何对象
dw=None
把swap()函数中的dw赋值为None后,再swap()函数中失去了对字典对象的引用,不可再访问该字典对象。但主程序中的dw变量不受任何影响,依然可以引用该字典对象所以依然可以输出字典对象的a、b元素的值。
通过上面介绍可以得出两个结论:
- 不管什么类型的参数,再Python函数中对参数直接使用“=”符号赋值是没用的,直接使用“-”符号赋值并不能改变参数。
- 如果需要让函数修改某些数据,则可以通过把这些数据包装成列表、字典等可变对象,然后把列表、字典等可变对象作为参数传入函数,在函数中通过列表、字典的方法修改它们,这样才可以改变这些数据。
5.2.6 变量作用域
再程序中定义一个变量时,这个变量是有作用范围的,变量的作用范围称为它的作用域。根据定义变量的位置,变量分为两种:
- 局部变量:在函数中定义的变量,包括参数,都被称为局部变量。
- 全局变量。在函数外面、全局范围内定义的变量,被称为全局变量。
每个函数在执行时,系统都会为该函数分配一块“临时内存空间”,所有局部变量都被保存在这块临时内存空间内。当函数执行完成后,这块内存空间就被释放了,这些局部变量也就失效了,因此离开函数之后就不能再访问局部变量了。
全局变量意味着它们可以再所有函数内被访问。
不管是在函数的局部范围内还是再全局范围内,都可能存在多个变量,每个变量“持有”该变量的值。从这个角度来看,不管是局部范围还是全局范围,这些变量和它们的值就像一个“看不见”的字典,其中变量名就是字典的key,变量值就是字典的value。
实际上,Python提供了如下三个工具函数来获取指定范围内的“变量字典”。
- globals():该函数返回全局范围内所有变量组成的“变量字典”。
- locals():该函数当前范围内所有变量组成的“变量字典”。
- vars(object):获取在指定对象范围内所有变量组成的“变量字典”。如果不传入object参数,vars()和locals()的作用完全相同。
globals()和locals()看似完全不同,当它们实际上也是有联系的,关于这两个函数的区别和联系大致有以下两点:
- locals()总是获取当前局部范围内所有变量组成的“变量字典”,因此,如果在全局范围内(在函数之外)调用locals()函数,同样会获取全局范围内所有变量组成的“变量字典”;而globals()函数无论在哪执行,总会获取全局范围内所有变量组成的“变量字典”。
- 一般来说,使用locals()和globals()获取的“变量字典”只应该被访问,不应该被修改。但实际上,不管是使用locals()还是globals()函数获取的全局范围内的“变量字典”,都可以被修改,而这种修改会真正改变全局变量本身;但通过locals()获取的局部范围内的“变量字典”,即使对它修改也不会影响全局变量。
def test():age=20#直接访问age局部变量print(age)#输出20#访问函数局部范围内的“变量数组”print(locals())#{'age': 20}#通过l局部变量范围内的“变量数组”访问age变量print(locals()['age'])#20#通过locals函数局部范围内的“变量数组”改变age变量的值locals()['age']=12#再次访问age变量的值print("xxx",age)#12#通过globals函数修改x全局变量globals()['x']=19
x=5
y=20
print(globals())#'x': 5, 'y': 20
#在全局范围内使用locals函数,访问的是全局变量的“变量数组”
print(locals())#'x': 5, 'y': 20
#直接访问x全局变量
print(x)#5
#通过全局变量的“变量数组”访问x全局变量
print(globals()['x'])#5
#通过全局变量的“变量数组”对x全局变量赋值
globals()['x']=39
print(x)#39
#在全局范围内使用locals函数对x全局变量赋值
locals()['x']=99
print(x)#99
提示:在使用globals()或locals()访问全局变量的“变量字典”时,将会看到程序输出的“变量字典”默认包含了很多变量,这些都是Python主程序内置的。
全局变量默认可以在所有函数内被访问,但如果在函数中定义了全局变量同名的变量,此时就会发生局部变量遮蔽全局变量的情形。
name='Charlie'
def test():#直接访问name全局变量print(name)#Charlie
test()
print(name)#Charlie
如果在上面程序代码改成:
name='Charlie'
def test():#直接访问name全局变量print(name)#Charliename="孙悟空"
test()
print(name)#Charlie
"""
UnboundLocalError: cannot access local variable 'name' where it is not associated with a value
"""
该错误提示所访问的name变量还未定义。这是什么原因:正是由于程序在test()函数中增加了“name=’孙悟空’”一行代码造成的。
Python语法规定:在函数内部对不存在的变量赋值时,默认就是重新定义新的局部变量。因此这一行代码相当于重新定义了name局部变量,这样name全局变量就被遮蔽了,所以上面程序中会报错。
为了避免这个问题,可以通过两种方式来修改上面程序。
1.访问被遮蔽的全局变量
如果程序希望上面的代码依然能访问name全局变量,且在后面重新定义局部变量_也就是在函数中可访问被遮蔽的全局变量,此时可通过globals()函数来实现.
name='Charlie'
def test():#直接访问name全局变量print(globals()['name'])#Charliename="孙悟空"
test()
print(name)#Charlie
2.在函数中声明全局变量
为了避免在函数中对全局变量赋值(不是重新定义局部变量),可使用global语句来声明全局变量.因此,可将程序改为:
name='Charlie'
def test():#声明name是全局变量,后面的赋值语句不会重新定义局部变量global nameprint(globals()['name'])#Charliename="孙悟空"
test()
print(name)#孙悟空
增加了global name”声明之后,程序会把name变量当作全局变量,这意味着test()函数后面对name赋值的语句只是对全局变量赋值,而不是重新定义全局变量。
5.3 局部变量
前面所看到的函数都是在全局范围内定义的,它们都是全局函数。Python还支持在函数体内定义函数,这种被放在函数体内定义的函数称为局部函数。
在默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭函数内有效,其封闭函数也可以返回局部函数,以便程序在其他作用域中使用局部函数。
#定义函数,该函数会包含局部函数
def get_math_func(type,nn):#定义一个计算平方的局部函数def square(n):return n*n#定义一个计算立方的局部函数def cube(n):return n*n*n#定义一个计算阶乘的局部函数def factorial(n):result=1for index in range(1,n+1):result*=indexreturn result#调用局部函数if type=="square":return square(nn)elif type=="cube":return cube(nn)else:return factorial(nn)
print(get_math_func("square",3))
print(get_math_func("cube",3))
print(get_math_func("factorial",3))
"""
9
27
6
"""
如果封闭函数没有局部函数,那么局部函数只能在封闭函数内部调用,如上面程序所示。
另外,还会出现一种情况,如果封闭函数将局部函数返回,且程序使用变量保存了封闭函数的返回值,那么这些局部函数的作用域就会被扩大。因此程序完全可以自由地调用它们,就像它们都是全局一样。
局部函数内的变量也会遮蔽它所在函数内的局部变量。
def foo():#局部变量namename='Charlie'def bar():#访问bar函数所在foo函数内的name局部变量print(name)#UnboundLocalError: cannot access local variable 'name' where it is not associated with a valuename='孙悟空'bar()
foo()
该错误是由局部变量遮蔽局部变量导致的,在bar()函数中定义的name局部变量遮蔽了它所在foo()函数内的name局部变量,因此导致程序中报错。
为了声明bar()函数中的“name=’孙悟空’”赋值语句不是定义新的局部变量,只是访问他所在foo()函数内的name局部变量,Python提供nonlocal关键字,提供nonlocal语句即可声明访问赋值语句只是访问该函数所在函数内的局部变量。将上面程序改写成:
def foo():#局部变量namename='Charlie'def bar():nonlocal name#访问bar函数所在foo函数内的name局部变量print(name)#UnboundLocalError: cannot access local variable 'name' where it is not associated with a valuename='孙悟空'bar()
foo()
增加nonlocal name代码之后,接下来bar()函数中的“name=’孙悟空’”就不再是定义新的局部变量,而是访问它所在函数(foo()函数)内的name局部变量。
提示:nonlocal和前面介绍的global功能大致相似,区别只是global用于声明访问全局变量,而nonlocal用于声明访问当前函数所在函数内的局部变量。
5.4 函数的高级内容
Python的函数是“一等公民”,因此函数本身也是一个对象,函数即可用于赋值,也可用作其他函数的参数,还可作为其他函数的返回值。
5.4.1 使用函数变量
Python的函数也是一种值:所以函数都是function对象,这意味着可以把函数本身赋值给变量,就像把整数、浮点数、列表、元组赋值给变量一样。
当把函数赋值给变量之后,接下来程序也可通过该变量来调用。
#定义一个计算乘方的函数
def pow(base,exponent):result =1for i in range(1,exponent+1):result*=basereturn result
#将pow()函数赋值给my_fun,则my_fun可被当成pow()调用
my_fun=pow
print(my_fun(3,4))#81
#定义一个计算面积的函数
def area(width,height):return width*height
#将area函数赋值给my_fun,则my_fun可被当成area使用
my_fun=area
print(my_fun(3,4))#12
提示:其实Python已经内置了很多计算乘方的方法,因此此处pow()函数并没有太大的实际意义,只是作为示范使用。
通过对my_fun变量赋值不同的函数,可以让my_fun再不同的时间指向不同的函数,从而让程序更加灵活。由此可见,使用函数变量的好处是让程序更加灵活。
除此之外,程序还可使用函数作为另一个函数的形参和(或)返回值。
5.4.2 使用函数作为函数形参
有时候需要定义一个函数,该函数的大部分计算逻辑都能确定,但某些处理逻辑暂时无法确定——这意味着某些程序代码需要动态改变,如果希望调用函数时能动态传入这些代码,那么就需要再函数中定义函数形参,这样即可在调用该函数时传入不同的函数作为参数,从而动态改变这段代码。
Python支持像使用其他参数一样使用函数参数。
#定义函数类型的形参,其中fn是一个函数
def map(data,fn):result=[]#遍历data列表中的每一个元素,并用fn函数对每个元素进行计算#然后将计算结果作为新数组的元素for e in data:result.append(fn(e))return result
#定义一个计算平方的函数
def square(n):return n*n
#定义一个计算立方的函数
def cube(n):return n*n*n
#定义一个计算阶乘的函数
def factorial(n):result=1for index in range(2,n+1):result*=indexreturn result
data=[3,4,9,5,8]
print("原数据:",data)
#下面程序代码调用map()函数三次,每次调用传入不同的函数
print("计算数组元素的平方")
print(map(data,square))
print("计算数组元素的立方")
print(map(data,cube))
print("计算数组元素的阶乘")
print(map(data,factorial))
print(type(map))
"""
原数据: [3, 4, 9, 5, 8]
计算数组元素的平方
[9, 16, 81, 25, 64]
计算数组元素的立方
[27, 64, 729, 125, 512]
计算数组元素的阶乘
[6, 24, 362880, 120, 40320]
<class 'function'>
"""
从上面介绍不难看出,通过使用函数作为参数可以调用函数时动态传入函数——实际上就可以动态改变被调用函数的部分代码。
5.4.3 使用函数作为返回值
前面提到,Python还支持使用函数作为其他函数的返回值。
def get_math_func(type):# 定义一个计算平方的函数def square(n):return n * n# 定义一个计算立方的函数def cube(n):return n * n * n# 定义一个计算阶乘的函数def factorial(n):result = 1for index in range(2, n + 1):result *= indexreturn result#返回局部函数if type=='square':return squareif type=='cube':return cubeelse:return factorial
#调用get_math_func(),程序返回一个嵌套函数
math_func=get_math_func('cube')
print(math_func(5))#125
math_func=get_math_func('square')
print(math_func(5))#25
math_func=get_math_func('factorial')
print(math_func(5))#120
5.5 局部函数于lambda表达式
lambda表达式是现代编程语言争相引入的一种语法。如果说函数是命名的、方便复用的代码块,那么lambda表达式则是功能更灵活的代码块,它可以在程序中被传递和调用。
5.5.1回顾局部函数
前面学习局部函数是有这样一串代码:
def get_math_func(type,nn):#定义一个计算平方的局部函数def square(n):return n*n#定义一个计算立方的局部函数def cube(n):return n*n*n#定义一个计算阶乘的局部函数def factorial(n):result=1for index in range(1,n+1):result*=indexreturn result#调用局部函数if type=="square":return square(nn)elif type=="cube":return cube(nn)else:return factorial(nn)
由于局部函数的作用域默认仅停留在其封闭函数内,因此这三个局部函数的函数名的作用域太有限了——仅仅是在程序的if语句中作为返回值使用。一旦离开了get_math_func(),这三个局部函数的函数名就失去了意义。
既然局部函数的函数名没有太大的意义,俺么就考虑使用lambda表达式来简化局部函数的写法。
5.5.2 使用lambda表达式代替局部函数
如果使用lambda表达式来简化上面的程序,可以改写为如下形式:
def get_math_func(type):result=1#该函数返回的是lambda表达式if type =='square':return lambda n:n*nelif type =='cube':return lambda n:n*n*nelse:return lambda n:(1+n)*n/2
#调用get_math_func(),程序返回一个嵌套函数
math_func=get_math_func("cube")
print(math_func(5))#125
math_func=get_math_func("square")
print(math_func(5))#25
math_func=get_math_func("other")
print(math_func(5))#15.0
lambda表达式语法格式如下:
lambda [parameter_list]:表达式
从上面的语法格式可以看出lambda表达式的几个要点。
- lambda表达式必须使用lambda关键字定义。
- 在lambda关键字之后、冒号左边的是参数列表,可以没有参数,也可以有多个参数。如果有多个参数,则需要使用逗号隔开,冒号右边是该lambda表达式的返回值。
实际上,lambda表达式的本质就是匿名的、单行函数体的函数。因此lambda表达式可以写成函数的形式。
lambda x,y:x+y
可改写为如下函数形式:
def add(x,y):return x+y
上面定义函数时使用了简化语法:当函数只有一行代码时,可以直接把函数体的代码放在与函数头同一行。
总体来说,函数比lambda表达式适应性更强,lambda表达式只能创建简单的函数对象(它只适合函数体为单行的情形),但lambda表达式依然有如下两个用途:
- 对于单行函数,使用lambda表达式可以省去定义函数的过程,让代码更简洁。
- 对于不需要多次复用的函数,使用lambda表达式可以在用完之后立即释放,提高了性能。
#传入计算平方的lambda表达式作为参数
x=map(lambda x:x*x,range(8))
print([e for e in x])#[0, 1, 4, 9, 16, 25, 36, 49]
#传入计算平方的lambda表达式作为参数
y=map(lambda x:x*x if x%2==0 else x,range(8))
print([e for e in y])#[0, 1, 4, 3, 16, 5, 36, 7]