【python学习笔记】Python装饰器

装饰器

参考:

搞懂Python装饰器

Python @wraps 修饰器

装饰器是什么

有兴趣的可以参考PEP 318的原文 Decorators for Functions and Methods 解释了语法用途以及设计出来装饰器的动机

The current method for transforming functions and methods (for instance, declaring them as a class or static method) is awkward and can lead to code that is difficult to understand. Ideally, these transformations should be made at the same point in the code where the declaration itself is made. This PEP introduces new syntax for transformations of a function or method declaration.


Translated by GPT-4o:当前用于转换函数和方法(例如,将它们声明为类方法或静态方法)的方法很笨拙,并且可能导致难以理解的代码。理想情况下,这些转换应该在代码中声明本身的同一位置进行。该PEP引入了一种新的语法,用于函数或方法声明的转换。

原文的例子中表明

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

等价于

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

用法

闭包(closure)

只是简单地介绍下,详细的请参考 理解Python闭包概念

闭包的特点:函数内部定义的函数能够记住它们创建时的环境,包括任何在外部函数作用域中可用的变量。

def outer_function(message):
    msg = [message]
    def inner_function(person):
        msg.append(person)
        print(msg)
    return inner_function

closure = outer_function("Hello, World!")
closure("x")
closure("y")
------------------------------------------
['Hello, World!', 'x']
['Hello, World!', 'x', 'y']

闭包和面向对象

闭包比较简洁,也是函数式编程的重要组成部分,但是在可读性方面有所欠缺

面向对象可以通过属性来轻松地在更加复杂的场景中,但是有时候比较重

注意事项

可变类型和不可变类型

参考 可变与不可变类型

内建函数id()返回值的内存地址。

可变对象可以在其id()保持固定不变的情况下改变其取值。
基本数据类型中,列表,集合,字典都是可变数据类型。

如果修改一个对象的值,必须创建新的对象,那么这个对象就是不可变对象。
基本数据类型中,数字,字符串,元组是不可变类型。

变量的引用

inner function中可以访问outer function中的变量,可以修改可变数据类型(如列表、字典),但是不能修改不可变数据类型(如数字、字符串、元组)
我的理解是内部函数并不能改变外部函数变量的地址,但是可以在原址上修改

def outer_function(message):
    msg = message
    def inner_function(person):
        msg += person
        print(msg)
    return inner_function

closure = outer_function("Hello, World!")
closure("x")
closure("y")
------------------------------------------
    msg += person
    ^^^
UnboundLocalError: cannot access local variable 'msg' where it is not associated with a value

但是可以通过 nonlocal 来告诉Python该变量来自外部函数

def outer_function(message):
    msg = message
    def inner_function(person):
        nonlocal msg
        msg += person
        print(msg)
    return inner_function

closure = outer_function("Hello, World!")
closure("x")
closure("y")
------------------------------------------
Hello, World!x
Hello, World!xy
闭包陷阱

还是用一下很经典的例子

# 该例子的本意为记录每次循环中i值的平方
def my_func(*args):
    fs = []
    for i in range(3):
        def func():
            return i * i
        fs.append(func)
    return fs

# 故按照该思路预期的结果应该是0 1 4
# 但实际上是4 4 4
fs1, fs2, fs3 = my_func()
print(fs1())
print(fs2())
print(fs3())

这里看了一篇博客 循环变量与闭包 其中介绍到

Python中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其他代码块(如ifforwhile)都不会引入新的作用域。同时,Python中的闭包是以引用的方式捕获变量的,所以我们已经可以猜到最终输出是相同值了。

对比该篇博客中的一些其他语言的情况,可以理解为因为Python中的循环变量是重复使用的,且Python中闭包捕获变量是用类似指针的方式指向的该变量的地址。
故没有新的变量,旧变量会随着循环在原地址中一直改变,fs中append进去的也是一直会随着地址中保存的变量改变而改变的变量。
所以最终循环结束,结果都会变为4

根据以上原因,可以使用会新建作用域的lambda

def my_func(*args):
    fs = []
    for i in range(3):
        fs.append((lambda x=i: x * x))
    return fs


fs1, fs2, fs3 = my_func()
print(fs1())
print(fs2())
print(fs3())

返回闭包中不要引用任何循环变量,或者后续会发生变化的变量。

修饰器

装饰器就是一种闭包的应用

我觉得直接看一个例子比较直观,先把整个代码放出来

from functools import wraps


def the_first(fn):
    @wraps(fn)
    def firstly_wrapped():
        """wrapped in the first function"""
        print("in firstly_wrapped")
        print(fn.__name__)
        print(fn.__doc__)
        return 'here is the first function - ' + fn()

    return firstly_wrapped


def the_second(fn):
    @wraps(fn)
    def second_wrapped():
        """wrapped in the second function"""
        print("in second_wrapped")
        print(fn.__name__)
        print(fn.__doc__)
        return 'here is the second function - ' + fn()

    return second_wrapped


@the_first
@the_second
def hello_world():
    """hello with two decoration"""
    return "Hello World"


print(hello_world())
-------------------------------------------------------------
in firstly_wrapped # firstly_wrapped中的print文本
hello_world # firstly_wrapped中输出的fn.__name__
hello with two decoration # firstly_wrapped中输出的fn.__doc__(即def下方用""""""围起来的文本)
in second_wrapped # second_wrapped中的print文本
hello_world # second_wrapped中输出的fn.__name__
hello with two decoration # second_wrapped中输出的fn.__doc__
here is the first function - here is the second function - Hello World # hello_world中的return

先看 hello_world 部分

@the_first
@the_second
def hello_world():
    """hello with two decoration"""
    return "Hello World"

print(hello_world())

两个装饰器等价于

the_first(the_second(hello_world()))

执行过程就是 the_firstthe_secondhello_world

再看 @wraps(fn) 的部分,这个修饰器的作用是为了 hello_world 不被覆盖,如果两个 @wraps(fn) 都注释掉了的情况会是

in firstly_wrapped # firstly_wrapped中的print文本
second_wrapped # firstly_wrapped中输出的fn.__name__
wrapped in the second function # firstly_wrapped中输出的fn.__doc__
in second_wrapped # second_wrapped中的print文本
hello_world # second_wrapped中输出的fn.__name__
hello with two decoration # second_wrapped中输出的fn.__doc__
here is the first function - here is the second function - Hello World # hello_world中的return

与一开始结果的区别主要在

second_wrapped # firstly_wrapped中输出的fn.__name__
wrapped in the second function # firstly_wrapped中输出的fn.__doc__

是因为此时传进 firstly_wrapped 已经不是 hello_world 了,而是覆盖掉 hello_worldsecond_wrapped

@wraps 装饰器可以把被包装函数的元数据,例如函数名、文档字符串、函数签名等信息保存下来。

可以理解为是将 fn 的信息复制给 firstly_wrapped ,这个函数中出现的 fn 都会是指 fn 本身而不会被覆盖掉

装饰器的应用

可以很轻松地进行一些缓存或者log操作

详情参考:装饰器的应用场景

posted @ 2024-07-04 16:30  ryukirin  阅读(2)  评论(0编辑  收藏  举报