概述
Python
四、函数
1、函数的介绍
在Python中,如果反复使用同一段代码来实现一个功能,那么就应该把这段代码封装成一个函数,提高代码的可读性和维护性。函数使用关键字def
来定义,语法如下:
def say_hello(参数): #def后面是函数名,要遵循变量名的命名规则
print('hello') #函数要执行的代码
say_hello() #调用函数
注意,函数在定义之后代码并不会执行,需要再次调用函数代码才会运行。
2、函数的参数
在定义函数时,如果需要传入参数,就必须定义参数。
def say_hello(name): #定义了一个参数,这个参数是形式上的传入,只是起一个指引作用
print('hello,%s'%name)
say_hello('jack') #传入一个实际参数
#hello,jack
在上面的函数中,定义的变量name
一般称为形参,作用是用来占位和指引的。而调用函数时传入的'Jack'
称为实参,是实际参与代码的数据。
3、函数的返回值
定义好函数后,如果需要得到函数的运行结果,就需要将函数的结果返回到用户手中,使用关键字return
来实现。函数也可以没有返回值,如果没有返回值,函数就返回None
。注意:None并不等同于数学意义上的0,它的意义更接近于‘无’
def add1(a,b):
c=a+b
return c
def add2(a,b):
c=a+b
print(add1(2,2)) #4
print(add2(2,2)) #None
4、函数的文档说明
在实际开发中,如果函数比较复杂,为了提高理解性,需要在函数中添加文档说明
def function_name(param1,param2,param3...):
'''
这是一个实现。。。。功能的函数
:param1:这是。。。。参数
:param2:这是。。。。参数
:param3:这是。。。。参数
:return:这个函数返回。。。
'''
pass
5、全局变量和局部变量
Python中,在函数外部定义一个变量,那么称这个变量为全局变量,在函数内部定义一个变量,则称其为局部变量。两者的区别是局部变量的作用域仅限于函数内部。在Python中,只有函数能够分割作用域。
在函数内部,能够访问全局变量的内容,但不能直接修改全局变量;在函数外部,不能访问局部变量,因为局部变量的作用域仅限于函数内部。
a=100 #声明一个全局变量并对其赋值
def func():
b=a #声明一个局部变量
print('局部变量b=%d'%b)
func() #局部变量b=100
print('局部变量b=%d'%b) #报错 尝试在函数外部访问局部变量b时,无法访问
#函数内部能够访问到全局变量,但不能修改
a=100 #声明一个全局变量并对其赋值
def func():
a=50 #尝试修改一个全局变量的值
func()
print('全局变量a=%d'%a) #全局变量a=100
在函数内部修改全局变量实际上只是在函数内部创建了一个与全局变量名’a’一模一样的局部变量,并没有真正的对全局变量进行修改。如果非要在函数内部修改全局变量,则需要使用关键字golbal
a=100 #声明一个全局变量并对其赋值
def func():
global a #在函数内部声明变量a是一个全局变量
a=50 #尝试修改一个全局变量的值
func()
print('全局变量a=%d'%a) #全局变量a=50
可以使用内置函数globals()
和locals()
来查看程序中所有的全局变量和局部变量
a=100
def func():
b=a
print(globals()) #{...'a': 100, 'func': <function func at 0x00000000027A7040>}} 函数名也是一个变量
print(locals()) #{'b': 100}
func()
#注意,如果locals()函数写在函数外面,作用域就是全局,实际上查看的与globals()函数查看到的内容一样
6、函数的多个返回值
在函数中,使用关键字return
来返回函数的结果,返回的结果可以是一个也可以是多个。注意,正常的函数中只能有一个return
语句,在同一段代码中,如果出现多个return
语句,只会执行第一个语句。例外:在finally
语句中,可以有多个return
并存,但后面的return
返回的值会覆盖前面的返回值。
def add(a,b):
return a+b,a-b #返回2个值,中间用逗号隔开,返回的内容实际上是一个元组
def sub(a,b)
return [a+b,a-b] #返回值还可以是列表、字典等
7、函数的参数
函数定义时的参数叫形参,调用时的参数叫实参
7.1、位置参数
当定义一个函数时,未对一个形参进行除命名外的任何操作,那么这个形参就是一个位置参数。
def power(x,n): #定义一个计算x的n次方的函数
s=1
if n==0:
return 1
else:
while n>0:
n=n-1
s=s*x
return s
在上面的函数中,我们定义了一个求n次方的函数,这个函数有2个参数,在调用函数时,需要对应参数的位置传入正确的数值。对于函数power()
而言,参数x,n就是2个位置参数。
#计算8的3次方
power(8,3) #512 传入的参数位置需要与定义的位置一一对应
power(3,8) #6561 错误,传入的参数位置不对,计算的是3的8次方
7.2、默认参数
在上面的函数中,我们可以利用power(x,n)来计算任意数的n次方,但我们常用的一般是计算一个数的平方,因此,我们可以把函数做一下修改。
def power(x,n=2): #定义一个计算x的n次方的函数
s=1
if n==0:
return 1
else:
while n>0:
n=n-1
s=s*x
return s
在定义函数时,我们给参数赋一个初始值作为默认值,当函数调动时,如果这个参数不传入新值,就是用默认值;传入新值,使用新值计算。
power(6) #36 只传入x的值,默认计算x的2次方
power(6,3) #216 给参数n传入新值,按新值计算
在上面的示例中,参数n就是一个默认参数。当函数有多个参数时,可以把变化较少的参数设置为默认参数,方便函数的调用。当一个函数有多个默认参数时,调用函数时既可以按顺序提供默认参数,亦可以用参数名来指定默认参数。
#定义一个小学生注册信息的函数,包含姓名、性别、年龄、城市等信息
#年龄和城市的信息是变化较小的参数,因此可以把这两者设为默认参数
def enroll(name,gender,age=6,city='chengdu'):
print('name:',name)
print('gender:',gender)
print('age:',age)
print('city:',city)
enroll('李霞','female',7) #只提供了一个默认参数,按照顺序分配给age
#name: 李霞
#gender: female
#age: 7
#city: chengdu
enroll('kabin','male',city='beijing') #使用参数名指定默认参数,直接分配给city
#name: kabin
#gender: male
#age: 6
#city: beijing
通过使用默认参数,可以降低函数调用的难度,但使用默认参数也要注意,由于默认参数的值在函数定义时已经被计算出来(参数名也是一个变量,默认参数定义时参数名会指向内存的一个地址),一旦默认参数指向的是一个可变数据,调用就可能出问题。
#定义一个函数,每次调用都在列表的后面增加一个end,然后结束
def ap(L=[]): #把一个空列表作为默认参数
L.append('end')
print(L)
ap([1,2,3]) #[1, 2, 3, 'end'] 正常调用没有问题
ap() #['end'] 调用空列表好像没有问题
ap() #['end', 'end'] 第2次调用就出现问题了,继续调用,end的个数就会增加
之所以出现上面的问题是因为列表是一个可变数据,当默认参数初始化时,使用默认参数,参数名的地址指向不变,使用的还是同一个地址,里面的数据在调用一次后已经被改变了。正常调用之所以看起来正常是因为传入的新列表实际指向了内存中一个新的地址,与默认参数指向的是不同的内存地址,所以不会出错。要解决上面的问题,可以把参数名L
指向一个不可变数据。
def ap(L=None):
if L==None:
L=[]
L.append('end')
print(L)
ap() #['end']
ap() #['end']
使用默认参数时,需要注意以下2点:
-
默认参数和位置参数的先后顺序:位置参数在前,默认参数在后,否则会报错。如果还有其他类型的参数,使用默认参数一定要带参数名进行传入,否则极易出错
-
默认参数必须指向一个不可变数据
7.3、可变参数
假设我们需要定义一个函数,实现计算a²+b²+c²+d²+……的功能,但我们在定义时不可能知道传入的究竟有几个参数,所以可以利用可变参数来实现,可变参数就是指在函数的定义中,传入的参数数量可以改变的参数类型。
def square_sum(*numbers): #在参数名前面使用一个*,代表这个参数是可变参数
sum=0
for num in numbers:
sum=sum+num*num
return sum
print(square_sum(2,3,4,5)) #54
print(square_sum()) #0
在Python中,利用*
可以定义可变参数,它的功能实际上是把数据打包成一个元组类型的数据传入函数内部,也允许传入0个参数。同时,如果调用的参数本身就是一个元组或列表,使用*
还可以对参数进行解包,方便函数的调用。
L=[1,2,3,4,5]
#列表L要使用上面的函数,普通写法是这样:
print(square_sum(L[0],L[1],L[2],L[3],L[4])) #55
#上面的写法不仅繁琐,数据量过大的话还容易出错,可以使用*简化调用
print(square_sum(*L)) #55
利用*
对列表或元组进行解包是很常见的写法,而且很实用。
7.4、关键字参数
与可变参数类似,可变参数允许用户调用函数时传入0个或任意个参数,关键字参数允许用户在调用函数时传入0个或任意个带参数名的参数。关键字参数使用**
来定义
#定义一个用户注册的功能,必须填写的信息时姓名和年龄,其他的信息未知
def enroll(name,age,**kw):
print('name={},age={},other={}'.format(name,age,kw))
enroll('John',25)
#name=John,age=25,other={} 不传入关键字参数时,函数内部接受到的就是一个空字典
enroll('lisa',26,job='teacher',city='new york')
#name=lisa,age=26,other={'job': 'teacher', 'city': 'new york'}
使用关键字参数,实际上就是把参数打包成一个字典传入函数中,使用时必须传入带参数名的参数,如果缺少参数名,函数会把这个参数当做是位置参数。
通过上面的函数,可以发现关键字参数的最大作用是扩展函数的功能,对于enroll()
函数,函数要求用户只传入姓名和年龄,但如果用户想提供更多的信息也是允许的,通过关键字参数,能够把额外的信息存储起来,在需要的时候能够进行查阅。
与可变参数一样,调用函数时传入的参数如果是一个字典,可以使用**
来进行解包。
info={'name':'jack','age':27,'job':'engneer','TEL':'134-4567-3456'}
enroll(**info)
#name=jack,age=27,other={'job': 'engneer', 'TEL': '134-4567-3456'}
7.5、命名关键字参数
通过关键字参数可以传入任意个带参数名的值,但如果要限制传入的内容,比如我们要限定只额外允许传入Job
和city
的信息,就可以使用命名关键字参数。
命名关键字参数与关键字参数不同,在参数名的前一个位置增加一个*
,用来表示*
后面所有的参数都是命名关键字参数。
def enroll(name,age,*,job,city): #注意*是与参数名分开的
print(name,age,job,city)
如果函数的参数中有可变参数,则命名关键字参数的*
可以不写。
def enroll(name,*personal_info,job,city):
#有一个可变参数personal_info,后面的job和city就是命名关键字参数
print(name,personal_info,job,city)
命名关键字参数在使用时,必须传入对应的带参数名的值,它与可变参数或关键字参数不一样,不允许传入0个值。另外,命名关键字参数也可以使用默认值来简化调用
def enroll(name,age,*,job,city='北京'): #给一个命名关键字参数设置默认值
print(name,age,job,city)
enroll('peter',24,job='engneer')
#peter 24 engneer 北京
7.6、参数组合
在Python中,定义函数时可以使用上述的5中参数,但必须注意参数定义的先后顺序:位置参数、可变参数、默认参数、 命名关键字参数、关键字参数。
函数调用时,如果需要指定默认参数,其参数名不能省略,位置在可变参数之后,可以在关键字参数之前,也可以在其后,也可以混杂其中
参数的顺序混淆混导致传入的数据混乱或者直接报错,因此在使用时要小心使用。另外,建议不要再同一个函数中同时使用过多的参数类型,过多的参数类型 会导致函数接口的可读性很差。
8、函数使用的注意事项
- 在Python中,函数名不允许重名,如果有重名的函数,后面的函数会覆盖掉前面的函数
- 函数的名字也是一个变量名,允许进行变量的操作,当函数名的指向发生改变时,函数就被覆盖了
def test(a):
print('hello,%s'%a)
say_hello=test #定义一个变量来指向函数名代表的地址
say_hello('jack') #hello,jack say_hello也可以当成一个函数用w1
test=5
test('adam') #报错,函数名的指向已经发生改变,不再指向函数本体
9、递归函数
函数中可以调用其他函数,当在函数代码内部调用自身时,我们称这种函数为递归函数。
#用递归函数实现1~n之间的数字之和
#分析fab(n=1+2+3+...+n=n+fan(n-1)=n+(n-1)+fab(n-2)
def fab(n):
if n==1:
return 1
return n+fab(n-1) #利用递归的特性来实现求和
print(fab(5)) #15
递归函数如果没有退出条件,就会一直调用下去直至内存的极限。所以,应当在函数内部设置好递归退出条件,控制递归的深度。在上面
10、匿名函数
函数名本身也是一个变量名,它指向的是内存中存储的函数代码块。函数名也可以赋值给其他变量,相当于给函数起了一个别名。
def say_hello():
print('Hello_World!')
hi=say_hello
hi() #Hello_World!
在Python中,除了使用关键字def
来定义一个函数外,还可以使用lambda
表达式来定义一个函数,我们称之为匿名函数。
lambda a,b:a+b #传入参数a和b,返回结果a+b
lambda x:x*x*x #传入参数x,返回x的3次方
lambda
函数本身没有名字,因此使用时必须依靠其他方式来调用。lambda
函数适合用在替换程序中定义的那些简单的并且使用次数很少的函数,能够提高代码的可阅读性。
#第一种调用方式,把函数本身赋值给一个变量
square=lambda x:x*x
square(3) #9
#第二种方式,把lambda函数当做参数传给另一个函数使用,这种用法较多
#现在要实现一个功能,通过调用一个计算函数calc(),能够进行加减乘除等运算
#正常的写法:
def add(a,b): #定义一个加法
return a+b
def get_sub(a,b): #定义一个减法
return a-b
def calc(a,b,fn): #定义一个计算函数,第三个参数是计算方式
result=fn(a,b) #通过函数名调用对应的函数
return result
print(calc(3,5,add)) #8
print(calc(15,10,get_sub)) #5
#简化写法,上面的数学计算函数其实很简单,其实在calc()函数中调用加减乘除的函数并不多
#因此,可以用lambda函数来进行替换
def calc(a,b,fn):
result=fn(a,b)
return result
print(calc(3,5,lambda x,y:x+y)) #8 利用lambda匿名函数来简化代码
print(calc(15,10,lambda x,y:x-y)) #5
#乘除法也是同样的道理,合理的利用匿名函数能够简化代码
11、sort()方法的拓展使用-高阶函数
在列表的方法中,通过sort()
方法能够对列表的元素进行排序。但如果遇到一个复杂的列表,就需要对sort()
方法进行拓展
L=[
{'name':'zhangsan','age':23,'score':98},
{'name':'lisi','age':25,'score':95},
{'name':'jack','age':21,'score':90},
{'name':'rick','age':27,'score':93},
{'name':'jerry','age':26,'score':100}
]
#对上述的列表直接使用sort方法进行排序就会报错,现在要求按照age或者score进行排序
L.sort()
#TypeError: '<' not supported between instances of 'dict' and 'dict'
sort(key=None, reverse=False)
sort
方法实际需要2个参数,key
是一个需要传入函数的参数,reverse
是顺序的控制。我们可以先给key
传入一个任意函数测试。
def test(x):
#如果不传入参数会报错,因为sort方法调用这个函数时会传入一个参数,因此在函数定义时传入一个参数
print(x)
#{'name': 'zhangsan', 'age': 23, 'score': 98}
#{'name': 'lisi', 'age': 25, 'score': 95}
#{'name': 'jack', 'age': 21, 'score': 90}
#{'name': 'rick', 'age': 27, 'score': 93}
#{'name': 'jerry', 'age': 26, 'score': 100}
#可以发现传入x的数据就是列表的每个元素
return x
L.sort(key=test)
#TypeError: '<' not supported between instances of 'dict' and 'dict'
在自定义函数test()
中,传入参数x,返回x会导致报错,并且看起来和没有传入函数的参数前是一样的。因此可以判断传入key
中的test()
就是一个对列表中元素进行处理的函数,现在根据报错代码,我们需要让拓展函数test()
能够返回一个支持比较运算的结果。
#再次对test函数进行修改
def test(x):
return x['age'] #利用字典的取值操作,返回年龄的数据
L.sort(key=test) #根据年龄对列表L进行排序
print(L)
#[{'name': 'jack', 'age': 21, 'score': 90},
#{'name': 'zhangsan', 'age': 23, 'score': 98},
#{'name': 'lisi', 'age': 25, 'score': 95},
#{'name': 'jerry', 'age': 26, 'score': 100},
#{'name': 'rick', 'age': 27, 'score': 93}]
#上面的test()算法简单,且用途较少,因此可以使用匿名函数来替换
#用匿名函数来实现根据score排序
L.sort(key=lambda x:x['score'])
print(L)
#[{'name': 'jack', 'age': 21, 'score': 90},
#{'name': 'rick', 'age': 27, 'score': 93},
#{'name': 'lisi', 'age': 25, 'score': 95},
#{'name': 'zhangsan', 'age': 23, 'score': 98},
#{'name': 'jerry', 'age': 26, 'score': 100}]
其实Python中的内置函数sorted()
的用法与sort()
方法差不多,也是可以通过传入函数来改变比较的数据类型。
12、高阶函数
在上面讲到了把一个函数作为另一个函数的参数来使用,我们称之为高阶函数。高阶函数有以下几种:
- 把另一个函数作为参数使用的函数
- 把另一个函数作为返回值的函数
- 在内部定义其他函数的函数,也称为函数的嵌套
#在内部定义其他函数的函数
def outer():
def inner():
print('我是inner函数')
print('我是outer函数')
outer() #我是outer函数
#这个高阶函数在调用时不会执行inner()函数
inner() #报错
#inner()函数在外部不能直接调用,因为它是在函数内部被定义的,要调用它,必须参照下列方法
def outer():
def inner():
print('我是inner函数')
print('我是outer函数')
return inner #返回inner函数名
outer()() #我是outer函数
#我是inner函数
#out()函数调用后返回inner函数名,搭配括号就可以间接访问inner()函数
下面再来介绍几个常用的高阶函数。
map()
函数
map(func,iterable)
函数需要传入2个参数,第一个是一个函数,第二个需要传入一个可迭代对象。它的作用是把可迭代对象的每一个元素传入函数中,并将函数的结果返回生成一个新的序列,这个序列是一个map类型的对象,也是一个可迭代序列,一般使用list()
来转换。
注意:其实
map()
函数返回的是一个惰性序列,也叫iterator
,要强迫一个惰性序列完成计算,一般使用list()
来获得结果并返回。
L=[1,3,5,7,9]
m=map(lambda x:x*x,L) #使用匿名函数来简化代码
print(m) #<map object at 0x00000233FDC88448>
print(list(m)) #[1, 9, 25, 49, 81]
filter()
函数
filter(func,iterable)
与map()
函数类似,也是接受一个函数和一个可迭代序列作为参数,它的作用是根据函数的返回值是True或False来决定传入函数的元素是保留还是丢弃,并返回一个惰性序列,也就是实现筛选功能。
L=[1,2,3,4,5,6,7,8,9,10] #筛出其中的基数,保留偶数
f=filter(lambda x:x%2==0,L)
print(f) #<filter object at 0x00000233FDC88508>
print(L) #[1,2,3,4,5,6,7,8,9,10] L的值没有改变
print(list(f)) #[2, 4, 6, 8, 10]
reduce()
函数
reduce()
函数与上面的类似,它是把一个函数作用在一个序列上,这个函数必须接受2个参数(一般是序列的前2个数据),reduce()
函数把这个函数的结果和下一个数据继续传入函数中,以此往复,最终返回函数的运算结果。
注意:reduce()函数是在functools模块中,使用前要先进行调用
from functools import reduce
L=[50,40,30,20,10]
m=reduce(lambda x,y:x+y,L) #对列表L求和
print(m) #150
print(reduce(lambda x,y:x-y,L)) #-50 L列表所有元素求差
在使用reduce()函数时,要注意作为参数的函数在接收2个参数时,运算参数类型要保持一致。
from functools import reduce
L=[{'name': 'jack', 'age': 21, 'score': 90},
{'name': 'zhangsan', 'age': 23, 'score': 98},
{'name': 'lisi', 'age': 25, 'score': 95}]
#要求用reduce()函数求上面列表中的总分数
#常规写法
print(reduce(lambda x,y:x['score']+y['score'],L))
#报错 TypeError: 'int' object is not subscriptable
#传入第一次数据时,参数x,y接收到的是一个字典,lambda能够正常计算,当把前2个元素的和再次传入参数x时,这时x接收到的是一个整数,整数不能使用关键字查找元素,参数x第一次和第二次接收到的数据类型不一致
#正确写法
print(reduce(lambda x,y:x+y['score'],L,0)) #283
#给reduce函数增加一个参数,默认是参数x的初始值,每次运算x就传入该初始值
13、函数的嵌套和闭包
- 函数的嵌套
在上面的高阶函数中,有一种情况是在函数内部定义函数,我们称之为函数的嵌套。
def outer(x):
def inner():
print('inner函数被调用了')
if x>0:
inner() #设置inner函数调用的条件
print('outer函数被调用了')
outer(10) #inner函数被调用了
#outer函数被调用了
通过上面的方式可以发现,要想调用内部函数,一是可以通过在函数内部直接调用,二是通过间接调用。
在内部函数中,要想修改外部作用域的变量,需要使用关键字nonlocal
,用法与关键字golbal
一样
def outer(x):
m=10
def inner():
nonlocal m
y=x+m
m=20
- 闭包
在嵌套函数中,有一种比较特殊的函数,如下:
def outer(x):
m=10
def inner():
return x+m
return inner
print(outer(10)) #<function outer.<locals>.inner at 0x00000000027BC0D0>
#out(10)返回的是一个函数,没有具体的值,要得出结果,需要对函数再次调用
f=outer(10)
print(f()) #20 利用函数的别名来实现函数的调用,等同于out(10)()
在上面的函数中,在外部函数outer中定义了一个函数inner,同时inner可以引用外部作用域(非全局作用域,即外部函数作用域)的参数和变量,并且在outer函数中返回inner函数,相关的参数和变量都保存在返回的inner函数中,这种情况我们称之为闭包。对于上面的代码,inner函数就实现了闭包的功能,它是outer的内定义函数,同时引用了外部作用于的参数x和变量m,然后outer函数返回inner函数时,把inner函数对参数x和变量m的处理结果保存了起来,直到调用inner函数时才会得出结果。
需要注意的是,每次调用outer函数返回的都是不同的函数,哪怕参数是一样的。
a=outer(10)
b=outer(10)
print(a==b) #False
14、装饰器
14.1、计算代码的执行时长
要计算一段代码的执行时长,就必须清楚UTC—世界协调时的概念,它是全世界通用的统一计时方式,中国在UTC计时方式中处于东8区,所以一般写为UTC+8。比如现在是北京时间13:00PM,那么UTC标准时间就是5:00AM。
在Python中,要获取当前的时间戳,要通过调用time模块的time
方法,通过time方法,能够获得一个自1970年1月1日00:00到当前时间的秒数,注意这个时间是UTC时间,要处理北京时间,需要提前转换。
import time
print(time.time()) #1617762425.2072818
#获取到的是当前的时间戳,单位是秒,UTC
要计算代码的执行时长,可以在代码开始执行前获取一个时间戳,代码执行结束后获取一个时间戳,两者相减就是代码的执行时长。注意一点,由于cpu以及内存的影响,每次代码执行的时间其实是不一样的。
import time
start=time.time()
sum=0
for i in range(1,50000000):
sum +=i
print(sum)
end=time.time()
print('这段代码执行时间为{}'.format(end-start)) #这段代码执行时间为5.209848403930664
14.2、装饰器的介绍
在上面的代码中,实现了计算代码的运行时长。但在实际的开发中,可能会有很多代码都需要计算运行时间,这时候用来获得时间的代码就需要反复的被书写。根据前面函数的使用场景,凡是使用频次高,变化极小的代码都可以封装成函数,因此,我们把统计时间的代码封装成一个函数。
def get_time():
start=time.time()
end=time.time()
print('这段代码执行时间为{}'.format(end-start))
仔细观察上面的代码,又发现一个问题,获得时间戳的函数是有了,但是又该怎么来使用这个函数呢?要想获得代码执行的时长,就必须把代码插进start
和end
中间去执行才能获得正确的执行时间。那要怎么样才能让代码执行时自动插进中间,获取执行时长呢,同时还要保证代码的正确运行。这里就需要使用装饰器来实现了。
先来看一个装饰器的示例:
#装饰器标准语法
def get_time(fn):
print(fn) #<function clac at 0x00000233FDC80E58>
print('get_time函数被执行')
def inner(*args,**kwargs):
start=time.time()
fn(*args,**kwargs)
end=time.time()
print('这段代码执行时间为{}'.format(end-start))
return inner
@get_time
def clac(*args,**kwargs):
sum=0
for i in range(1,50000000):
sum +=i
print(sum)
clac() #get_time函数被执行
#1249999975000000
#这段代码执行时间为2.9709460735321045
在上面的示例中,把获取时间的代码放到了一个内部函数中,外部函数的参数被内部函数使用,同时返回内部函数,整个过程实际上就是一个闭包。
然后在中间,有一段代码@get_time
,我们称之为装饰器,用法是@+函数名,注意函数名对应的函数必须实现闭包功能。@也被称为装饰器的语法糖
在Python内部,@get_time
执行时,实际上实现了3个功能。第一步是调用get_time
函数,第二步是把装饰器下方的函数clac
当做参数传入get_time
函数中。第三步是把clac
函数替换为内部函数inner
或者说是把clac
函数的指向更改为指向内部函数inner
。执行clac()
函数调用时实际上是调用inner
函数。这三步实际上在clac
函数调用前就已经完成了。
在clac
代码中打印函数本体,结果发现实际调用的是inner
函数。在上述的装饰器使用中,调用clac
实际上就是调用inner
函数,然后在inner
函数中再根据参数fn
来调用clac
函数。inner
、clac
、fn
之间传入的参数必须保持一致,否则会报错。
14.3、装饰器的应用
使用装饰器时,调用clac
函数时,实际上是调用的inner
函数。假如clac本来有一个返回值,就会出现一个问题:inner函数本身没有返回值。因此一般而言,需要在inner函数中返回clac函数的结果。因此,一个装饰器(decorator)用法应当是下面这样的:
def decorator(func):
def wrapper(*args,**kwargs):
pass
return func(*args,**kwargs)
#在内部包装器(wrapper)-内部函数中返回func(),即返回func函数的结果
return wrapper
@decorator
def func(*args,**kwargs):
pass
写了那么多代码来实现装饰器的功能,那装饰器究竟有什么用呢?
装饰器主要的作用就是用来拓展一个函数的功能。比如在开发中,我们写好了N个函数实现不同的功能,现在需要实现一个功能,在调用这些不同的函数时,打印函数的调用日志。
import time
def add():
print('{}函数正在执行'.format(add.__name__),time.asctime(time.localtime(time.time())))
#函数有一个__name__属性,可以获得函数的名字
pass
add() #add函数正在执行 Wed Apr 7 13:29:54 2021
对于一个函数这么修改没有问题,但假如一共有几百个函数,都需要增加这样的代码,工作量也未免太大。这时候就可以使用装饰器,为这些函数装饰一个可以打印调用日志的功能,减少开发人员的工作量。
import time
def log(func):
def wrapper(*args,**kwargs):
print('{}函数正在执行'.format(func.__name__),time.asctime(time.localtime(time.time())))
return func(*args,**kwargs)
return wrapper
@log
def add():
pass
@log
def sub():
pass
add() #add函数正在执行 Wed Apr 7 13:50:52 2021
sub() #sub函数正在执行 Wed Apr 7 13:50:52 2021
这样无论有多少个函数都可以通过装饰器来快速的增加打印调用日志的功能。以上的代码其实有bug存在,我们第一次调用add函数时,函数名称就是'add'
,但经过装饰器修饰后,add
函数的名称发生了改变,因为这个变量名'add'
的指向已经变成了wrapper
函数的内存地址。
import time
def log(func):
def wrapper(*args,**kwargs):
print('{}函数正在执行'.format(func.__name__),time.asctime(time.localtime(time.time())))
return func(*args,**kwargs)
return wrapper
@log
def add():
pass
add()
print(add.__name__) #wrapper 函数名已经变成了wrapper
函数名改变会有什么问题呢?有些代码要借用函数的签名来执行,一旦函数名改变,这些代码可能就会报错。这是就需要用到functools.wraps
方法,他可以保证在装饰后函数的属性不会发生变化。所以完整的装饰器定义如下:
import functools
def decorator(func):
@functools.wraps(func) #把func函数__name__等属性拷贝一份到wrapper函数中
def wrapper(*args,**kwargs):
pass
return func(*args,**kwargs)
return wrapper
@decorator
def func(*args,**kwargs):
pass
总结:装饰器(decorator)可以增强函数的功能,虽然定义起来比较复杂,但使用起来很方便。
14.4、带参数传入的装饰器
假如装饰器本身需要传入参数,就需要编写一个返回装饰器的高阶函数。例如,定制一个装饰器,根据函数的功能,打印不同的调用日志。
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args,**kwargs):
print('{} is {}'.format(func.__name__,text))
return func(*args,**kwargs)
return wrapper
return decorator
@log('start') #给装饰器本身传入参数
def now():
pass
now() #now is start
@log('end')
def to_y():
pass
to_y() #to_y is end
最后
以上就是娇气泥猴桃为你收集整理的Python四Python的全部内容,希望文章能够帮你解决Python四Python所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复