本章讲的是函数操作。在了解函数操作之前,先来介绍一种编程思想的方法,面向过程思想。
面向过程思想是一种以过程为中心的编程思想。是最为实际的一种思考方式。面向过程思想的大概意思是先分析出解决问题所需要的步骤,接着编写函数来实现每一步的功能,最后在对每个函数依次调用。
在面向过程开发中,最重要的是模块化的思想。函数在模块化思想中起到了至关重要的作用。它可以将程序裁分成一个个程序片段,来独立实现某个完整的功能,进而实现模块化。
函数(function)是组织好的、可重复使用的、具有一定功能的代码段。它能提高应用的模块性和代码的重复利用率。在Python中,用户可以自定义函数。同时Python还提供了很多内建函数,比如print、input等方便用户调用。
Python中有固定的格式来定义函数,函数也有具体的组成部分(函数名、函数体、参数、返回值)。从分类上又可以分为内置函数、自定义函数等。为了实现不同的编程需求,它们在使用中会有各种各样的规则,同时还要配合这作用域的限制,来完成整个的功能流程。具体介绍如下:
定义函数使用关键字def,后接函数名和放在圆括号( )中的可选参数列表,函数内容以冒号起始并且缩进。一般格式如下
def函数名(参数1, 参数2,……, 参数N):
例:
defhello(strname): #定义一个函数hello
print (strname) #函数的内容为一句代码,实现将指定内容输出
单独的函数是运行不起来的。需要对其调用才可以执行。调用的时候,直接使用函数名称即可。例:
hello(“Ilove Python!”) #调用函数,这时候屏幕会输出:I love Python!
Python中的函数有固定的组成部分。这些是构成一个普通函数的基本要素。在后面还会讲解不同类型的函数,其变化也都是在基本组成部分上变化而来的。
函数一共可以分为四个组成部分:
l 函数名:def后面的名字,例如6.1.1例中的hello;
l 函数参数:函数名后面的变量,例如6.1.1例中的strname;
l 函数体:函数名的下一行代码,起始需要缩进,例如6.1.1例中的print (strname);
l 返回值:函数执行完的返回内容,用return开始。当没有返回值时可以不写。例如6.1.1例中没有返回值。
对于一般类型的函数来讲,函数名和函数体是必须有的,函数的参数和返回值是可选的。
在函数体中,一般第一行会放置一个多行字符串,用来说明函数的功能及调用方法。这个字符串就是函数的文档字符串,或称为docstring 。我们可以使用print(function.__doc__)输出文档。例如:
defgetrecoder(): #定义一个函数getrecoder
'''该函数是返回用户名字和年龄'''
name = 'Gary'
age = 32
return name,age #返回name和age的值
print(getrecoder.__doc__) #返回文档字符串。输出:该函数是返回用户名字和年龄
例子中,在函数体的第一句就放置了一个文档字符串。该字符串用来对函数进行描述。实际应用中,文档字符串主要用于描述一些关于函数的信息,让用户交互地浏览和输出。建议养成在代码中添加文档字符串的好习惯。
6.1.2中谈到的参数都是属于形参。形参是从函数的角度来说的。如果从调用的角度来说,在调用时传入的参数就是实参。
举例6.1.1例子中的调用代码hello(“I love Python!”) ,其中 “I love Python!”就是传入hello函数中的实参。它代表实际的参数值。在函数执行时,在函数hello的函数体中,实现的功能是将参数strname打印出来。参数strname的值就是“I love Python!”,于是屏幕就会输出“I love Python!”。
前面介绍过,函数不需要返回值时,可以什么都不做。在需要有返回值时,就要使用return语句将具体的值返回回去。使用return语句可以一次返回多个值,调用函数的代码使用等号来接收。可以使用与返回值对应的变量个数来接收、也可以使用一个元组来接收。例如:
defgetrecoder(): #定义一个函数getrecoder
name = 'Gary'
age = 32
return name,age #返回name和age的值
myname,myage= getrecoder() #在调用的时候,使用与返回值对应的两个值来接收
print(myname,myage) #将返回值打印出来,输出:Gary 32
person= getrecoder() #在调用的时候,使用与返回值对应的一个值来接收
print(person) #将返回值打印出来,输出:('Gary',32)
在某种情况下,有可能是需要用到返回值的一个,其他的想忽略掉。可以使用下划线(_)来接收对应返回值。例如:
personname,_= getrecoder() #在调用的时候,使用_来接收不需要的返回值
print(personname)
在6.1.2中的“2.文档字符串”中讲到过函数的文档字符串部分。它是存放在函数的__doc__属性中。这个属性是可更改的,可以修改函数的__doc__属性值来为函数指定新的文档字符串。例如:
defgetrecoder(): #定义一个函数getrecoder
'''该函数是返回用户名字和年龄'''
name = 'Gary'
age = 32
return name,age #返回name和age的值
print(getrecoder.__doc__) #返回文档字符串。输出:该函数是返回用户名字和年龄
getrecoder.__doc__= "新文档字符串" #修改该函数__doc__属性值
print(getrecoder.__doc__) #打印文档字符串。输出:新文档字符串
类似这样的功能,在定义函数的同时,还可以为函数添加自定义属性。每个函数都有个默认的__dict__字典,为函数添加的属性就会放在这个字典里面。例如:
defrecoder(strname,*,age): #定义一个函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
print(recoder.__dict__) #函数recoder没有任何属性,__dict__为空。输出:{}
recoder.attr="person" #为函数recoder添加属性attr等于person
print(recoder.__dict__) #再次观察__dict__的值。输出:{'attr': 'person'}
print(recoder.attr) #可以直接将attr的值取出来。输出:person
直接在函数名后面加个点就可以加上任意需要的属性;获取该属性时也同样是在该函数名后面加个点,再跟上函数的具体属性名。这种方式可以在使用函数时传递更多的信息。
函数本质也是一个对象,支持被调用的函数都会继承了可调用的方法call,可以使用函数callable来检查某函数是否可以被调用。例如:
defrecoder(strname,*,age): #定义一个函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
print(callable(recoder) ) #测试函数recoder是否可以被调用。输出:True
例子中使用了callable函数判断recoder是否可以被调用。结果返回True,表明函数recoder是可以被调用的。
从用户角度看,函数可以分为内置函数,与自定义函数。所谓内置函数就是Python内部自带的函数。自定义函数为用户自己写的函数。
从功能上又可以分为生成器函数、一般函数。
从形式的角度来看,Python中的函数主要有五种形式:
l 普通的函数,使用def来定义;
l 匿名函数,使用lambda关键字;
l 可迭代函数,属于一种特殊的内置函数;
l 递归函数,自己调用自己的函数;
l 偏函数,使用partial关键字。
本篇默认讲的都是普通函数,在下文会对匿名函数、可迭代函数、递归函数和偏函数单独介绍。
函数只有在调用的过程中才会运行,与函数调用息息相关的就是参数,明白了形参和实参之后,就可以系统的了解函数参数的定义形式以及具体的调用规则了。
函数参数的定义有四种方式,下面来一一介绍:
这是最常见的定义方式,一个函数可以定义任意个参数,每个参数间用逗号分割。例如:
defrecoder(strname,age): #定义一个函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
单独的函数是运行不起来的。需要对其调用才可以执行。调用的时候,直接使用函数名称,并且还要提供个数相等的实参。默认的情况下实参顺序需要与形参一一对应。例:
recoder("Gary", "32") #调用函数,这时候屏幕会输出:姓名:Gary 年纪:32
上面代码中recoder函数有两个参数strname,age。调用函数recoder的时候,必须也要传入两个参数。在没有任何指定的情况下,这这两个实参还需要与形参的顺序一一对应。
当然也可以指定具体某个形参来为其赋值。在指定的具体形参的情况下,传入的参数就可以与形参的顺序无关了。例如上面的调用语句还可以写成这样:
recoder(age=32,strname="Gary ") # 通过指定形参的方式调用函数,输出:姓名:Gary 年纪:32
这次传入实参顺序不一样了,并且age的参数的类型也不是字符串了,而是一个整型。因为Python中的变量在定义时是不需要指定类型的。只有在调用时,系统才会根据传入的实参(32)定义函数(recoder)中的形参(age)的类型(int)。所以在该例子中,可以为一个函数的形参传入两个不同类型的实参。
这种方式的通用性也是有限制的。如果在函数体里做了某个特殊类型的操作,那就必须传入这个类型的实参。例如,改写上面的recoder函数如下:
defrecoder(strname,age): #定义一个函数recoder
return age+1 #将age+1返回
在函数体里将形参age进行加一操作,并且使用return关键之返回该值。这很显然是将age当成了数值类型来处理。这时如果传入字符串就会报错,因为字符串不能与一个整型常量(1)相加。这种错误在函数调用时,会带来许多不必要的麻烦。为了解决这个问题,可以提前对参数类型进行检查。当传入错误类型时,被调函数能够及时发现。具体的方法见6.2.2小节的内容。
另外,在参数的列表中还可以直接使用星号(*),代表调用函数时,在星号的后面的参数都必须要指定参数名称,例如:
defrecoder(strname,*,age): #定义一个函数recoder,要求形参age必须被指定
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
recoder("Gary",age= 32) #调用函数,并指定形参age
recoder("Gary",32) #错误写法。因为没有指定形参age
例子中,函数recoder的形参使用了星号,星号后面为形参age。这表明该函数被调用时age必须被指定。接下来又给出了两种调用方法,其中第一种指定了形参age。而第二种为错误方法,因为没有指定形参age。
这种方式是对第一种的改进,为某些参数提供了默认值。在调用的时候,被提供默认值的形参可以不需要有实参与其对应。没有传入实参的形参,自动会取默认值为其初始化。例如:
defrecoder(strname,age=32): #定义一个函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
调用的时候,传入一个或两个参数就可以,例如:
recoder("Gary", "32") #调用函数,传入两个参数。输出:姓名:Gary 年纪:32
recoder("Gary") #调用函数,传入一个参数。输出:姓名:Gary 年纪:32
这里要注意的地方是,有默认值的形参必须要放在没有默认值的形参后面。否则会报错。例如下面的是错误的写法:
defrecoder(age=32,strname): #错误的写法
这种写法只有一个形参,它允许在调用的时候,传入任意多的实参。当被调用时,形参会被定义为一个元组。传入的实参都是这个元组类型的形参的元素。在函数体中,可以通过访问形参中的元素来获取传入的实参。例如:
defrecoder (*person): #定义一个函数,形参person的类型为元组
print('姓名:', person[0],'年纪:', person[1]) #函数的内容为一句代码,实现将指定内容输出
调用的时候,传入两个参数,例如:
recoder("Gary", "32") #调用函数,传入两个参数。输出:姓名:Gary 年纪:32
上面代码中传入实参的个数为3个、4个等也会正常执行。如果传入一个实参会有问题。因为在函数recoder的函数体里,会获取形参的第二个元素(person[1])。如果只传入一个实参,相当于person只有一个元素,获取其第二个元素自然会失败。这是需要注意的地方。
另外,还有两点要注意的地方:
l 用这种方式就无法通过指定形参名称来传入实参了。而且传入的实参顺序与形参内部元素的顺序必须一一对应;
l 因为接收参数的类型是元组。用这种方式传值后,不能对形参内容进行修改。
最后再介绍一种调用的方法,可以支持将一个列表或元组当作实参传入。具体做法就是在列表或元组的前面加上一个星号传入。例如:
Mylist= ["Gary", "32"] #定义一个list
recoder(*Mylist) #调用函数,传入list作为实参。输出:姓名:Gary 年纪:32
到这可以发现函数recoder的参数为*person,是将接收的参数当作元组,而调用时传入了*Mylist,是一个列表的类型前面加个星。那么,如果把person前面的*与Mylist前面的*同时去掉是不是也可以呢?它与加个*的传递有什么区别呢? 读者可以先自己想一下,在6.2.3部分会详细讲解。
前面的第3种方式中,不限制实参个数是比较方便的。但是要求获取的实参与传入的顺序一一对应,这就是不方便的地方。在这个基础上进行改进:传入的实参同时,为其定义个形参。这样在函数里就可以通过指定具体形参名称来获取实参了。这种方式也是通过一个形参来接收,将该形参当成字典。这样传入的实参和对应的名字就可以放到这个字典里。形参为字典中元素的key,实参为字典中元素的value。取值的时候,直接通过字典里的key找到value即可。例如:
defrecoder (**person): #定义一个函数,形参person的类型为字典
print('姓名:', person['name'],'年纪:', person['age']) #函数的内容为一句代码,实现将指定内容输出
调用的时候,在传入实参的同时也指定了形参名称,例如:
recoder(age=32,name="Gary ") # 指定形参名称调用函数,输出:姓名:Gary 年纪:32
如果使用了这种写法,就必须为形参指定名称,不然系统会报错误。例如:
recoder("Gary", "32") #错误的写法
类似第三种方法,该方法中可以将一个字典当作实参传入。具体方法是在调用函数时,传入字典变量,并在前加两个星号。例:
Mydic= {"name":"Gary", "age":"32"} #定义一个字典
recoder(**Mydic) #调用函数,传入list作为实参。输出:姓名:Gary 年纪:32
其实上面的4种方式也可以混合使用。因为第二种方式本来就包含了第一种方式,这里不再重复。这里主要介绍下其他其中方法混合使用的情况。
(1)字典和元组的解包参数同时作为形参来接收实参:
具体做法为:定义两个形参,第一个前有一个星号,用来接收实参并转为元组;第二个前有两个星号,用来接收实参并转为字典。例如
defrecoder(*person1,**person2): #定义一个函数recoder,包括两个形参
if len(person1)!=0: #如果元组的形参接收到内容,就打印
print ('姓名:',person1[0],'年纪:',person1[1])
if len(person2)!=0: #如果字典的形参接收到内容,就打印
print ('姓名:',person2["name"],'年纪:',person2["age"])
调用时候,可以放入的实参可以指定形参也可以不指定。例如:
recoder("Gary",32) #调用函数recoder,传入不指定形参的实参,由person1接收
recoder(age=32,name="Gary") #调用函数recoder,传入指定形参的实参,由person2接收
还可以将指定形参的实参与不指定形参的实参同时放入函数来调用。例如:
recoder("Gary",32,age=32,name="Gary")#传入指定形参的实参与不指定形参的实参,person1、person2同时接收
上面的这种写法必须是不指定形参的实参在前,指定形参的实参在后,不然会报错。例如:
recoder(age=32,name="Gary","Gary",32)#错误写法
(2)字典或元组解包参数与单个形参的混合使用:
直接将字典或元组的解包参数与单个形参放在一起即可。但是放置的先后顺序会影响到调用时的写法。先看看元组解包参数在前单个形参在后的写法:
defrecoder(*person1, ttt): #定义一个函数recoder,两个形参
if len(person1)!=0:
print ('姓名:',person1[0],'年纪:', ttt)
recoder("Gary",ttt=32) #调用时需要指定后面的单个形参,输出:姓名:Gary 年纪:32
元组解包参数在前单个形参在后时,调用语句必须得指定形参名称。
如果形参在前,元组解包参数在后的时候,需要如下的写法;
defrecoder(ttt,*person1): #定义一个函数recoder
if len(person1)!=0:
print ('姓名:',ttt,'年纪:',person1[0])
recoder("Gary",32) #调用时不需要指定形参,输出:姓名:Gary 年纪:32
函数的前单个形参在前元组解包参数在后时,调用语句不需要形参名称。当然传入实参时指定形参名称也是可以的。
(3)字典解包参数、元组的解包参数、单个形参放一起使用:
当字典、元组的解包参数与单个形参放在一起时,必须保证字典的解包参数放在最后。例如:
defrecoder(ttt,*person1,**arg): #定义一个函数recoder
if len(person1)!=0:
print ('姓名:',ttt,'年纪:',person1[0])
recoder("Gary",32) #调用时不需要指定形参,输出:姓名:Gary 年纪:32
当字典解包参数、元组的解包参数、单个形参放一起,只需要注意下三者的顺序即可,对于调用的规则,还是与前面(1)(2)点一致。
如果将第一个形参(ttt)与第二个形参(*person1)颠倒一下,也是可以的。例如
defrecoder(*person1, ttt,**arg): #定义一个函数recoder
if len(person1)!=0:
print ('姓名:',ttt,'年纪:',person1[0])
recoder("Gary",ttt=32) #调用时不需要指定形参,输出:姓名:Gary 年纪:32
按照前面(2)的规则当元组的解包参数在单个形参前面时,单个形参需要被指定。所以调用的时候第二个实参指定了形参ttt。
如果想下面的写法就是错误的:
defrecoder(*person1, **arg, ttt): #错误,arg没有在最后
if len(person1)!=0:
print ('姓名:',ttt,'年纪:',person1[0])
上例中字典的解包参数在中间,没有在最后,所以错误。这是个必须要注意的地方,即便形参中带有默认实参,也需要放到字典的解包参数前面。例如:
defrecoder(*person1,ttt=9,**arg): #ttt给定了一个默认值,但是它也得放在arg前面。
if len(person1)!=0:
print ('姓名:',ttt,'年纪:',person1[0])
上例子中函数recoder的形参ttt给定了一个默认值,一般情况下这种带默认值的形参需要放在最后面(这里的先后指的是从左到右的顺)。但是有了arg的存在,就需要放在arg前面,其他形参的后面。
那么在Python中,函数的实参与形参是如何传递的呢?下面就来看看具体规则。
在函数调用中,Python本身的语法没有对传入参数类型的检查。这种做法增加了代码的灵活度,同时也提升了程序出错的概率。而在工业生产环境下应用的程序,更需要的是代码的健壮性。这种情况下,需要手动对函数的参数进行检查,来提升自有代码的健壮性。具体的方法是通过使用isinstance函数来对参数进行检查。
isinstance函数的作用是检查某个变量是属于某个类型。具体定义如下:
isinstance(obj,class_or_tuple)
l 第一个参数obj表示传入待检查的具体对象;
l 第二个参数class_or_tuple为一个tuple的类型序列或是class;
确切的说,该函数的功能是检查obj是否为class_or_tuple的一个实例。关于类和实例的的介绍可以在第9章学到。
在使用的过程中,直接在函数体的刚开始部分使用isinstance判断一下参数即可。isinstance的第一参数传入带判断的形参;isinstance的第二个参数放的就是合法的类型。例如:
defrecoder(strname,age): #定义一个函数recoder
if not isinstance(age, (int, str)): #对参数类型进行检查,合法的类型为int和str
raise TypeError('bad operand type') #如果类型错误,使用raise函数进行报错
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
recoder("Gary",age= 32.2) #掉用时传入了age为float类型
上面的代码中允许age可以传入的类型为整型和字符串类型,但是传入了浮点型。所以报错。这里使用了raise函数进行了报错提示并退出函数。关于raise类似的功能及介绍可以参考第7章内容。
在实参与形参的传递过程中,分为两种情况:传值和传引用。
l 对于不可变对象,函数调用时,是传值,意味着函数体里可以使用实参的值,但不能改变实参;
l 对于可变对象,函数调用时,是传递引用。意味着在函数体里还可以对实参进行修改。
具体情况举例如下:
不可变对象的传值是指:当传入的实参指向一个常量,或是一个元组等不允许修改的对象时,就把该对象的值传递给形参。例如:
deffun(arg): #定义一个函数
arg = 5 #在函数体里通过为形参赋值的方式改变形参
x= 1 #定义一个变量x,让它等于1
fun(x) #输出将x当作实参传入函数fun
print(x) #打印x,因为x的值(1)为不可变对象,所以其值不变。输出: 1
上面这段代码把x作为参数传递给函数,这时x和arg都指向内存中值为1的对象。
在函数体中,执行了arg = 5。由于int对象不可改变,于是创建一个新的int对象(值为5)并且令arg指向它。而x仍然指向原来的值为1的int对象,所以函数没有改变x变量。
可变对象的传递引用是指:当传入的实参指向一个list,或是一个字典等允许修改的对象时,就把该对象的值传递给形参。例如:
deffun(arg): #定义一个函数,形参为一个列表
arg.append(3) #在函数体里通过为形参添加一个元素的方式改变形参
x= [1, 2] #定义一个列表x,
fun(x) #将x传入函数
print(x) #执行完函数fun,列表x反生了变化,输出:[1, 2, 3]
这段代码同样把x传递给函数fun。x和arg都会指向同一个list类型的对象。
函数体中使用列表的append方法为其在其末尾添加了一个元素。因为列表对象是可以改变的,所以列表对象的内容发生了改变。由于x和arg指向的是同一个list对象,所以变量x的内容也发生了改变。
在6.2.1中的“6通过元组或列表的解包参数的方式:fun(*参数)”部分,抛出了一个问题,使用解包参数与列表传递有什么不同。看下面的例子:
Mylist=["Gary", "32"]
defrecoder (person): #定义一个函数,形参为person,未指定类型
person[0] = 'sss' #修改person中元素的值
print('姓名:',person[0],'年纪:', person[1]) #将指定内容输出
recoder(Mylist) #调用recoder,输出:姓名: sss 年纪: 32
print(Mylist) #将Mylist打印,输出:['sss', '32']
这个例子是个可变对象的传值,Mylist的值在调用完recoder函数发生了变化。接下来为函数recoder的参数person与调用是传入的Mylist前面同时加上*,看下使用解包参数传值的情况:
Mylist=["Gary", "32"]
defrecoder (*person):
person[0] = 'sss' ##修改person中元素的值
print('姓名:',person[0],'年纪:', person[1]) #将指定内容输出
recoder(*Mylist) # 调用函数,传入*Mylist。会报错误,提示函数recoder中不可以修改person的值
print(Mylist)
上例中,使用解包参数的方式进行值传递,并且在recoder函数中对person进行了修改,这是错误的写法,因为person是元组类型,不支持修改。所以在调用时会报错误。
这两个例子就可以所名解包参数和直接列表传值的区别。使用解包参数的函数不能对参数修改,而使用列表传值的函数是可以对参数修改的。
匿名函数一般适用于单行代码函数。它的存在会把某一简单功能的代码封装起来,让整体代码更规整一些。一般只调用一次就不用了,所以名字也省略了,于是变成了匿名函数。
匿名函数是以关键字lambda开始的,它的写法如下:
Lambda参数1,参数2…:表达式
上面的写法中,表达式的内容只能是一句话的函数,而且不能有return关键字存在。
例如:
r= lambda x,y:x*y #定义一个匿名函数实现x与y相乘
print(r(2,3)) #传入2和3,并把它们打印出来。输出:6
lambda表达式可以在任何地方使用,它相当于一个可以传入参数的单一表达式。例如在一般函数里使用:
defsum_fun(n): #定义个函数
return lambda x: x+n #返回一个匿名函数
f= sum_fun(15) #得到一个匿名函数,函数体为x+15
print(f(5)) #像匿名函数里传入5。输出:20
在上面的例子中,在一个一般函数里返回匿名函数,并为其指定了一个被加数。在调用的过程中,只需要放入另一个加数,便实现了两个数相加。
匿名函数本质与函数是没什么两样的。在实际应用中,匿名函数常常会与可迭代函数配合使用。可迭代函数就是一种有循环迭代功能的内置函数,包括reduce、map、filter等。在每个可迭代函数中,都需要指定一个处理函数.处理函数习惯上就会使用匿名函数,当然使用普通函数也是有效的。下面就来一一介绍。
匿名函数常常来与reduce函数组合使用。reduce函数的功能是依次从sequence中取一个元素,和上一次调用function的结果做参数再次调用function。其定义如下:
reduce(function,sequence, [initial])
l 第一个参数function表示所要回调的函数;
l 第二个参数sequence为一个序列类型的数据;
l 第三个参数可选,是一个初始值。
该函数本质上也可以算作是一个内嵌循环的函数。reduce函数与匿名函数的结合使用,能够以更为简洁的代码实现较复杂的循环计算功能。例如,下面使用reduce函数与匿名函数的结合写法,来实现一个求1到100的和:
fromfunctools import reduce #导入reduce函数
print(reduce(lambda x,y:x + y,range(1,101) ) ) #第一个参数是个匿名函数实现两个数加和,输出:5050
函数里面通过匿名函数实现了两个数的加和,然后使用range函数得到一个1到100的列表。依次取出列表里的值,将它们加在一起。
reduce函数一般用于归并性任务。
类似reduce函数,匿名函数还可以与map函数组合。map函数的功能是:循环所传入的序列类型中的元素,依次调用所指定的函数里。具体定义:
map(function,sequence[, sequence, ...])
l 第一个参数function表示所要回调的函数;
l 第二个参数sequence为一个或多个序列类型的数据;
该函数返回值为一个map对象。在使用时,还得用list或tuple等函数进行转化。
当map后面直接跟一个序列数据时,直接取该序列数据中的元素,依次调入前面的函数即可。例如:
t= map(lambda x: x ** 2,[1, 2, 3, 4, 5] ) #使用map函数,将列表[1,2,3,4,5]的元素平方。返回值赋给t
print(list(t)) #将t转成列表类型,并打印。输出:[1, 4, 9, 16, 25]
例子中,map函数会将传入的列表[1, 2, 3, 4, 5]中的每个元素传入匿名函数里,进行平方运算得出的值会放入新的map对象中,最后将整个map对象赋给变量t。通过list函数对t进行类型转换,生成新的列表。在新的列表里,每个元素都为原来列表元素平方后的结果。
当map后面直接跟多个序列数据时,所提供的处理函数的参数要与序列数据的个数相同。运行时,map内部会同时取每个序列数据中的元素,一起放到所提供的处理函数中。直到循环遍历完最短的那个序列。例如:
t= map(lambda x,y: x+y,[1, 2, 3, 4, 5],[1, 2, 4, 5] )#使用map将2个列表[1,2,3,4,5]的元素加和。返回值赋给t
print(list(t)) #将t转成列表类型,并打印。输出:[2, 4, 7, 9]
该例子是对两个序列中的元素依次求和。生成的新列表中的元素分别为两个原序列中对应位置的加和。
第一个序列长度是5,第二个序列长度为4。两个序列长度不相等,循环会以最小长度对所有序列进行截取。于是生成的新列表中,也只有四个元素。
map函数一般用于映射性任务。
filter函数的功能是对指定序列进行过滤。filter的处理函数只接受一个参数的输入,返回值是布尔型。处理函数输出为False的序列元素将会被过滤掉,只留下返回为True的元素。定义如下:
filter(functionor None, sequence)
l 第一个参数function表示所要回调的函数,返回布尔型。意味着某元素是否要留下;
l 第二个参数sequence为一个或多个序列类型的数据;
整个函数的返回值是一个filter类型,需要转成列表或元组等序列才可以使用。例如:
t=filter(lambdax:x%2==0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) #过滤一个列表中为偶数的元素
print(list(t)) #转成列表,并打印。输出[2, 4, 6, 8, 10]
例子中实现了通过fliter来过滤数组中偶数的元素。如果fiter的处理函数为None,则会返回结果和sequence参数相同。例如:
t=filter(None,[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) #过滤一个列表中为偶数的元素
print(list(t)) #转成列表,并打印。输出[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
当fliter的处理函数为None时,返回的元素与原序列的一样。从功能上说没有什么意义;从代码框架的角度会更有扩展性。当处理函数的输入是个变量时,就可以把filter函数的调用部分代码固定下来。为处理函数变量赋值不同的过滤函数,来实现不同的处理功能。
fliter函数一般用于过滤性任务。
每个可迭代函数返回的值都属于一个生成器对象(在后文xxx还会详细介绍),它与迭代器对象(迭代器对象在前面5.7章节有介绍)用法一样,该对象会有一个__next__方法。调用该方法可以取到内部的各个值。最终__next__将通过StopIteration 异常来停止迭代。例如:
t=filter(lambdax:x%4==0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) #过滤列表中被4整除的元素,返回元素4,8
print(t.__next__()) #使用__next__方法获取元素,输出:4
print(t.__next__()) #使用__next__方法获取元素,输出:8
print(t.__next__()) #已经没有元素,返回StopIteration
例子中通过fliter得出的结果t中只有两个元素,通过调用__next__方法可以取得内部的下一个元素。同样在没有元素的情况下会返回StopIteration异常。因为这个原理之与for循环语句的处理很类似,所以也可以使用for循环来将t对象内容意义取出。例如上面代码可以改写成:
t=filter(lambdax:x%4==0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) #过滤列表中被4整除的元素,返回元素4,8
fora in t: #使用循环取出t中元素
print(a) #打印出来,输出:4 8
可以看到对于可迭代函数的返回结果可以直接转换成列表或元组来取得,也可以使用循环来取得。这里只是那filter举例,对于所有可迭代函数都可以这样使用。
生成器对象与迭代器对象的主要区别在于生成器对象只能迭代一次,而迭代器对象可以迭代多次。所以对于可迭代函数的结果只能取一次,再次取就没有了。这个一定要注意。例如:
t=filter(lambdax:x%4==0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) #过滤列表中被4整除的元素,返回元素4,8
fora in t: #使用循环取出t中元素
print(a) #打印出来,输出:4 8
print(list(t)) #已经取过一次,所以这次就没了。输出[]
上面代码使用了一个for循环将filter的返回值打印了出来。接着想再次用list函数将filter的返回值转换成列表,并同时打印出来。由于生成器对象取一次就没了,所以转化的结果为空,打印的内容也是空。
偏函数是对原始函数的二次封装,它是属于寄生在原始函数上面的函数。可以理解为重新定义一个函数,向原始函数添加默认参数。有点像面向对象中的父类与子类的关系。
偏函数的关键字是partial,其定义如下:
partial(func,*args, **keywords)
l 第一个参数func表示所要封装的原函数;
l 第二个参数为一个元组或列表的解包参数(见6.2.1中6):代表出入的默认值(可以不指定参数名);
l 第三个参数为一个字典的解包参数(见6.2.1中7):代表出入的默认值(指定参数名)。
其中的第二个参数与第三个参数的作用是一样的,只不过是支持不同形式的传入形参默认值方式。偏函数的作用是为其原函数指定一些默认的参数。调用该偏函数时,就相当于调用了原函数,同时将默认的参数传入。在使用partial前,必须引入functools模块。下面通过例子说明:
fromfunctools import partial
defrecoder(strname,age): #定义一个函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
Garyfun= partial(recoder, strname="Gary") #定义一个偏函数
Garyfun(age= 32) #调用偏函数,传入age = 32。输出:姓名: Gary 年纪: 32
偏函数的本质是将函数式编程、缺省参数和冗余参数结合在一起。通过偏函数传入的参数调用关系与正常函数的参数调用关系是一致的。
偏函数通过将任意数量(顺序)的参数转化为另一个带有剩余参数的函数对象,从而实现了截取函数功能(偏向)的效果。在实际应用中,可以使用一个原函数,然后为其封装多个偏函数,在调用函数时全部调用偏函数。这样的代码就会更有可读性。
递归函数就是自己调用自己的函数,当然要在函数体里写出跳出函数的条件,不然会产出无穷的调用最终导致栈溢出。因为所有的函数调用都是一个压栈的过程,而系统为每个进程分配的栈空间是有限的。一旦发生无穷调用时,系统会不停往栈里写入函数地址,直到把栈写满,最后程序崩溃。
Python语言中对函数的递归调用没有做特别的优化处理。这会导致一个问题:一旦循环调用自己的次数足够多也会将栈写满,从而导致程序崩溃。所以这部分内容作者略过,为了程序的健壮性也不建议在程序中使用递归函数。凡是用递归可以解决的,用循环都可以解决。
eval与exec函数属于Python的内置函数,由于该函数的特殊性,有必要单独说明一下。更多的内置函数介绍可以参见本书附件一部分。
eval与exec都可以执行一个指定Python的代码(代码以字符串的形式提供),相当于一个Python的解释器。在使用Python开发服务端程序的时候,这两个函数用得非常广泛。比如客户端像服务端发送一段字符串代码,服务端不需要关心具体的内容,而直接空过eval或exec来执行即可。这样的设计会使服务端与客户端的耦合度更低,系统更易扩展。
先通过一个例子展示一下eval与exec的用法,代码如下:
exec("print(\"Ilove Python \")") #exec里面传入一句代码。输出:I love Python
eval("print(\"Ilove Python \")") #exec里面传入一句代码。输出:I love Python
二者不同的是eval执行完要返回结果,而exec执行完不返回结果。例如下面的代码:
a= 1
exec("a= 2") #相当与直接执行a=2
print(a) #输出a 的值为2
a=exec("2+3") #相当与直接执行2+3,但是并没有返回值,a应为None
print(a) #输出a 的值为None
a=eval('2+3') #执行2+3,并把结果返回给a
print(a) #输出a 的值为5
可以看出exec中最适合放置运行完没有结果的语句。而eval中适合放置有结果返回的语句。如果eval里要是放置一个没有结果返回的语句会怎么样呢?例如下面代码:
a=eval("a = 2")
这时会报错。提示eval中不识别等号语法。
对于exec与eval的定义有三个参数(expression, globals=None,locals=None),具体含义如下:
l expression:这个参数是一个字符串,代表要执行的语句。该语句是受后面两个参数globals字典和locals字典限制的。只有在globals字典和locals字典的作用域内的函数和变量才能被执行。
l globals:这个参数管控的是一个全局的命名空间,也就是expression可以使用全局的命名空间中的函数。如果提供了这个参数,并且没有提供自定义的__builtins__。那么会将当前环境中的__builtins__拷贝到自己提供的globals里,然后才会进行计算;如果globals没有被提供,则使用Python的全局命名空间。
l locals:这个参数管控的是一个局部的命名空间,和globals类似。当它和globals中有重复或冲突的部分时,以locals的为准。如果locals没有被提供,则默认为globals。
下面通过例子来演示参数globals作用域的作用。来观察它是何时将__builtins__拷贝到globals字典中去的。例如
dic={} #定义一个字典
dic['b']=3 #在dic中加一条元素,key为b
print(dic.keys()) #先将dic的key打印出来,有一个元素b。输出:dict_keys(['b'])
exec("a= 4", dic) #在exec执行的语句后面跟一个作用域dic
print(dic.keys()) #exec后,dic的key多了一个,输出: dict_keys(['a', '__builtins__', 'b'])
上面的代码是在作用域dic下执行了一句a=4的代码。可以看出,在exec之前dic中的key只有一个b。执行完exec,系统在dic中生成了两个新的key:a和__builtins__。其中a为执行语句生成的变量,系统将其放到指定的作用域字典里。__builtins__就是系统加入的内置key。
__builtins__是Python的内建模块,例如我们平时使用的int、str、abs等都在这个模块中。通过下面的代码可以查看__builtins__所对应的value:
print(dic["__builtins__"]) #接上面代码,将__builtins__的value取出
输出如下:
{'chr':
......
'ArithmeticError':
为了更好理解作用域的功能,看下面的代码:
dic={} #定义一个字典
a= 2
exec("a= 4", dic) #在exec执行的语句后面跟一个作用域dic
print(a) #打印a的值。发现a没有变化。输出2
print(dic['a']) #其实执行的语句所生成的变量是在dic中。输出:4
上面的代码表明exec执行的语句中,赋值的变量a并不是全局的变量a,而是新生成了一个a,存放在dic中。
下面在来看看eval函数的使用。代码如下:
dic={} #定义一个字典
dic['a']= 3 #往字典里加个键值对a:3
dic['b']= 4 #往字典里加个键值对b:4
result= eval('a+b',dic) #让dic内部的a与b对应的值相加,结果赋给result
print(result) #将结果打印出来,输出:7
在上面的代码中,eval中的语句是计算a加b,系统会自动上dic中找a和b对应的value。如果里面没有a或者b,程序会报错。
再来看看指定locals的情况下:
a=10
b=20
c=30
g={'a':6,'b':8} #定义一个字典
t={'b':100,'c':10} #定义一个字典
print(eval('a+b+c',g,t)) #定义一个字典 116
使用exec和eval时,一定要记住里面的第一个参数是字符串。而字符串的内容一定要是可执行的代码。下面以eval为例,用代码演示常犯的错误:
s="hello"
print(eval(s)) # 错
这个例子出错的地方在于字符串的内容是hello,而hello并不是可执行的代码(除非定义了一个变量叫hello)。
如果要将字符串hello通过print函数打印出来,可以写成如下的样子:
s="hello"
print(eval('s')) #对
这种写法是要eval执行“hello”这句代码。这个hello是有引号的,在代码中,代表字符串的意思,所以可以执行。同理,也可以写成这样:
s='"hello"' #s是个字符串,字符串的内容是带引号的hello
print(eval(s)) #输出hello
这种写法的意思是s是个字符串,并且其内容是个带引号的hello。所以直接将s放入到函数eval中也可以执行。
还可以不去改变原有字符串s的写法,直接使用repr函数来进行转化,也可以得到同样的效果。例如:
s="hello"
print(eval(repr(s))) # 使用函数repr进行转化,输出hello
虽然函数eval与str的返回值都是字符串。但是使用str函数对s进行转化,程序同样会报错。这是值得注意的地方。例如:
s="hello"
print(eval(str(s))) #错
为什么会有这个区别呢?
同样对带字符串s的转化,使用repr与str得到的结果是有差别的,直接将二者的结果打印出来就可以很明显的看出不同。见下面代码:
s="hello"
print(repr(s)) # 输出:'hello'
print(str(s)) # 输出:hello
可见使用repr返回的内容,输出后会在两边多一个单引号。这一功能与前面的“1.exec与eval的可接受参数” 部分完全一致,这也是其内部的真正原理。
注意:
在编写代码时,一般传入到eval或exec函数内的字符串都会是动态的,并且还需要经过若干次的类型转化与拼接。一般会使用repr函数返回的字符串来进行传入。repr函数的更多介绍可以参考9.6.1的注意部分。
如果以后接触到Tensorflow框架时,就会发现Tensorflow中的静态图就是类似这个原理实现的。Tensorflow中先将张量定义在一个静态图里。就相当于本例子中,将键值对添加到字典里一样;Tensorflow中通过session和张量的eval函数来进行具体值的运算。就相当于本例中,使用eval函数进行具体值的运算一样。
另外,在使用eval或是exec来处理请求代码时也要额外小心。它常常会被黑客利用成为可以执行系统级命令的入口点,进而来攻击网站。这种的解决方法是可以通过设置其命名空间里的可执行函数,来限制eval和exec的执行范围。
在6.3.5中提到过生成器对象的概念,它与迭代器的主要区别是只能迭代一次。这里再详细说明一下:
生成器对象外表的使用方式与迭代器一样,但是内部的原理却完全不同。迭代器是所有的内容都在内存里,使用next函数来一个一个的往下遍历;而生成器不会把内容放到内存里,每次调用next函数时,返回的都是本次计算出来的那个元素,用完之后即刻销毁。
所以当整个序列占用内存特别大的时候,使用生成器对象会节约内存。当系统的运算资源不足时使用迭代器会节约运算资源。二者在应用上的区别属于时间优先与空间优先之间区别。
下面再来介绍一下生成器函数。
生成器函数(Generator)是用来创建生成器对象的工具,它的返回值就是一个生成器对象。其形式跟函数一样。唯一不同的,是生成器使用 yield 语句返回,而不是 return 语句。例如:
defReverse(data): #定义一个函数实现字符串反转
for idx in range(len(data)-1, -1, -1):
yield data[idx] #使用yield返回具体的一个元素
forc in Reverse('Python'): #使用for循环来迭代Reverse返回的生成器对象
print(c, end=' ') #输出:n o h t y P
上面的代码中,定义了函数Reverse,功能是将一个字符串倒序,并返回生成器对象。在调用中,使用for循环对生成器对象进行遍历,并打印出来。在for循环的内部实现里,每次循环生成器都会返回一个元素,等全部循环结束后,生成器也随之结束。不会在内存里有残留。
生成器还有总简单的写法,就是用生成器表达式。它适合一句代码的逻辑。生成器表达式的写法与5.6中讲过的for循环列表推导式写法很像,也是使用for循环来实现的。不同的是 for循环列表推导式外层是中括号,而生成器表达式外层是圆括号。例如:
mylist= [x*x for x in range(3)] #使用列表推导式为一个新定义的列表赋值
fori in mylist: #使用for循环将其打印出来
print(i) #输出:0 1 4
例子中使用了for循环列表推导式,对0到2的数字计算平方,并以列表的形式输出出来。当所有语句执行完mylist的值还是纯在的。如果换成生成器表达式,则写法如下:
myGen= ( x*x for x in range(3) ) #使用生成器表达式返回一个生成器对象
fori in myGen: #使用for循环将其打印出来
print(i) #输出:0 1 4
上面就是个生成器表达式的写法。这个例子执行后输出也是0、1、4。不同的是,生成器myGen的值在打印输出之后就不在了。
Python为变量分配了4个作用域。代表在程序中出现同名变量的有效范围,具体如下:
l L:本地作用域,被当前函数包括;
l E:上一层结构中def或lambda的本地作用域 (其实就是函数嵌套的情况);
l G:全局作用域,不被任何函数包括;
l B:最后是内置作用域,Python内置的命名空间;
这几个作用域的优先级由高到底排列依次为LEGB,俗称LEGB原则。
意思就是如果代码里用到了某个变量(例如:a),系统内部会按照LEGB的顺序去不同的作用域里面找变量a,并且在第一个能够找到这个变量名的地方停下来,如果在这4个作用域中都没找到,Python会报错。
前三个作用域LEG是分布在用户代码中的,在自己的工程代码中可以看到。下面通过代码演示:
var= 1 #在全局作用域G中定义个变量var,值为1
deffun1(): #定义一个函数fun1,其内部的函数体都属于作用域E
def fun2(): #在函数fun1中定义一个函数fun2,其内部的函数体都属于作用域L
var = 3 #在L中定义一个变量var,值为3
print(var) #输出var的值
var = 2 #在E中定义变量var,值为2
fun2() #在函数fun1中中调用了fun2
fun1() #调用函数fun1,最终会进入fun2, fun2中的var生效。输出:3
这个例子中fun1中嵌入了一个函数fun2。fun2函数中调用了其函数体内自己定义的变量var。输出的结果为3。
如果将fun2函数体内的变量var去掉,代码如下:
var= 1 #在全局作用域G中定义个变量var,值为1
deffun1(): #定义一个函数fun1,其内部的函数体都属于作用域E
def fun2(): #在函数fun1中定义一个函数fun2,其内部的函数体都属于作用域L
print(var) #输出var的值
var = 2 #在E中定义变量var,值为2
fun2() #在函数fun1中中调用了fun2
调用fun1,屏幕会输出2。因为fun2中没有了变量var,根据LEGB原则会去E作用域中找,在E作用域中有个var值为2,所以就输出了2。
如果接着再将fun1函数中的var定义去掉,代码如下:
var= 1 #在全局作用域G中定义个变量var,值为1
deffun1(): #定义一个函数fun1,其内部的函数体都属于作用域E
def fun2(): #在函数fun1中定义一个函数fun2,其内部的函数体都属于作用域L
print(var) #输出var的值
fun2() #在函数fun1中中调用了fun2
再调用fun1,屏幕会输出1。系统会根据LEGB原则,逐层去找var。最终在G作用域下找到,此时G作用域下的var会生效。
第四个作用域B属于Python中的内置作用域,可以通过如下代码查看
importbuiltins
dir(builtins)
输出结果如下:
['ArithmeticError',
......
'tuple',
'type',
'vars',
'zip']
如果在本地作用域L或嵌套作用域E下,想对全局作用域G的变量赋值等操作,可以使用global语句。例如:
a=6 #全局变量a为6
deffunc(): #定义一个函数
global a #获得全局变量a
a=5 #对a赋值5
func() #调用函数func
print(a) #将a的值打印出来,输出5
例子中函数func内对全局变量修改了。所以最终输出的值为5。如果不加global这句的话,将会输出6。例如:
a=6 #全局变量a为6
deffunc(): #定义一个函数
a=5 #对a赋值5
func() #调用函数func
print(a) #将a的值打印出来,输出6
这个例子中,函数func为a赋值,a会被当作一个函数func的本地作用域下的变量处理。所以并没有对全局变量a进行赋值。打印全局变量a时,最终输出的还是6。
Global语句是可以引用全局的优先级变量。类似global语句,nonlocal可以引用比其优先级低的作用域下的变量。如果在本地作用域L或嵌套作用域E下,想对全局作用域G的变量赋值等操作,可以使用global语句;而使用nonlocal语句则会在本地作用域以外,按照优先级的顺序就逐级去找声明的变量。并引用该变量。例如:
a=6 #全局变量a为6
deffunc(): #定义一个函数
a=7 #获得全局变量a
def nested(): #内嵌函数nested
nonlocal a #使用nonlocal关键字,引用外层的a
a+=1 #对a进行加一操作,改变a的值
nested() #调用函数nested
print("本地:",a) #将本地变量打印。输出:本地:8
func() #调用函数func
print("全局:",a) #将a的值打印出来,输出:全局:6
例子中函数func内使用了内嵌函数nested。在nested体内对func的变量进行了修改。所以最终输出的值中全局变量没变,为6;func的本地变量变了,为8。
工厂函数某种程度的实现了面向对象思想的编程方法(关于面向对象思想在第9章也会有介绍),它可以让代码更有层次。在复杂程序架构中,利用工厂函数的思想来做架构设计会使代码更有扩展性。
首先利用作用域的知识来实现一个偏函数的功能。例如6.4中的偏函数的例子,还可以写成如下:
defrecoder(strname,age): #定义一个函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
defGaryfun(age): #实现了偏函数的功能
strname = 'Gary' #定义了本地作用域下的变量
return recoder(strname,age) #直接将固定的变量strname传入
Garyfun(age= 32) #调用偏函数,传入age = 32。输出:姓名: Gary 年纪: 32
其实上述这个写法就已经实现了工厂函数。为了更深入理解,接着上面的代码,再加一个函数。如下:
defAnnafun(age): #再定义一个工厂函数Annafun
strname = 'Anna' #定义了本地作用域下的变量
return recoder(strname,age) #直接将固定的变量strname传入
Annafun(age= 37) #调用偏函数,传入age = 37。输出:姓名: Anna 年纪: 37
上面两段代码中,有两个工厂函数。分别是对recoder的封装。一个是Garyfun,放置的是默认名字Gary;另一个是Annafun,放置的默认名字是Anna。它们有同样的属性,都可以传入年龄。并将名字和年龄一起输出。
例子中,基于这种编程思想的实现需要编写不同的封装函数,每一个函数里都指定了一个strname变量。对于Anna与Gary两个名字实现了两个函数的封装。如果要支持的strname名字有n个,是不是需要写n个封装函数与之对应呢?这显然是不可取的方法。下面介绍一种简化的方式来解决这个问题,使用闭合函数(closure)。
闭合函数又叫闭包函数,本质上与普通工厂函数的编程思想一致,是普通工厂函数的更优化形式。由自由变量与嵌套函数组成。其实现方法是:将名字作为自由变量,将原有的recoder函数作为嵌套函数。通过一次函数的封装即可实现前面的功能。代码如下:
defwapperfun(strname): #闭合函数, strname为自由变量
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
fun= wapperfun ('Anna') #自由变量设为Anna
fun(37) #为age赋值,输出:姓名: Anna 年纪: 37
fun2= wapperfun ('Gary') #自由变量设为Gary
fun2(32) #为age赋值,输出:姓名: Gary 年纪: 32
闭合函数wapperfun中实现了一个嵌入函数recoder,并将嵌入函数recoder返回。嵌入函数recoder中调用了外部变量strname,这个strname是由warpperfun被调用时传入的参数。当warpperfun被调用后,返回了自身的嵌入recoder同时,又将自身的参数strname与内嵌recoder绑定起来,赋值给了fun。fun就等同一个strname被初始化后的recoder函数。此时的fun可以理解为一个recoder的闭合函数,自由变量strname存在于该闭合函数之内。
闭合函数会比普通的函数多一个属性__closure__,该属性会记录着自由变量的参数对象地址。当闭合函数被调用时,系统就会根据该地址找到自由变量,完成整体的函数调用。接着上面的代码,演示查看__closure__属性的代码如下:
print(fun.__closure__) #输出(
可以看到显示的内容是一个字符串对象,这个对象就是fun中自由变量strname的初始值。__closure__属性的类型是一个元组,表明闭合函数可以支持多个自由变量的形式。
装饰器是Python语言中,专门为软件工程服务的编程思想。在软件工程中,一个项目的多个版本间迭代要尽量遵循开发封闭原则。即,对于已经实现的功能代码不允许被修改,但可以被扩展。
装饰器的主要作用就是在扩展原有功能基础上,最大化的使用已有代码。也可以理解成在不改变原有代码实现的基础上,添加新的实现功能。它的实现做法主要是在原有的函数外面再包装一层函数,使新函数在返回原有函数之前实现一些其他的功能。例如,可以修改6.9.2中的wapperfun函数,为其增加参数校验的功能。修改后的代码如下:
defcheckParams(fn): #装饰器函数,参数是要被装饰的函数。相当于闭合函数
def wrapper(strname): #定义一个检查参数的函数
if isinstance(strname, (str)) : #判断是否是字符串类型
return fn(strname) #是则调用fn(strname)返回计算结果
print("variable strname is not astring tpye") #如果参数不符合条件,则打印警告,然后退出
return
return wrapper #将装饰后的函数返回
defwapperfun(strname): #闭合函数, strname为自由变量
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
wapperfun2= checkParams (wapperfun ) #对wapperfun进行装饰,即,将自由变量设为wapperfun函数
fun= wapperfun2 ('anna') #wapperfun2为带有参数检查的闭合函数
fun(37) #为age赋值,输出:姓名: Anna 年纪: 37
fun= wapperfun2 (37) #当输入参数不合法时,输出:variablestrname is not a string type
这段代码中,添加了一个checkParams函数。checkParams为装饰器函数,返回值为内部定义的wrapper函数。为了实现对原有函数wapperfun 的参数检查,将wrapper函数的参数与wapperfun参数保持一致。当检查参数合法时,在条用原有函数,并将参数透传进去;当检查参数非法时,就打印警告。
在使用时,直接将函数wapperfun传入到checkParams中去,来完成对原函数wapperfun的装饰得到带有参数检查的闭合函数wapperfun2。于是在最后一行代码中传入值为37而不是字符串时,系统就会打印警告。
装饰器本质就是个闭合函数,只不过自由变量是一个函数而已。它的存在可以使代码的重用性与扩展性大大加强。可以想象这样一种场景,假设前面6.9.2的例子是某个系统低版本的部分代码。在使用过程中,发现在调用wapperfun函数时,偶尔会输入一个数字或非字符串类型导致程序出错。面对这种情况,就可以在新的版本中对wapperfun进行装饰使其具有参数检查的功能。实际使用时,可以将本节例子代码中的wapperfun2全部变成wapperfun( 装饰函数的语句就会变成:wapperfun =checkParams (wapperfun ) ),这样得到的wapperfun就具有了参数检查的功能,代码的改动量也会变得更小。
在实际情况中,装饰器的应用场景非常广泛。除了为函数添加参数检查,还可以为函数添加调试信息、日志等功能。
6.9.3中的装饰器函数,还有另外一种写法,就是使用@修饰符。@修饰符的作用是直接可以在原函数定义时就为其指定修饰器函数。等同于将6.9.3中的代码wapperfun = checkParams (wapperfun)移到了wapperfun的定义部分。
这么做的好处是使修饰器与被修饰函数的关系更加明显,也使得需要修饰的函数在第一时间得到修饰,降低了编码出错的可能性。
@修饰符的语法是需要在其后面添加修饰器函数,同时在其下一行添加被修饰函数的定义。将6.9.3的例子使用@修饰符的方法改写后,代码如下:
defcheckParams(fn): #装饰器函数,参数是要被装饰的函数。相当于闭合函数
def wrapper(strname): #定义一个检查参数的函数
if isinstance(strname, (str)) : #判断是否是字符串类型
return fn(strname) #是则调用fn(strname)返回计算结果
print("variable strname is not astring tpye") #如果参数不符合条件,则打印警告,然后退出
return
return wrapper #将装饰后的函数返回
@checkParams #使用@修饰符来实现对wapperfun的修饰
defwapperfun(strname): #闭合函数, strname为自由变量
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
fun= wapperfun ('Anna') #wapperfun为带有参数检查的闭合函数
fun(37) #为age赋值,输出:姓名: Anna 年纪: 37
fun= wapperfun(37) #当输入参数不合法时,输出:variablestrname is not a string type
使用一个@符号,就可以把修饰器与被修饰函数的关系简洁的体现出来。这也是编写代码中较常用的写法。
了解完装饰器的原理之后,在来学习下Python中更高级的装饰器。这些高级装饰器在真正编程场景中,是最为常用的,因为它可以灵活的适应很多情况。
在6.9.3的例子中,装饰器checkParams的内部实现了一个wrapper函数。wrapper函数的参数要求必须与被装饰函数参数一样。这样才能够实现对被装饰函数的参数检查,并将输入参数透传给被装饰函数的功能。
为了在定义装饰器时,解耦内部wrapper的参数对于被装饰函数参数的强依赖关系。可以使用能够适应任何参数的通用参数装饰器。
通用参数装饰器的实现很简单,利用了6.2.1中参数的定义知识。即,在wrapper函数的参数中,使用字典和元组的解包参数同时作为形参来接收实参。这样得到的装饰器便可以使用与各种函数。例如,将6.9.4的代码改写成通用参数装饰器的形式(只改变checkParams函数即可),如下:
defcheckParams(fn): #通用参数装饰器函数定义
def wrapper(*arg, **kwargs): #使用字典和元组的解包参数同时作为形参来接收实参
if isinstance(arg[0], (str)) : #判断第一个参数是否是字符串类型
return fn(*arg, **kwargs) #满足条件,则将参数透传给原函数,并返回
print("variable strname is not astring tpye") #如果参数不符合条件,则打印警告,然后退出
return
return wrapper #将装饰后的函数返回
用上面代码来替换6.9.4中的实例代码前7行,整个程序还可以正常运行。但是装饰器checkParams却变得更为灵活,它不仅仅只适应于对wapperfun的装饰。只要需要对第一个参数做字符串类型检查的函数,都可以用checkParams来装饰。这就是通用参数装饰器的便捷之处。
通用参数装饰器解决了装饰器参数与被装饰函数间的强关联关系。为了让装饰器有更大的通用性,还可以通过在装饰器的外部传入参数的方式,来告诉装饰器当时使用的外部情况。这样装饰器内部就可以通过传进来的外部变量来选择不同的执行分支。从而可以适应更多的调用场景。
可接收参数的通用装饰器在是现实时,需要在原有的通用参数装饰器外面再加一层函数封装,这个函数的参数就是用来接收外部变量的。例如:
defisadmin(userid): #可以接收参数的装饰器函数
def checkParams(fn): #通用参数装饰器函数
def wrapper(*arg, **kwargs): #定义一个检查参数的函数
if userid !='admin': #对外部调用环境进行判断,不是admin则直接返回
print('Operation isprohibited as you are not admin! ')
return
if isinstance(arg[0], (str)) : #判断是否是字符串类型
return fn(*arg, **kwargs) #满足条件,则将参数透传给原函数,并返回
print("variable strname is nota string tpye") #如果参数不符合条件,则打印警告,然后退出
return
return wrapper #将装饰后的函数返回
return checkParams
@ isadmin(userid='admin') #在admin模块中,传入admin身份到装饰器
defwapperfun(strname): #闭合函数, strname为自由变量
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
@ isadmin(userid='user') #在user模块中,传入user身份到装饰器
defwapperfun2(strname): #闭合函数, strname为自由变量
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
fun= wapperfun ('anna') #wapperfun为带有参数检查的闭合函数
fun(37) #为age赋值,输出:姓名: Anna 年纪: 37
fun= wapperfun2(37) #身份不对,输出:Operation is prohibited asyou are not admin!
假设外部情况有两个模块:一个是admin、一个是user。在上面的代码中,为通用参数装饰器checkParams外部添加一层封装,变为isadmin装饰器。该装饰器的意义为只允许调用者的身份为admin。在admin中实现了函数wapperfun,user中实现了函数wapperfun2,两个函数都用isadmin来装饰。在实际调用时,wapperfun可以正常使用,但是wapperfun2在调用时就会显示“Operation isprohibited as you are not admin!”信息。原因是isadmin装饰器中对传入的外部参数进行了判断。只有admin才会正常运行。
与普通的装饰器相比,与@注解后面跟着可接收参数的装饰器等同的代码如下:
@ isadmin(userid='user') #在user模块中,传入user身份到装饰器
defwapperfun2(strname): #闭合函数, strname为自由变量
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
等同于
defwapperfun2(strname): #闭合函数, strname为自由变量
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
wapperfun2= isadmin (userid='user')(wapperfun2)
上面最后一行的语句中,先执行isadmin(userid='user')返回一个装饰器checkParams函数,再调用checkParams函数参数是wapperfun2,返回wrapper函数。所以这时执行下列代码:
print(wapperfun2.__name__) #输出:wrapper
打印出来的是wrapper。表明最终会将wrapper函数返回并赋值给wapperfun2。这反映了个客观的现象:装饰器在装饰函数的时候,改变了函数本身的名称。为了避免这个现象,可以使用如下写法。
当函数被装饰完时,对函数的名字属性再赋一次值,将函数的名称恢复过来。这样就可以避免装饰完后,函数名字变化的现象。例如,将isadmin函数改写如下:
defisadmin(userid): #可以接收参数的装饰器函数
def checkParams(fn): #通用参数装饰器函数
def wrapper(*arg, **kwargs): #定义一个检查参数的函数
if userid !='admin': #对外部调用环境进行判断,不是admin则直接返回
print('Operation isprohibited as you are not admin! ')
return
if isinstance(arg[0], (str)) : #判断是否是字符串类型
return fn(*arg, **kwargs) #满足条件,则将参数透传给原函数,并返回
print("variable strname is nota string tpye") #如果参数不符合条件,则打印警告,然后退出
return
wrapper.__name__ = fn.__name__ #将函数名称属性恢复
return wrapper #将装饰后的函数返回
return checkParams
上面代码中的倒数第三行,为新加的语句。该语句的意思是将被装饰的函数名称赋给wrapper函数。这样装饰器返回的wrapper函数就与被装饰的函数名称一致了。
另外Python中还提供了一个内置的装饰器函数functools.wraps,它的作用就是将被装饰的函数名称还原赋值给装饰后的返回函数。不过在使用时需要先引入functools模块。例如:
import functools
defisadmin(userid): #可以接收参数的装饰器函数
def checkParams(fn): #通用参数装饰器函数
@functools.wraps(fn) #内置的装饰器,用于恢复函数名称
def wrapper(*arg, **kwargs): #定义一个检查参数的函数
if userid !='admin': #对外部调用环境进行判断,不是admin则直接返回
print('Operation isprohibited as you are not admin! ')
return
if isinstance(arg[0], (str)) : #判断是否是字符串类型
return fn(*arg, **kwargs) #满足条件,则将参数透传给原函数,并返回
print("variable strname is nota string tpye") #如果参数不符合条件,则打印警告,然后退出
return
return wrapper #将装饰后的函数返回
return checkParams
上面代码的第一行,引入了functools模块。紧接着,在第4行,使用了@符号对wrapper进行装饰,这样wrapper的函数名称就会变成与传入的fn参数一样的函数名称。实现了装饰之后的函数与原函数名称一致的功能。
在软件工程学会非常注重代码的可复用性,装饰器的使用可以使代码拥有更好的可复用性。在实际编码中,可以将具有不同功能的装饰器合起来用,来实现组合装饰的效果。
实现组合装饰时,仅需要将不同的装饰器使用@符号一行一行的堆叠起来即可。例如:
defcheckParams(fn): #通用参数装饰器,用于检查参数
def wrapper(*arg, **kwargs): #使用字典和元组的解包参数同时作为形参来接收实参
if isinstance(arg[0], (str)) : #判断第一个参数是否是字符串类型
return fn(*arg, **kwargs) #满足条件,则将参数透传给原函数,并返回
print("variable strname is not astring tpye") #如果参数不符合条件,则打印警告,然后退出
return
return wrapper #将装饰后的函数返回
deflogging(userid): #可接收参数的通用装饰器,用于打印日志
def checkParams(fn): #装饰器函数,参数是要被装饰的函数。相当于闭合函数
def wrapper(*arg, **kwargs): #使用字典和元组的解包参数同时作为形参来接收实参
print(userid,end=':') #将调用者的身份打印出来
return fn(*arg, **kwargs) #将参数透传给原函数,并返回
return wrapper #将装饰后的函数返回
return checkParams
@logging(userid= 'admin') #多行@符号,实现组合装饰
@checkParams
defwapperfun(strname): #被装饰函数
def recoder(age): #定义一个嵌套函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
return recoder #返回recoder函数
fun= wapperfun ('anna') #wapperfun为带有参数检查的闭合函数
fun(37) #打印的结果带有调用身份,输出:admin:姓名: anna 年纪: 37
上面代码中,实现了两个装饰器:checkParams与logging。checkParams是用于参数检查;logging是用于输出调用者的身份信息。在函数wapperfun定义的前面分别使用@符号进入了装饰器checkParams与logging,同时为装饰器logging传入了调用身份admin。这样在最后一行的代码执行时,就会输出的admin信息。
善于使用装饰器,可以将代码按照实现功能的主次逐层分开(例如:核心功能、安全检查、日志信息等),使代码更有调理。同时将核心功能与参数检查、外围交互等功能逐层分开的思想,也大大降低了代码的复杂度。这就是面向切面的编程思想(AOP是Aspect Oriented Program)。
在上面的例子中的两个装饰器:checkParams与logging,在实际运行中的顺序是什么样的呢 ?这里就来剖析一下。
多装饰器的载入顺序是从下往上的。在调用时,执行的函数顺序是从上往下的。怎么理解这句话呢?来看下面这段代码:
deflogging(fn): #logging函数
print ('in logging')
def wapper_logging(*args, **kwargs):
print ('in wapper_logging')
return fn(*args, **kwargs)
return wapper_logging
defcheckParams(fn): #checkParams函数
print ('in checkParams')
def wapper_checkParams(*args, **kwargs):
print ('in wapper_checkParams')
return fn(*args, **kwargs)
return wapper_checkParams
@logging
@checkParams
defwapperfun(strname): #被装饰函数
print ('姓名:',strname) #函数的内容为一句代码,实现将指定内容输出
上面代码中将每个函数里都加上一句打印的话,用于演示其内部的调用关系。整个代码运行后,会输出如下结果:
incheckParams
inlogging
这时代码中根本没有调用的语句,但是同样会打印出内容。这表明装饰器在整个代码载入时,就开始执行了。而且顺序是按照@的顺序,从下往上加载装饰器函数的(结果中先输出了下面的checkParams函数,后输出了上面的logging函数)。接下来再执行一次调用语句,如下:
wapperfun("Anna") #对装饰函数进行调用
这句话运行后,后有下列输出:
inwapper_logging
inwapper_checkParams
姓名: Anna
这次的输出中,依照@的顺序,上面的wapper_logging被先执行,下面的wapper_checkParams被后执行。整体过程相当于一个压栈弹栈的过程,即,载入时下执行的装饰函数其返回的函数在最后被调用。这就是多装饰器的调用顺序。
在使用工厂函数时,常会配合循环来批量生成多个函数。这里有个特例,值得注意:如果通过循环来生成工厂函数,循环时该作用域下的默认参数值会被循环值所覆盖。这种情况会导致,所有在这个循环中产生的函数都将会有相同的默认值。例如:
defrecoder(strname,age): #定义一个函数recoder
print ('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
defmakerecoders(): #该函数的作用将要批量生成工厂函数
acts=[]
for i in ["Gary","Anna"]: #通过循环将列表中的元素作为默认值,依次生成工厂函数
acts.append(lambda age:recoder(i,age)) #每个生成的函数放到列表里
return acts #将批量的函数返回
fora in ( makerecoders()): #调用函数批量生成工厂函数,并用for便利每个函数
a(age = 32) #将每个函数取出,一次调用。输出:
# 姓名: Anna 年纪: 32
# 姓名: Anna 年纪: 32
例子中,想通过for循环遍历列表来批量生成工厂函数。其中列表里的值为工厂函数的默认值。但在实际调用过程中,发现只有列表中最后的一个元素的值起到了默认值的作用。前面的元素都没有生效。这是由于在循环时,后面的变量覆盖了前面的变量。
为了避免这种情况发生,在循环生成工厂函数的过程中,就不能将默认值放到作用域空间来存储。必须要当作参数传入到原函数recoder中。例如:
defrecoder(strname,age): #定义一个函数recoder
print('姓名:',strname,'年纪:',age) #函数的内容为一句代码,实现将指定内容输出
defmakerecoders(): #该函数的作用将要批量生成工厂函数
acts=[]
for i in["Gary","Anna"]: #通过循环将列表中的元素作为默认值,依次生成工厂函数
acts.append(lambdaage,i=i:recoder(i,age)) #将循环值i作为参数也传入到匿名函数里
return acts #将批量的函数返回
fora in ( makerecoders()): #调用函数批量生成工厂函数,并用for便利每个函数
a(age = 32) #将每个函数取出,一次调用。输出:
# 姓名: Gary年纪: 32
# 姓名: Anna 年纪: 32
这次在函数makerecoders中的for循环内,将循环值i也作为参数传入了匿名函数里。避免了本地作用域下的变量在循环中会被覆盖的问题。实现了正确的功能。输出的两个函数一个默认值为Gary一个默认值为Anna。
配套免费视频:http://v.qq.com/vplus/1ea7e3c40fd64cd5a25e9827b38c171e/foldervideos/xvp0024019vk2to