认识闭包
python代码中的变量分为全局变量与局部变量,导入写好的模块后,会有篡改全局变量的可能,如何解决?可以采用闭包的形式,当然闭包的作用不仅限于此,先从下面的代码中,认识一下闭包。
def outer_fun(num1):
temp = 100
def inner_fun(num2):
print(num1*temp+num2)
return inner_fun
def other_fun(x):
return x
fn = outer_fun(1)
print(fn.__closure__) # <cell at 0x000001514C779F00: int object at 0x00007FFF64E139B8>, <cell at 0x000001514C779E10: int object at 0x00007FFF64E14618>
print(other_fun.__closure__) # None
从上方代码可知,Python闭包是一个嵌套函数,这个嵌套函数定义在一个外部函数内部,可以访问外部函数的变量。
闭包一旦创建,外部函数的变量会绑定在闭包上,即使外部函数运行结束后,外部函数的变量依然存在,闭包始终可以访问(甚至修改)。
闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所建立所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。
因此,闭包和普通函数的区别在于,
①闭包捆绑了外部函数的变量
②拓展了外部函数的变量的作用域,将外部函数的变量缓存在内存中,其生存期不再受到外部函数的限制。
闭包的构成条件
通过闭包的定义,我们可以得知闭包的形成条件:
①在函数嵌套(函数里面再定义函数)的前提下
②内部函数使用了外部函数的变量(还包括外部函数的参数)
③外部函数返回了内部函数
闭包的使用
还用上面的代码作为例子
def outer_fun(num1):
temp = 100
def inner_fun(num2):
print(num1*temp+num2)
return inner_fun
def other_fun(x):
return x
fn = outer_fun(1)
fn(1) # 101
fn(100) # 200
闭包可以保存外部函数内的变量,不会随着外部函数调用完而销毁。 注意点: 由于闭包引用了外部函数的变量,则外部函数的变量没有及时释放,消耗内存。
闭包内修改外部变量
来试一下在闭包内直接修改外部变量
def outer_fun(num1):
temp = 100
def inner_fun(num2):
temp += 100
print(num1*temp+num2)
return inner_fun
fn = outer_fun(1)
fn(1)
"""
Traceback (most recent call last):
File "d:\pytest\alient_invasion\re_test.py", line 10, in <module>
fn(1) #
^^^^^
File "d:\pytest\alient_invasion\re_test.py", line 4, in inner_fun
temp += 100
^^^^
UnboundLocalError: cannot access local variable 'temp' where it is not associated with a value
"""
由上可见,行不通会报错,是因为在inner_fun()内部并没有temp这个变量,又无法引用外部函数的变量,怎么解决?
答:使用"nonlocal"关键字进行声明,声明闭包函数中使用的变量为外部函数的变量
声明后再试一下
def outer_fun(num1):
temp = 100
def inner_fun(num2):
nonlocal temp
temp += 100
print(num1*temp+num2)
return inner_fun
fn = outer_fun(1)
fn(1) # 201
装饰器
装饰器就是一个特殊的闭包,把一个函数当作外部函数的参数,在内部函数中使用这个函数。使用装饰器则是使用语法糖@来装饰函数, 其功能就是在不破坏目标函数原有的代码和功能的前提下,为目标函数增加新功能。
如下为一个简单的装饰器例子
def outer_fun(func):
def inner_fun():
print("inner fun")
func()
return inner_fun
@outer_fun
def test_fun():
print('test fun')
test_fun()
"""
输出
inner fun
test fun
"""
可以看到在原test_fun的打印基础上,增加了inner_fun的打印
装饰器的执行顺序
我们为上面的函数添加一些打印信息来看一下
# 1
def outer_fun(func):
print("a")
def inner_fun():
print('b')
func()
print('c')
print('d')
return inner_fun
# 2
@outer_fun
def test_fun():
print('test fun')
# 3
test_fun()
"""
a
d
b
test fun
c
"""
咦?为什么输出不是 a b test fun c d ?
因为在文件执行时,装饰器就开始装饰函数了,装饰的过程已经开始了。所以先执行的外部函数,再执行内部函数。
在"1"处定义了装饰器"outer_fun"
在"2"处定义了"test_fun"并且装饰器开始装饰"test_fun",相当于调用了"outer_fun",把"inner_fun"函数赋给了"test_fun",注意,此时"test_fun"已经不再是原来的函数了。装饰器内层函数之外的代码就开始执行了,所以打印了"a"和"d"
接着执行到"3",执行"test_fun",此时"test_fun"已经装饰器装饰,相当于执行了内部函数"inner_fun",按照内部"inner_fun"函数的执行顺序,依次打印"b",执行传进来的的func即"test_fun",打印"test fun",再打印"c"。
为了验证一下"在文件执行时,装饰器就已经开始装饰函数"这句话,我们只定义装饰器和被装饰函数,不调用被装饰函数试一下,看是否会执行装饰器的外部函数
def outer_fun(func):
print("a")
def inner_fun():
print('b')
func()
print('c')
print('d')
return inner_fun
@outer_fun
def test_fun():
print('test fun')
"""
a
d
"""
上面的代码可知,在没有调用"test_fun"的情况下,依然打印了 a d,确认装饰器已经执行了。
装饰器会修改原函数元信息
装饰器会对原函数的元信息进行更改,比如函数的docstring,__name__
参数列表等。
我们来看一下
def outer1(func):
def inner1():
print("inner1 start!")
func()
return inner1
@outer1
def index():
print("i am index!")
print(f"index name is : {index.__name__}") # index name is : inner1
从上面的测试代码可以看到,当"index"被装饰后,他的''name''参数被修改成了''inner1'',仅仅如此吗,并不是。
此时的“index”并不是原来的“index”了,而是有了新的功能和一个新的地址,也就是这时候的index指向的是“inner1”的地址,所以拥有"inner1"的功能以及名字。
测试一下
def outer1(func):
def inner1():
print(f"inner1 address is : {inner1}")
func()
return inner1
@outer1
def index():
print("i am index!")
index()
print(f"index address is : {index}")
# inner1 address is : <function outer1.<locals>.inner1 at 0x000002749CE19120>
# i am index!
# index address is : <function outer1.<locals>.inner1 at 0x000002749CE19120>
可以看到,调用了''index'',但是输出的是''inner1"的地址和 "i am index!",同时"index"的内存地址与"inner1"的内存地址也是一样的。其实也就是相当于执行了如下代码。
def outer1(func):
def inner1():
print(f"inner1 address is : {inner1}")
func()
return inner1
def index():
print("i am index!")
print(f"index old address is : {index}")
index = outer1(index)
print(f"index new address is : {index}")
index()
"""
输出:
index old address is : <function index at 0x000001FE43DB9080>
index new address is : <function outer1.<locals>.inner1 at 0x000001FE43DB9120>
inner1 address is : <function outer1.<locals>.inner1 at 0x000001FE43DB9120>
i am index!
"""
验证了上面的说法,原"index"的内存地址,被替换为"inner1"的内存地址
所以,结论:
1.语法糖装饰函数,该函数名变量不再指向原内存地址,而是指向一个新的内存地址,即装饰器内层函数的地址。
2.该函数变量的"__name__"属性,也被修改为装饰器内层函数的函数名。
解决修改元信息问题
解决此问题可以使用functools.wraps
对原函数信息修改,格式如下
from functools import wraps
def outer1(func)
@wraps(func)
def inner1()
pass
return inner1
@outer1
def index()
pass
来看段代码验证一下,
from functools import wraps
def outer1(func):
@wraps(func)
def inner1():
func()
return inner1
def index():
print("i am index!")
"""
执行
index = outer1(index)
也就是相当于执行了
@outer1
def index():
pass
"""
print(f"index old name is : {index.__name__}")
index = outer1(index)
print(f"index new name is : {index.__name__}")
index()
"""
index old name is : index
index new name is : index
i am index!
"""
可以看到 index 被装饰前后的名字没有发生改变。但是注意,前后的__name__属性没有改变,但是"index"的内存地址还是会改变。
带参数的装饰器
我们来回忆两个问题:1,什么是装饰器?2,什么是闭包?3,区别在哪?
闭包:是一个嵌套函数,定义在一个外部函数内部,可以使用外部函数传入的参数,重点,传入的参数是变量。
装饰器:也是一个嵌套函数,定义再一个外部函数内部,可以使用外部函数传入的参数,重点,传入的参数是函数
区别:都是一个闭包,区别是传入的参数,一个是变量,一个是函数。
那么思考一个问题,如果我在装饰器内部,既想使用外部函数的参数,又想为一个函数添加功能,怎么办?
咦?好办,在闭包模式下,为外层函数再添加一个参数传入就好了嘛,如下,装饰器外部函数增加了一个参数'flag',供内部函数使用
def outer_fun(func,flag):
def inner_fun():
if flag == 'ok':
print("im ok!")
if flag == 'sick':
print("im sick!")
func()
return inner_fun
@outer_fun('ok')
def test_fun():
print('test fun')
test_fun()
编译一下试试,
Traceback (most recent call last):
File "d:\pytest\alient_invasion\wrapper.py", line 10, in <module>
@outer_fun('ok')
^^^^^^^^^^^^^^^
TypeError: outer_fun() missing 1 required positional argument: 'flag'
额,,,看来是不行,那么应该怎么做?
答:在原有的装饰器函数体外层,再封装一个有参函数,它返回一个装饰器。
也就是,调用了两次装饰器,最外层传入参数,返回一个装饰器,返回的装饰器再去装饰函数。
看一个例子:
def msg_fun(msg):
def outer_fun(func):
def inner_fun(a,b):
result = func(a,b)
return f"msg:{msg},data:{result}"
return inner_fun
return outer_fun
@msg_fun("计算两个数的和")
def test_fun(a,b):
return (a+b)
print(test_fun(1,2)) # msg:计算两个数的和,data:3
同样的,既然是调用两次,那么在装饰函数时(还未对函数调用),会先执行最外层函数,再执行中间层函数。
def msg_fun(msg):
print('1')
def outer_fun(func):
print('3')
def inner_fun(a,b):
result = func(a,b)
return f"msg:{msg},data:{result}"
print('4')
return inner_fun
print('2')
return outer_fun
@msg_fun("计算两个数的和")
def test_fun(a,b):
return (a+b)
"""
输出
1
2
3
4
"""
即有参数也可无参数的装饰器
如何让一个装饰器使用的时候吗,即可以传参,也可以不传参?
如下:
# 不带参数的
@my_decorator
# 带参数的
@my_decorator("param")
可以在有参数的装饰器内进行判断,如果没有传参,进行兼容,代码如下:
def my_decorator(param=None):
# 定义装饰器函数
def decorator(func):
def wrapper(*args, **kwargs):
if param is not None:
print(f"Decorator with parameter {param} is called")
else:
print("Decorator without parameter is called")
result = func(*args, **kwargs)
return result
return wrapper
# 如果传递了参数给装饰器,返回带参数的装饰器,把被装饰函数通过param传给func
if callable(param):
return decorator(param)
# 如果没有传递参数给装饰器,返回无参数的装饰器,相当于直接使用了decorator装饰器
else:
return decorator
# 使用无参数装饰器,相当于 my_decorator(my_function)
@my_decorator
def my_function():
print("Inside the function")
print(my_function)
# 使用带参数的装饰器,相当于my_decorator("example_param")(my_decorator)
@my_decorator("example_param")
def another_function():
print("Inside another function")
print(another_function)
if __name__ == "__main__":
my_function()
another_function()
类装饰器
不带参数的类装饰器
class AddPrint:
def __init__(self, func) -> None:
self.func = func
def __call__(self, *args, **kwargs):
rv = self.func(*args, **kwargs)
print("result is {}".format(rv))
return rv
@AddPrint
# 等价于 add = AddPrint(add)
def add(x, y):
return x + y
if __name__ == '__main__':
result = add(2, 3)
"""
输出
result is 5
"""
上述相当于把一个装饰器变成了一个 AddPrint类的对象,然后 add 函数被传入进了 __init__中,保存为self.func,在后面调用 add(2,3) 的时候,实际上相当于调用了__call__这个函数,做了一个对象的调用,后面参数2和3就被传入到了__call__里面,然后依顺序运行了代码。
带参数的类装饰器
class AddPrint:
def __init__(self, param) -> None:
self.param = param
def __call__(self, func):
print("AddPrint.__call__")
def wrapper(*args,**kwargs):
rv = func(*args,**kwargs)
print("param is {}, result is {}".format(self.param,rv))
return rv
return wrapper
@AddPrint("test")
# 等价于 add = AddPrint(add)
def add(x, y):
return x + y
if __name__ == '__main__':
print(add(2, 3))
"""
输出
AddPrint.__call__
param is test, result is 5
5
"""
上述把一个装饰器初始化为 AddPrint
类的对象,然后 "test"
参数被传入进了 __init__
中,之后调用__call__
函数返回一个闭包。之后就相当于装饰器函数了,用wrapper
装饰add
函数。
多层装饰器
多层装饰器是指在单个源代码函数名上方添加了多个语法糖,使这段源代码函数体具备多个功能。多层语法糖加载顺序由下往上。
下面的例子我们来看一下多层语法糖的执行顺序
def outer1(func):
print("outer1 in")
def inner1(*args,**kwargs):
print("inner1 in")
func()
print("outer1 out")
return inner1
def outer2(func):
print("outer2 in")
def inner2(*args,**kwargs):
print("inner2 in")
func()
print("outer2 out")
return inner2
def outer3(func):
print("outer3 in")
def inner3(*args,**kwargs):
print("inner3 in")
func()
print("outer3 out")
return inner3
@outer1
@outer2
@outer3
def index():
print("excute index function!")
index()
"""
输出
outer3 in
outer3 out
outer2 in
outer2 out
outer1 in
outer1 out
inner1 in
inner2 in
inner3 in
excute index function!
"""
多层的语法糖的执行逻辑,是从下往上,所以上面的例子中,从打印信息能看到先后执行了@outer3,@outer2,@outer1的外层函数,然后又依次执行了inner1,inner2,inner3,与index,为什么内层函数不是从下往上了呢?我们来具体的分析一下,是怎么执行的。
1.加载outer3,index作为参数传递给outer3,并返回inner3,inner3 = outer3(index),此时的inner3并没有被执行,而是被返回。
(此时的inner3的功能是什么?执行inner3和index的打印,也就是"inner3 in","excute index function!")
2.加载outer2,inner3作为参数传递给给outer2,并返回inner2,inner2 = outer2(inner3).
(此时的inner2的功能是执行inner2的打印和func的打印,func此时是什么,是传进来的inner3,看第一条,打印顺序也就是"inner2 in","inner3 in","excute index function")
3.加载outer1,inner2作为参数传递给outer1,并返回inner1,inner1的地址重新被index接收,index = outer1(inner2)
(同理,此时index的功能是执行inner1和func(也就是inner2)的打印,所以执行index的功能是打印"inner1 in","inner2 in","inner3 in","excute index function")
4.在装饰器装饰函数的时候,外层的函数就已经被执行了,顺序是outer3 -> outer2 -> outer1,然后调用index,所以也就是上面例子的打印了,语法糖的加载顺序是从下至上,内层函数调用是从上至下。
递归函数
函数直接或间接的调用自身,就是递归调用。递归函数一定要有边界,也就是明确的退出条件,不然将陷入死循环。
例子
def sum_number(num):
print(num)
# 递归的出口,当参数满足某个条件时,不再执行函数
if num == 1:
return
# 自己调用自己
sum_number(num - 1)
sum_number(5)
"""
输出
5
4
3
2
1
"""