第九章:函数
什么是函数
函数相当于具备某一功能的工具
函数的使用必须遵循:
先定义
后使用
为什么要用函数
-
1、代码的组织结构不清晰,可读性差
2、代码冗余
3、代码可维护性、扩展性差
函数的使用
- 必须遵循先定义后使用的原则
1、函数的基本使用
定义函数时:
- 1、申请内存空间保存函数体代码。
- 2、将函数体申请内存空间的内存地址绑定给func函数名。
- 3、定义函数时不会执行函数体代码,但是会检测函数体的语法。
调用函数时:
- 1、通过func函数名找到函数的内存地址。
- 2、函数名加括号就是触发函数体代码的执行。
def func(): # func = 函数的内存地址
print('来自func函数')
func() # 函数func内存地址加括号就是在调用函数func
def foo():
print('来自foo函数')
func() # 在函数foo中调用func函数
foo()
1、函数定义语法
一个完整的函数定义语法如下:
def 函数名([参数1,参数2...]):
'''函数功能说明''' # 简单的函数可以不用写明
函数体
return 值 # 默认为None
函数名() # 调用函数
2、函数参数的基本使用
函数的参数就相当于函数这个工厂加工一件物品所需要的原材料。
- 在函数定义时,函数名后小括号内的元素,我们统称为参数。通常也被称为形参。
- 同样,在函数调用时,小括号内的元素,也统称为参数。通常被称为实参。
具体介绍如下:
定义函数
1、无参函数
定义函数时小括号内没有参数
def func(): # 定义函数时小括号内没有参数
pass
2、有参函数
定义函数时,函数名后小括号内有参数
def func(a,b):
print(a,b)
func(1,2) # 位置参数形式,调用函数时,必须为函数传值,传值数量必须与定义时一致。
3、空函数
函数体的代码为pass或…
通常应用于函数构思时候暂时占位使用。这里函数有无参数都称为空函数。
def func():
# pass
...
调用函数
调用函数就相当于给函数这个工厂传入原材料。
- 函数调用的本质:就是将值的内存地址传递给函数的形参。所以调用时传入的实参,可以是任意形式,因为它的本质就是内存地址的传递,比如:变量形式、函数对象形式。
1、语句形式
只加括号调用函数
def func():
print('来自func函数')
func()
def add(a, b):
result = a + b
print(result)
add(1, 2)
2、表达式形式
def add(a, b):
result = a + b
print(result)
add(1, 2 * 10)
3、返回值当做参数调用
def add(a, b):
result = a + b
return result
res = add(add(1, 2), 7)
print(res)
函数的返回值
返回值相当于函数这个工厂加工后得到成品。
-
1、函数体内没有return关键字,返回None
-
2、函数只返回一个值:return 值
3、函数返回多个值,用逗号分隔开多个值。会被return以元组形式返回
return关键字是函数 结束的标志,函数体代码一旦运行到return就立即终止函数运行(不管后面有多少代码),并且会将return 后的值当做本次运行的结果返回。
def func(a, b):
result = a * b
return result # 函数只返回一个值
print(a + b) # 这段代码不会运行
res = func(2, 7)
print(res)
# 若代码中有多个返回值时,用逗号分隔开多个值,会被return以元组形式返回
def func():
return 2, 7, [2, 7] # 这里不需要用其他符号来将众多返回值揽括
res = func()
print(res,type(res)) # (2, 7, [2, 7]) <class 'tuple'>
2、函数参数的使用
1、形参与实参
形参
在定义函数阶段,函数名后面的,小括号内的参数被称为形式参数,简称为形参。相当于变量名。
def func(a, b): # 形参a、b,用来接收函数调用时传入的值。
...
实参
在函数调用阶段,函数名后小括号内,传入的值被称为实际参数,简称为实参。相当于变量值。
def func(a, b): # a = 2, b = 7
...
func(2,7) # 实参2, 7
实参传值的三种形式:
- 1、直接以值的形式传值
- 2、以变量形式为其传值
- 3、用函数值为函数传值
def func(a, b):
...
def foo(c, d):
result = c + d
return result
# 1、直接以值的形式传值
func(2, 7)
# 2、实参为形参传值,实际上就是将值的内存地址绑定给形参,所以也可以用变量的形式为其传值。
x = 2
y = 7
func(x, y) # 相当于定义变量时 x = 2;a = x。 y = 7;b = y。
# 3、用函数值为函数传值
func(foo(2,7),7) # 类似于:func(int('2'),7)
形参与实参的关系
-
1、只用在调用阶段才会产生联系。在调用阶段,实参(变量值)会绑定给形参(变量名)。
-
2、这种绑定关系只能是在函数体内使用。
-
3、实参与形参的绑定关系只有在函数调用时生效,函数调用结束后就会解除绑定关系。
2、形参与实参具体使用
1、位置参数
按照从左往右的顺序依次定义的参数称为位置参数。
-
位置形参 :在函数定义阶段,按照从左往右的顺序直接定义的“变量名”。
-
位置实参 :在函数调用阶段,按照从左到右的顺序依次传入的“值”。
-
特点 :函数调用时必须传值,多一个少一个都不行;且按照顺序与形参一一对应。
def func(a,b):
print(a, b)
func(2, 7)
2、关键字参数
-
也称为关键字实参 :在函数调用阶段,按照key=value的形式传入的“值”。
特点:指名道姓的给形参传值,可以不考虑传入的顺序。
def func(a, b):
print(a, b)
func(b=7, a=2)
func(2, 7)
混合使用
- 1、位置实参必须放在关键字实参前
def func(a, b):
print(a, b)
func(2, b=7)
func(b=7, 2) # 语法错误:SyntaxError
- 2、不能为同一个形参重复传值
def func(a, b):
print(a, b)
func(2, 7, b=9) # 类型错误:TypeError
3、默认参数
-
在定义函数阶段,已经被赋值的形参,称为默认参数。也叫默认形参。
特点:在定义阶段就已经被赋值了,也就是说在调用阶段可以不用为其赋值。通常可以用于多个重复传入的值,为其设为默认值,需要时也可以传为需要的值(如:程序猿大多是boy)。
def func(a, b=7):
print(a, b)
func(2)
func(2, 9) # b=7是默认参数,重新为其赋值为9
位置形参与默认形参混用
- 1、位置形参必须在默认形参左边。
- 2、默认参数的值是在函数定义阶段已经被赋值的,也就是被赋予的是值的内存地址。
y = 7
def func(a, b=y): # b->>> 值7的内存地址
print(a, b)
y = 9 # 这里是为全局变量y重新赋值为9
func(2) # 调用func函数,b所绑定的值的内存地址是7的内存地址
# 为形参绑定一个可变容器
# 下述代码会受外界影响,通常不推荐使用
y = []
def func(a, b=y): # b->>> []空列表所指向的内存地址
print(a, b)
y.append('7')
# 这里虽然为列表里面追加了值,但是变量y所指向的内存地址没有改变,
# 形参b所指向的内存地址也没有发生改变。
func(2) # 2 ['7']
3、虽然默认值可以被指定为任意数据类型,但是不推荐使用 可变类型 。
函数最理想的状态:函数的调用只跟函数本身有关系,不受外界代码的影响。使用一个函数通常能够预知得到的结果。
# 相对上一段代码来说,下述代码不受外界代码影响,通常不得已情况下推荐使用
def func(a, b, l=None):
if l is None: # 用is而不是== ,这样使得我们更专业
l = []
l.append(a)
l.append(b)
print(a, b, l)
func(2,7) # 2 7 [2, 7]
4、可变长度的参数(*、**)
- 可变长度参数:指在使用函数时,传入实参(值)的个数不固定。而实参是用来为形参赋值的,对应着,溢出的实参必须有对应的形参来接收。
1、可变长度的位置参数
-
形参中带 *
-
1、* 形参名:用来接收溢出的位置实参,溢出的位置实参会被 * 封装成元组的格式然后赋值给紧跟其后的形参名。 * 后面跟任意形参名字都可以,约定成俗的是args。
def func(a, b, *args): # *将7和9保存成元组的格式赋值给args
print(a, b, args)
func(2, 7, 9, 11) # 2 7 (9, 11)
- 实参中带 *
- 2、* 可以用在实参中,实参中带星,星先将值打散为位置实参,再以位置实参格式按照位置关系赋值给对应的形参。
l = [2, 7]
def func(a, b, c, d):
print(a, b, c, d)
func(2, 7,*l) # 2 7 2 7
- 形参和实参带 *
- 3、调用阶段先将容器中的元素打散,进入定义阶段再将其多余的元素封装为元组赋值给星后面的形参名 args。
def func(a, b, *args): # 按照位置:a=2,b=9;*将多余的元素封装为元组args=(9,11)
print(a, b, args)
func(*[2, 7, 9, 11]) # *先将列表中内存打散为2,7,9,11 四个元素
2、可变长度的关键字参数
-
1、 ** 形参名:用来接收溢出的关键字实参,** 会将溢出的关键字实参封装成字典的格式,然后赋值给紧随其后的形参名。
** 后跟的也可以是任意的名字,但是约定成俗为 **kwargs
def func(a, b, **kwargs):
print(a, b, kwargs)
func(b=7, a=2, c=9, d=11)
# 2 7 {'c': 9, 'd': 11}
- 2、** 可以用在实参中,但是** 后面只能跟字典,实参中的星星会将字典打散成关键字实参。然后 按照关键字实参传值的方式为形参传值。按照传值的约定实参(值)与形参(变量)的关系必须一一对应。
def func(a, b, c):
print(a, b, c)
dic = {'b':7,'a':2,'c':11}
func(**dic)
-
3、形参与实参中都带 **
同理:函数调用时,先将字典中的元素打散为关键字实参形式,按照关键字传参原则,多余的关键字实参会被形参中的星星封装成字典的形式赋值给kwargs。
def func(a, **kwargs):
print(a, kwargs)
dic = {'a': 2, 'b': 7, 'c': 11}
func(**dic) # 2 {'b': 7, 'c': 11}
3、可变长度位置参数与可变长度关键字参数混用
形参:
* args必须在 **kwargs之前
实参:必须满足星在星星之前。
def func(a, *args, **kwargs):
print(a, args, kwargs)
l = [7, 9]
dic = {'d': 11, 'e': 13}
func(2, *l, **dic)
# 调用阶段*和**先分将列表l和字典dic中的元素打散为位置参数和关键字参数,
# 即func(2, 7, 9, d=11, e=13),然后将其以位置参数关键字参数的形式为形参传值。
5、命名关键字参数
-
在定义函数时,* 后除紧挨形参外的参数
特点:命名关键字参数的 形参必须按照key=value的形式为其传值。可以为命名关键字参数定义默认值。
def func(a, b, *, c=9, d): # c、d就是命名关键字参数
print(a, b)
print(c, d)
func(2, 7, c=11, d=13)
6、组合使用
- 1、形参混合使用顺序:位置形参、默认形参、*args、命名关键字参数、 **kwargs。
def func(a, b=7,*args,d,**kwargs):
...
- 2、实参混合使用的顺序:保证其打散后遵循位置实参在关键字实参 左边 原则。
即:位置实参、*、( ** 和关键字实参无顺序要求)。
一般情况不会有这种混合使用的情形
3、名称空间与作用域
名称空间namespace:存放名字的地方,实际上是对栈区名称类型的一个归类。
- 特点:有了名称空间之后,就可以在同一个栈区中存放相同的名字。
1、名称空间
1、三种名称空间
1、内置名称空间
-
存放的是pyhton解释器的内置名字,也就是python的关键字及内置的一些函数名字。
存活周期:python解释器启动则产生,python解释器关闭则销毁。
2、全局名称空间
-
只要不是函数内定义的、也不是python解释器内置的,剩下的都是全局名称空间的名字。
存活周期:python文件执行则产生(软件运行的第三个阶段),python文件运行完毕后销毁。
1、需要声明使用的一些模块如,import os
2、def func(): # 函数func也是属于全局名称
...
3、class Foo: # 类名class也是属于全局名称
...
4、if 判断内定义的一些变量等
3、局部名称空间
-
在函数调用时,运行函数体代码过程中产生的函数内的名字。
存活周期:在函数调用时存活,函数调用完毕后就销毁。
def func():
x = 11
func() # 调用完变量x销毁
2、名称空间的加载顺序
内置名称空间 > 全局名称空间 > 局部名称空间
3、销毁顺序
局部名称空间 > 全局名称空间 > 内置名称空间
4、名字的查找优先级
- 1、从当前所在的位置开始,再向上一层一层查找。
即:局部名称空间 > 全局名称空间 > 内置名称空间
x = 1
def func():
x = 2
def foo():
x = 3
print(x)
foo()
func() # 运行结果是3
- 2、名称空间的"嵌套"关系是以 函数定义阶段为准,与调用位置无关。
- 名字的查找只有优先级之分,本身并无嵌套关系
# ex1:
def func():
print(x)
x = 1
func() # 1
# ex2: # 函数嵌套调用
x = 1
def func():
print(x)
def foo():
x = 2
func()
foo() # 1
# ex3:函数嵌套定义
x = 1
def func():
def foo():
# x = 3
print(x)
x = 2
foo()
func() # 2
'''
ex4:查找变量名时是先在本地查找,本地里面有变量x,但是它没有遵循先定义后引用的原则。
由此可以知道,查找变量名的时候并不是在代码头顶上面查找,
而是由内而外的进行’全局‘查找。名称空间的查找关系是以函数的定义阶段为标准的。
'''
x = 1
def func():
print(x)
x = 2 # UnboundLocalError: local variable 'x' referenced before assignment
# func()
5、总结
-
名称空间的查找顺序,是以当前所在位置向外查找;
-
名称空间之间的查找,只有优先级之分,本身并无嵌套关系;
-
函数的使用分为:定义、调用两个阶段。
1、名称空间的嵌套关系决定了名字的查找顺序;
2、而名称空间的嵌套关系是以函数定义阶段为准的;
3、即函数的嵌套关系与名字的查找顺序是在定义阶段就已经确定好的。
2、作用域
1、全局作用域:
-
包含:内置名称空间、全局名称空间
特点:全局存活、全局有效(被所有函数共享)。
2、局部作用域:
-
包含:局部名称空间的名字。
特点:临时存活、局部有效(函数内有效)。
作用域遵守LEGB原则
L:local(函数内部)
E:enclosing(嵌套函数的外出函数内部)
G:global(模板全局)
B:built-in(内建)
3、global和nonlocal
x = 1 # 定义全局变量x
def func():
x = 2 # 定义局部变量x
func()
print(x) # 1
# 由于名称空间的存在使得两个x的名字虽然相同,但是它们互不影响。
- 如果在局部内想要修改全局变量的值(不可变类型),需要使用global关键字声明。
x = 1
def func():
global x # 声明x是全局的名字,不用再造新名字了
x = 2
func()
print(x) # 2
- 对于可变类型,因为其指向的内存地址是不变的局部可以直接改变全局的元素。
l = []
def func():
l.append('2')
func()
print(l) # ['2']
def foo():
l = 1 # 这种是将列表重新赋值,而不是对它进行修改
- nonlocal修改函数外层函数包含的名字对应的值(不可变类型),对于可变类型与上述类似。
x = 1
def func():
x = 2
def foo():
nonlocal x
x = 3
foo() # 必须要调用函数运行代码才能完成修改
print('func函数里面的值',x)
4、函数对像
1、函数对象的本质:将函数当成变量去用
- 在名称空间中,函数名就是变量名(属于全局变量)。
- 定义函数就是将函数体的内存地址赋值给函数名。相应的函数的内存地址也可以赋值给其他变量名使用。
def func():
print('函数func')
# f = func() # 这是将函数的返回值赋值给了变量f
f = func # 将函数func的内存地址赋值给变量f
print(f,func)
# 可以知道变量f和函数func指向的内存地址一样,实际上它们就是同一个函数
f() # 函数func的内存地址加括号,即调用函数func
2、将函数当做参数传给函数
def func():
print('函数func')
def foo(x): # 2、x=func的内存地址
x() # 3、相当于func()
foo(func) # 1、(func的内存地址)
3、将函数作为另一个函数的返回值
def func():
print('函数func')
def foo(x): # 2、x = func的内存地址
return x # 3、return func的内存地址
result = foo(func) # 1、将func的内存地址传入foo,result接收返回值func的内存地址(result此时就相当于func函数)
result() # 4、相当于func()
4、将函数做为容器类型的一个元素
def func():
...
l = [func]
l[0]() # 调用函数
dic = {'k':func}
dic['k']() # 调用func函数
具体应用
# ATM 功能函数
def longin():
print('登录功能')
def transfer():
print('转账功能')
def check_banlance():
print('查询余额')
def withdraw():
print('提现')
def register():
print('注册')
while True: # 循环提示和输入操作的指令
print('''
0 退出
1 登录
2 转账
3 查询余额
4 提现
5 注册
''')
choice = input('输入命令:').strip() # 除去可能误输入的空格
if not choice.isdigit():
print('必须输入对应的数字编号!!!')
continue
if choice == '0':
break # 直接退出程序
if choice == '1':
longin()
elif choice == '2':
transfer()
elif choice == '3':
check_banlance()
elif choice == '4':
withdraw()
elif choice == '5':
register()
else:
print('输入的指令不存在!!!')
# 弊端:需要修改功能时修改的地方过多
- 因为函数对象的存在,函数可以作为容器类型的元素。上述代码可以改进为:
- 那么后面的if 判断,可以简化为,判断其是否在字典中,如果在,则可以取出字典中对应的功能,再进行调用即可。
- 这时需要添加新功能时,必须要增加相应的功能函数、提示信息
# ATM 功能函数
def longin():
print('登录功能')
def transfer():
print('转账功能')
def check_banlance():
print('查询余额')
def withdraw():
print('提现')
def register():
print('注册')
func_dict = {
'1':longin,
'2':transfer,
'3':check_banlance,
'4':withdraw,
'5':register
}
while True: # 循环提示和输入操作的指令
print('''
0 退出
1 登录
2 转账
3 查询余额
4 提现
5 注册
''')
choice = input('输入命令:').strip() # 除去可能误输入的空格
if not choice.isdigit():
print('必须输入对应的数字编号!!!')
continue
if choice == '0':
break # 直接退出程序
if choice in func_dict:
func_dict[choice]() # 调用命令的函数功能块
else:
print('输入的指令不存在!!!')
- 继续改进:需要修改这段代码时,只需要修改函数功能和字典中的元素
# ATM 功能函数
def longin():
print('登录功能')
def transfer():
print('转账功能')
def check_banlance():
print('查询余额')
def withdraw():
print('提现')
def register():
print('注册')
# 格式尽量一致,没有值就用None代替
func_dict = {
'0':('退出',None),
'1':('登录',longin),
'2':('转账',transfer),
'3':('查询余额',check_banlance),
'4':('提现',withdraw),
'5':('注册',register)
}
while True: # 循环提示和输入操作的指令
for i in func_dict:
print(i,func_dict[i][0])
choice = input('输入命令:').strip() # 除去可能误输入的空格
if not choice.isdigit():
print('必须输入对应的数字编号!!!')
continue
if choice == '0':
break # 直接退出程序
if choice in func_dict:
func_dict[choice][1]() # 调用命令的函数功能块
else:
print('输入的指令不存在!!!')
5、函数嵌套
-
1、函数嵌套调用:在调用一个函数的过程中又调用其他函数。
-
应用于:将一个大功能切分为多个小功能组合的形式
这样使得开发思路更加清晰(需要哪个功能就调用哪个功能)
# 比较数字的大小
def max2(x,y):
if x > y:
return x
else:
return y
def max3(a,b,c,d):
# 第一步:比较a, b 得到result
result = max2(a, b)
# 第二步:比较result,c得到result1
result1 = max2(result, c)
# 第三步:比较result1, d得到最终结果res
res = max2(result1, d)
return res
res = max3(1,2,3,4)
print(res)
嵌套定义:闭包与装饰器的本质
-
函数嵌套定义:在函数内定义其他函数。
-
应用于:对于一个大的功能进行封装时会用到。如:定义圆形,可以将圆的所有特征封装到一个函数内。
-
特点:内部的函数它是属于局部的,这样就使得外部访问不到;相当于将外部的函数当作容器来使用。
def func():
def foo():
...
6、闭包函数
-
闭包函数 = 名称空间与作用域 + 函数嵌套 + 函数对象
-
核心:名字的查找关系是以函数定义阶段为准
1、什么是闭包函数
"闭"函数指的是该函数是内嵌函数(函数内定义的函数,即函数嵌套定义,属于内函数)。
"包"函数指的是该函数包含对 外层函数作用域名字的引用(不是对全局作用域)。
1、闭包函数:名称空间与作用域的应用+函数嵌套
- 下面这个函数说明一个核心,名字的查找关系是以函数定义阶段为准。它的包函数始终是函数f1,不管发生生么都不会改变。
def f1():
x = 2
def f2():
print(x) # 这个函数是闭包函数,它被函数f1包含形成闭函数,又对它的外层函数即f1的变量进行了引用。
f2()
x = 1
def func():
x = 3
f1() # 函数的查找关系是以函数定义阶段为准,所以调用f1()它的变量x始终是定义时候的2,与全局变量和其他函数内的局部变量无关。
def foo():
x = 4
func()
foo()
2、闭包函数:名称空间与作用域+函数嵌套+函数对象
- 闭包函数中,如果没有引入函数对象概念时。内函数是不可能被外界访问到的,因为函数对象概念的加入,使得它的调用位置被打破。
def f1():
x = 2
def f2():
print('函数f2:', x)
return f2 # 函数对象中的函数作为另外一个函数的返回值
x = 1
f = f1() # 将函数f2当作返回值形式给了全局变量f,即内函数f2的内存地址赋值给了全局变量f
f() # 相当于:内函数f2的内存地址加括号调用
def foo():
x =3
f()
foo()
2、为什么要有闭包函数
- 作为函数的另一种传参方式
3、两种为函数体传参的方式:
1、直接把函数体需要的参数定义成形参
def func(x):
print(x)
func(2)
2、闭包函数为函数体传参
- 有些情况下,不能直接为函数体传参,这时就需要使用闭包函数为其传参。
def f1(x): # 调用函数时为其传入值的内存地址,需要哪个传哪个。
# 如果直接定义就把变量的值写死了,所以在定义时将其定义在外层函数f1内作为形参。
# x = 3
def f2(): # 函数f2
print(x)
return f2 # 因为f2本身就是属于全局的,为了让函数f2能够在全局中调用
f = f1(3) # 调用函数时为其传入实参,此时是将f2的内存地址赋值给全局变量f
# 虽然现在返回值是函数f2,但它已经不是以前的f2了,现在它属于函数f1的内层函数。
# 理论上函数运行完,函数内的变量就不会再被引用,函数体代码就会被回收。
# 但是,现在将内部函数赋值给外部的变量了,它一直被引用,所以函数体代码不会被回收。
f()
格式:
def 外层函数(变量名):
def 需要传入参数的函数():
引用变量名
...
return 需要传入参数的函数
使用函数对象为返回值赋值为一个变量
7、装饰器
1、什么是装饰器
-
器:指的是工具,可以定义成函数
装饰:指的是为其他事物添加额外的东西点缀
合到一起:装饰器指定义一个函数,专门用来为其他函数添加额外的功能。
-
装饰器不仅仅只能为函数装饰,也可以为其他功能(类)进行装饰。
-
装饰器的定义必须要定义在被装饰对象之前
2、为什么要用装饰器
开放封闭原则
-
开放:指的是对拓展功能是开放的;这里的拓展指的是在不修改源代码的基础上。
封闭:指的是对于修改源代码是封闭的(源代码若无必要不能轻易修改,如:已上线的软件,用户在使用过程中,修改源代码后出现强制下线,会造成不可估计的损失。)
-
装饰器就是在不修改被装饰器对象源代码以及调用方式的前提下为被装饰对象添加新功能。
3、无参装饰器
需求 :为代码添加一个统计运行时间的功能
- 这里需要引入一个模块:时间模块
- import time 导入时间模块,time.time对时间进行统计。
def index(a, b):
print(a + b)
index(2, 7)
- 方案1:直接在定义函数阶段,添加记录时间功能的代码
import time
def index(a, b):
start = time.time()
time.sleep(1) # 使代码暂停一秒
print(a + b)
end = time.time()
print(end - start)
index(2, 7)
# 没有修改被装饰对象的调用方式,但是修改了其源代码
上述方案:虽然没有修改被装饰对象的调用方式,但是修改了源代码。
- 方案2:因为函数体的运行,是函数调用的时候才运行的,所以可以将功能添加到调用函数的地方。
import time
def index(a, b):
print(a + b)
start = time.time()
time.sleep(1)
index(2, 7)
end = time.time()
print(end - start)
上述方案:虽然没有修改被装饰对象的调用方式,也没有修改函数的源代码,并且为它添加了新功能。但是造成了代码冗余(方法虽然是通用的,但是它需要,在所有需要添加该功能的地方,都插入该代码块)。
- 方案3:将需要重复的代码封装成一个函数,需要用的时候,直接调用这个函数即可。
import time
def index(a, b):
print(a + b)
def wrapper():
start = time.time()
time.sleep(1)
index(2, 7)
end = time.time()
print(end - start)
wrapper()
'''
虽然解决了方案2的代码冗余问题,
但是函数的调用方式改变了,并且index函数调用时的参数被写死了,
可以通过闭包为其传值,使得它能传入其他不一样的值。
'''
# 调用阶段
def wrapper(*args, **kwargs):
start = time.time()
time.sleep(1)
index(*args, **kwargs)
end = time.time()
print(end - start)
wrapper(2, 7)
wrapper(3, 5)
'''
改进后,虽然使得函数index能传入自己需要的各种值,但它也仅限于函数index,
不适用于其他函数,所以还有待该进使他能适用其他函数。
即将函数作为参数传入,由于函数wrapper传入的值直接受函数index决定,
所以它里面的形参肯定不能被修改,只有在外层,
继续套一层函数,为需要添加该功能的函数参数传值(传入的值作为函数)。
'''
# 还是调用阶段
def outter(func): # func ->>传入函数的内存地址
# func = index # 直接在小括号内写的变量,就相当于这一步操作
def wrapper(*args, **kwargs):
start = time.time()
time.sleep(1)
func(*args, **kwargs)
end = time.time()
print(end - start)
return wrapper
# 此时的index是全局的变量名,它指向的是函数wrapper的内存地址
# f = outter(index) 与下一行代码意思一样
index = outter(index) # index ->> wrapper的内存地址
index(2, 7) # wrapper(2, 7)
# 如果需要为其他函数添加该功能: 如foo,foo = outter(foo)
'''
上述代码将函数wrapper的内存地址,赋值给全局变量index的目的,
就是为了偷梁换柱,使得wrapper做的跟被装饰对像一模一样,
还需要为其添加原函数index内的返回值以及它的属性。
'''
# 添加返回值
import time
import func
def index(a, b):
print(a + b)
return a
def outter(func): # func ->>传入函数的内存地址
# func = index
def wrapper(*args, **kwargs):
start = time.time()
time.sleep(1)
result = func(*args, **kwargs) # 这里得到的是被装饰函数的返回值
end = time.time()
print(end - start)
return result # 将被装饰函数的返回值用作函数wrapper的返回值
return wrapper
wrapper = outter(index) # index ->> wrapper的内存地址
wrapper(2, 7)
print('index的返回值:', index(2, 7))
print('wrapper的返回值:', wrapper(2, 7))
# 为其添加属性,在总结后面
4、语法糖
- 如上述所示,如果某一个函数需要被装饰,就在它的头上添加(以函数index为例)即可。
index = outter(index)
def index(a, b):
...
index = outter(index)
def foo():
...
- 这样做虽然可以实现,但是给人一种比较复杂的感觉。通常在被装饰对象的头上添加 @装饰器名称
@outter # index = outter(index)
def index(a, b):
...
@outter
def foo():
...
显而易见,被装饰对象也可以有多个装饰器,它装饰的是下面的是上面的被装饰对象。
@f3 # index3 = f3(index2)
@f2 # index2 = f2(index1)
@f1 # index1 = f1(index)
def index(a, b):
...
总结:
@func()的形式,是先运行括号,然后将运行函数的返回值带入@符号中运算
无参装饰器模板
def outter(func):
def warpper(*args, **kwargs):
result = func(*args, **kwargs) # 1、调用原函数
# 2、为其增加新功能
return result # 将func函数的返回值作为函数warpper
return warpper
@wraps为装饰器添加原函数的属性
- 装饰器的偷梁换柱,就是将原函数名指向的内存地址,偷梁换柱成wrapper函数,所以就该把wrapper做的跟原函数一样才行。
from functools import wraps
def outter(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
'''
手动写入原函数的属性,就是原函数中以__开头和结尾的所有方法,
都要添加到函数outter中,函数中属性的获取是不需要运行函数的。
(由于函数wrapper中的函数体必须要在调用时才运行,
所以需要写在与wrapper函数同级)。
'''
def outter(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
# 手动将原函数的属性赋值给wrapper函数
# 1、函数wrapper.__name__ = 原函数.__name__
# 2、函数wrapper.__doc__ = 原函数.__doc__
wrapper.__name__ = func.__name__ # 将函数名覆盖给wrapper
wrapper.__doc__ = func.__doc__ # 将文档说明覆盖给wrapper
偷梁换柱后:
-
1、函数wrapper的参数与index一样。
-
2、函数wrapper的返回值与index一样。
3、函数wrapper具备函数index的所有属性。
-->>from functools import wraps
5、有参装饰器
- 由于语法糖@的限制,outter函数只能有一个参数,并且该函数 只用来接收被装饰对象的内存地址。所以只有再嵌套函数,为里面所需要传值的函数传值。
1、虽然语法糖它不支持再往outter函数内传值,但是我们可以不使用语法糖为需要被装饰的函数传值。
如:传入文件的来源(文件、数据库和ldap)
def outter(func, db_type):
def wrapper(*args, **kwargs): # 这里的参数受到函数func的限制
result = func(*args, **kwargs)
if db_type == 'file':
print('来源于文件的验证!!!')
...
elif db_type == 'mysql':
print('来源于数据库验证!!!')
...
elif db_type == 'ldap':
print('来源于ldap验证!!!')
...
return result
return wrapper
def index(a, b):
print('>>>>', a + b)
# 这样的装饰显得有些繁杂
index = outter(index, 'file')
index(2,7)
index = outter(index, 'mysql')
...
index = outter(index, 'ldap')
...
所以我们能够想到使用闭包函数为里面的函数传值
def auth(db_type):
def outter(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if db_type == 'file':
print('来源于文件的验证!!!')
...
elif db_type == 'mysql':
print('来源于数据库验证!!!')
...
elif db_type == 'ldap':
print('来源于ldap验证!!!')
...
return result
return wrapper
return outter
index = auth(db_type='file') # 这里的index变量绑定的是函数outter的内存地址,类似于无参装饰器
@index
def index(a, b):
...
# @index ==> @auth(db_type='file'),可以简化为
@auth(db_type = 'file')
def index(a,b):
...
- 有参函数中的@符号,优先级小于小括号,所以会先运行函数调用,这里的本质就是函数outter。
- 即@auth(参数) ==> @outter ==> 原函数名=outter(被装饰函数)
有参装饰器模板
- 通常有参装饰器已经足够装饰器的所有应用范围了,因为它外层可以传入多个参数。
from functools import wraps
def auth(a, b, c):
def outter(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
# 添加需要修饰的功能
return result
return wrapper
return outter
@auth(1, 2, 3)
def 被装饰对象():
...