自学python笔记09——闭包与装饰器

本文围绕Python编程展开,详细介绍了闭包和装饰器。闭包是嵌套函数,可访问外部函数变量,能解决全局变量篡改问题,但会消耗内存。装饰器是特殊闭包,能为函数增加新功能,不过会修改原函数元信息,还介绍了其执行顺序、参数使用等,最后提及递归函数。

认识闭包

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
"""

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值