Python-闭包和装饰器

闭包

在了解闭包之前,我们先了解一下Python中的作用域规则。

变量作用域规则

当在函数内部引用一个变量时,Python解释器会按照LEGB规则依次查找该变量:

  • L: Local,局部作用域,即函数内部
  • E: Enclosing,嵌套函数的作用域
  • G: Global,全局作用域
  • B: Built-in,内置模块的作用域

注意: 在Python中,函数内部的全局变量是不可变的,如果需要在函数内部修改全局变量,需要使用global关键字 (也就是说,在函数内部初始化的变量,会被编译成局部变量)。

 1>>> b = 6
 2>>> def f1(a):
 3...    global b
 4...    print(a)
 5...    print(b)
 6...    b = 9
 7>>> f1(3)
 83
 96
10>>> b
119

闭包的概念

闭包,指延伸了作用域的函数,可以访问函数定义体之外定义的非全局变量(即Enclosing层)。

特点:

  • 闭包必须是一个嵌套函数
  • 嵌套函数必须引用外部函数的非全局变量
  • 外部函数返回嵌套函数
 1# 嵌套函数但不是闭包函数
 2def nested_func():
 3    x = 10
 4    def inner_func():
 5        # 没有引用外部函数的非全局变量
 6        print("inner_func")
 7    # 没有返回嵌套函数
 8    inner_func()
 9
10# 闭包函数
11def closure_func():
12    x = 10
13    def inner_func(y):
14        return x + y
15    return inner_func

nonlocal关键字

在Python3中,引入了nonlocal关键字,用于在函数内部修改外部函数的局部变量。例如:

 1def closure_func_one():
 2    x[0] = 10
 3    def inner_func():
 4        x[0] += 1
 5        return x
 6    return inner_func
 7
 8def closure_func_two():
 9    x = 10
10    def inner_func():
11        nonlocal x
12        x += 1
13        return x
14    return inner_func

对比closure_func_oneclosure_func_twoclosure_func_one中的x是一个列表(可变类型),可以在内部函数中修改。 而对于一些不可变类型,如整型、字符串等,在内部函数中,只能读取,修改会隐式创建局部变量,所以不会保存在闭包中。

closure_func_two中,使用nonlocal关键字,指将x标记为自由变量,即使在函数中修改x,闭包中保存的绑定也会更新。

那么,为什么要使用闭包呢?

  • 避免随意使用全局变量,提高代码的可维护性
  • 提供了一种封装机制,可以隐藏函数内部定义一些私有变量,外部无法直接访问,只能通过函数内部的方法来访问和修改
  • 提供更加灵活的函数调用方式

装饰器

装饰器是Python中的一种高级函数,可以用来修改或扩展函数或方法的行为。它本质上是一个函数,接受一个函数作为参数,将它返回或者将其替换为另一个函数或可调用对象。装饰器可以在不修改原函数代码的情况下,为函数添加新的功能或行为。

下面是一个简单的装饰器示例:

 1def my_decorator(func):
 2    def inner_func():
 3        print("Before calling the function")
 4        func()
 5        print("After calling the function")
 6    return inner_func
 7
 8# 使用装饰器
 9@my_decorator
10def my_function():
11    print("This is my function")

装饰器和闭包

装饰器,可理解为一种特殊的闭包,接受一个函数作为参数,返回一个函数,用于修改或扩展函数的行为。而普通的闭包只是一个嵌套函数,用于延伸作用域。

@my_decorator 装饰器将函数 my_function 传递给函数 my_decorator,函数 my_decorator 执行完毕后返回被包装后的 my_function 函数,而这个过程其实就是通过闭包实现的。

如果不使用装饰器,我们需要显式调用 my_decorator(my_function) 来实现相同的效果。严格来说,装饰器是一种语法糖,使得代码更加简洁易读。

装饰器的执行顺序

装饰器在定义时就会执行,而不是在调用时执行。当Python解释器执行到装饰器时,会立即执行装饰器函数,并将被装饰的函数作为参数传递给装饰器函数。

示例:

 1registry = []
 2def register(func):
 3    print(f"Registering {func}")
 4    registry.append(func)
 5    return func
 6
 7@register
 8def f1():
 9    print("Running f1()")
10
11@register
12def f2():
13    print("Running f2()")
14    
15def main():
16    print("Running main()")
17    print("Registry:", registry)
18    f1()
19    f2()
20
21if __name__ == "__main__":
22    main()

输出为:

1Registering <function f1 at 0x7f8b3b7b7d30>
2Registering <function f2 at 0x7f8b3b7b7e50>
3Running main()
4Registry: [<function f1 at 0x7f8b3b7b7d30>, <function f2 at 0x7f8b3b7b7e50>]
5Running f1()
6Running f2()

装饰器叠放

装饰器可以叠放,即一个函数可以被多个装饰器修饰,装饰器的执行顺序是从下往上执行,即从内到外执行。

1@decorator1
2@decorator2
3def func():
4    pass

相当于:

1def func():
2    pass
3func = decorator1(decorator2(func))

参数化装饰器

装饰器也可以接受参数,这样可以根据参数的不同,为函数添加不同的功能。

示例:

 1def register(active=True):
 2      def wrapper(func):
 3          if active:
 4              print(f"Registering {func}")
 5          else:
 6              print(f"Not registering {func}")
 7              
 8          return func
 9      return wrapper
10
11@register()
12def f1():
13    print("Running f1()")
14    
15@register(active=False)
16def f2():
17    print("Running f2()")
18
19def main():
20    f1()
21    f2()
22    
23if __name__ == "__main__":
24    main()

输出为:

1Registering <function f1 at 0x0000021C9A1EEC00>
2Not registering <function f2 at 0x0000021C9A1EDA80>
3Running f1()
4Running f2()

标准库中的装饰器

内置装饰器

Python标准库中提供了一些内置的装饰器,如@staticmethod@classmethod@property等,用于修饰类的方法。

@staticmethod:将类的方法转换为静态方法,不需要self参数,不需要实例化,直接类名.方法名()调用。

@classmethod:将类的方法转换为类方法,不需要self参数,但第一个参数需要表示自身类的cls参数,不需要实例化,可以类名调用,也可以对象调用,因为持有cls参数,可以调用类的属性,类的方法,实例化对象等,避免硬编码。

@property:将类的方法转换为属性,并且只有一个self参数,可以通过属性的方式访问,而不是通过方法调用。

functools模块

另外,functools模块中也有一些常用的装饰器,如@wraps@lru_cache等。

@wraps

用于保留原函数的元信息,如函数名、文档字符串、参数列表等,避免装饰器修改原函数的元信息。 可以通过下面的示例来理解:

 1from functools import wraps
 2
 3def a_decorator(func):
 4    def wrapper(*args, **kwargs):
 5        """A wrapper function"""
 6        # Extend some capabilities of func
 7        func()
 8    return wrapper
 9 
10@a_decorator
11def first_function():
12    """This is docstring for first function"""
13    print("first function")
14 
15print(first_function.__name__) # wrapper
16print(first_function.__doc__) # A wrapper function
17
18def b_decorator(func):
19    @wraps(func)
20    def wrapper(*args, **kwargs):
21        """A wrapper function"""
22        # Extend some capabilities of func
23        func()
24    return wrapper
25
26@b_decorator
27def second_function():
28    """This is docstring for second function"""
29    print("second function")
30
31print(second_function.__name__) # second_function
32print(second_function.__doc__) # This is docstring for second function
@cache

用于缓存函数的返回值,避免重复计算,提高函数的执行效率。 没有使用缓存的递归斐波那契数列函数:

 1import time
 2def clock(func):
 3    def clocked(*args, **kwargs):
 4        t0 = time.time()
 5        result = func(*args, **kwargs)
 6        elapsed = time.time() - t0
 7        name = func.__name__
 8        arg_str = ', '.join(repr(arg) for arg in args)
 9        print(f'[{elapsed:.8f}s] {name}({arg_str}) -> {result}')
10        return result
11    return clocked
12
13@clock
14def fibonacci(n):
15    if n < 2:
16        return n
17    time.sleep(1)
18    return fibonacci(n-2) + fibonacci(n-1)
19
20if __name__ == '__main__':
21    print(fibonacci(4))

输出为:

 1[0.00000000s] fibonacci(0) -> 0
 2[0.00000000s] fibonacci(1) -> 1
 3[1.00106740s] fibonacci(2) -> 1
 4[0.00000000s] fibonacci(1) -> 1
 5[0.00000000s] fibonacci(0) -> 0
 6[0.00000000s] fibonacci(1) -> 1
 7[1.00131822s] fibonacci(2) -> 1
 8[2.00183153s] fibonacci(3) -> 2
 9[4.00414205s] fibonacci(4) -> 3
103

使用缓存的递归斐波那契数列函数:

 1from functools import cache
 2
 3@cache
 4@clock
 5def fibonacci(n):
 6    if n < 2:
 7        return n
 8    time.sleep(1)
 9    return fibonacci(n-2) + fibonacci(n-1)
10    
11if __name__ == '__main__':
12    print(fibonacci(4))

输出为:

1[0.00000000s] fibonacci(0) -> 0
2[0.00000000s] fibonacci(1) -> 1
3[1.00009847s] fibonacci(2) -> 1
4[1.00089359s] fibonacci(3) -> 2
5[3.00199485s] fibonacci(4) -> 3
63

可以明显看到,使用缓存后,不会重复计算已经计算过的值,提高了函数的执行效率。

@lru_cache

若缓存较大,@cache可能会耗尽内存,这个时候可以设置@lru_cache中的参数maxsize(缓存条目)和typed(是否将不同参数类型分开保存)灵活缓存数据。

用法如下:

1from functools import lru_cache
2
3@lru_cache(maxsize=128, typed=True)
4def fibonacci(n):
5    if n < 2:
6        return n
7    time.sleep(1)
8    return fibonacci(n-2) + fibonacci(n-1)
@singledispatch

用于实现函数的重载,根据不同的参数类型调用不同的函数。

 1from functools import singledispatch
 2
 3@singledispatch
 4def fun(arg):
 5    print("I don't know what to do with this:", arg)
 6    
 7@fun.register(int)
 8def _(arg):
 9    print("Received an integer:", arg)
10    
11@fun.register(str)
12def _(arg):
13    print("Received a string:", arg)
14
15fun(1) # Received an integer: 1
16fun("hello") # Received a string: hello
17fun(3.14) # I don't know what to do with this: 3.14

总结

闭包和装饰器是Python中非常重要的概念,可以帮助我们更好地理解Python的函数式编程特性。

是小柒鸭
是小柒鸭
佛系·猫奴·程序媛

在无聊的时间里就从事学习。 —— 亚伯拉罕·林肯