首页 > 编程语言 >彻底理解Python中的闭包和装饰器(下)

彻底理解Python中的闭包和装饰器(下)

时间:2022-12-06 23:44:53浏览次数:68  
标签:闭包 return 函数 Python func 装饰 def log

上篇讲了Python中的闭包,本篇要讲的装饰器就是闭包的一个重要应用。

如果你还不知道什么是闭包,猛戳这里阅读:彻底理解Python中的闭包和装饰器(上)

什么是装饰器

装饰器的作用是在不修改函数定义的前提下增加现有函数的功能,比如打印函数名称、计算函数运行时间等。装饰器的本质是一个闭包。

下面是一段典型的装饰器定义代码:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

还记得闭包的两个特征吗?两个嵌套的函数,内函数引用了外函数的局部变量,外函数返回了内函数的引用。

在上面的代码中,内函数wrapper()引用了外部函数的变量func,这个变量是一个函数的引用。func.__name__的意义是获得__name__属性,即函数的名称。所以该装饰器的功能是在原函数调用时输出函数名称。

我们随便写一个函数在shell中输出一句话:

def myfunc():
    print('This is original function!')

按照通常的闭包使用方式,要用一个变量保存内函数的引用,然后使用这个函数指针变量调用内函数。所以上面的装饰器可以这样使用:

f = log(myfunc)
f()

运行结果如下:

call myfunc():
This is original function!

仍然牢记闭包的性质,定义函数指针时内函数仍未执行,直到用指针调用内函数。调用时,首先print句输出函数名称,接着return句中实际调用了func函数,即参数传入的myfunc函数。

这里调用内函数时没有传入任何参数,但将内函数定义为(*args, **kw)就可以接受任意类型的参数,具有通用性。

上面的用法完全正确,但装饰器的通常用法并不是这样,而是有更简单的写法。

装饰器的@语法

上面的用法是闭包的一般用法,但在装饰器中,我们一般使用@语法。

写法十分简单,只要在“被装饰”的函数定义前加上@闭包名称即可,如上面的装饰器应当这样写:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

@log
def myfunc():
    print('This is original function!')

其中的@log产生了如下效果:

myfunc = log(myfunc)

这表示myfunc()函数本身的指针(引用)代替了通常闭包中创建函数指针的一步。因此,每当直接调用myfunc函数时就会执行装饰器的闭包,从而产生了对原函数进行“装饰”的效果。

当调用一次原函数时:

myfunc()

输出为:

call myfunc():
This is original function!

读者一开始看到@语法时可能会觉得难以理解,但是将它还原成函数调用的形式,然后像通常的闭包一样理清执行顺序,就会简单许多。

注意:如果一个函数定义前使用了装饰器,那么装饰器本身会完全代替原函数。所以在使用装饰器时必须注意两点:

  1. 函数功能不变。
  2. 函数返回值不变。

为了做到第1点,装饰器必须接收一个函数引用的变量,并在闭包中调用且只调用一次。

为了做到第2点,装饰器的内函数必须返回原函数的返回值。

在本文最后的例子中我们还将继续探讨第2点。

自定义装饰器参数

正如上文所说,为了使原函数功能不变,装饰器必须接收一个原函数引用的变量作为参数。那么如果想传入其他参数怎么办呢?比如说,我想对不同的函数打印不同的字符串日志,该怎么办?

这时就必须再嵌套一层函数闭包,外层接收自定义参数,内层接收函数引用参数。代码如下:

import functools

def log(text):
    def decorator(func):
    	@functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s call %s:' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

@log('execute func1')
def func1():
    print('This is function 1')

@log('execute func2')
def func2():
    print('This is function 2')

func1()
func2()

输出为:

execute func1 call func1:
This is function 1
execute func2 call func2:
This is function 2

@语句和调用func1()和func2()的语句相当于:

f1 = log('execute func1')(func1)
f1()
f2 = log('execute func2')(func2)
f2()

这实际上是两层嵌套的函数调用。'execute func1'是传给log的参数,log('execute func1')的返回值返回值是内层函数的引用,因此后面还可以再加一个括号,将参数传入内层函数decorator(func)。

这里还多做了一件事:

@functools.wraps(func)

这是Python内置的一个装饰器,作用是将外层函数的属性复制给内层函数,这样输出的函数名称也会是传进来的参数func的值。

然而在测试时发现,不加这句话输出结果并没有变化。其中原因暂时没有搞清楚。为了避免问题,通常还是需要加上这个装饰器。

两个例子

再举两个例子,补充说明一下其他问题。这两个都是廖雪峰教程中的例子。

计时器

设计一个计时器装饰器,计算函数的运行时间。读者不妨自己尝试一下,再来看答案。

Tips:Python中计时可以使用time模块

代码如下:

def metric(func):
    @functools.wraps(func)
    def decorator(*args, **kw):
        start_time = time.time()
        ret = func(*args, **kw)
        end_time = time.time()
        print('%s execution time is %f seconds' % (func.__name__, end_time - start_time) )
        return ret
    return decorator

@metric
def fsum(x, y):
    time.sleep(5.0)
    return x + y

res = fast(11, 22)
print('res = ', res)

在之前的例子中,我们的内函数返回值是这样写的:

return func(*args, **kw)

物理上,执行这句代码实际上创建了中间的临时变量来保存返回值。为了免去繁杂的底层机理,我们可以直观理解为,首先调用了func()函数,将func()的返回值返回给这句return“后面”的地方,再由这句return返回给调用内函数处。

这样写保证了之前所要求的原函数返回值不变,同时节约了代码。

然而在计时器中这样写遇到了麻烦。因为代码的缩略,一旦返回,函数就结束了呀,我们怎么读取函数结束时的时间呢?笔者一开始也被困扰许久,后来发现其实解决方法十分简单,只要用一个变量先保存返回值,最后再return这个变量不就行了嘛。

究其原因,是因为一开始没有透彻理解代码的意思,只是遵从书写的“规则”而已。所以学习语言不能死板,而是要深入理解原理。

支持加/不加自定义参数

设计一个装饰器,使其既支持@log,又支持@log('execute')。

看到题目的第一反应是使用默认参数,但仔细一想就会发现这与默认参数不是一回事。当使用@log时,传入的第一个参数是函数引用本身,当使用@log('execute')时,传入的第一个参数是自定义的字符串。因此不能简单地使用默认参数解决。

笔者的解决方案是:判断参数类型,从而决定返回哪一个函数。完整代码如下:

def log(logarg):
    
    def decorator(func):
        @functools.wraps(func)
        def wrapper1(*args, **kw):
            print('%s call %s:' % (logarg, func.__name__))
            return func(*args, **kw)
        return wrapper1

    def wrapper2(*args, **kw):
        print('call %s():' % logarg.__name__)
        return logarg(*args, **kw)

    if isinstance(logarg, str):
        return decorator
    else:
        return wrapper2

@log
def func1():
    print('This is function 1')

@log('execute func2')
def func2():
    print('This is function 2')

func1()
func2()

小结

  1. 装饰器是一个闭包,其作用是增强“被装饰”函数的功能
  2. 为了保证原函数功能不变,必须在装饰器中调用且只调用一次原函数,且返回原函数的返回值
  3. 使用嵌套的闭包设计自定义参数的装饰器,并使用@functools.wraps(func)将函数属性赋值给内层闭包。

标签:闭包,return,函数,Python,func,装饰,def,log
From: https://www.cnblogs.com/midoq/p/16961810.html

相关文章

  • python 函数闭包(二)
      程序开始执行,执行到test()函数,不执行继续往下执行,当遇到test(100)调用函数的时候,将实参100传给形参number,然后又执行到内部的test_in()函数,程序不执行,执行......
  • (转)Python中动态导入对象importlib.import_module()的使用
    背景一个函数运行需要根据不同项目的配置,动态导入对应的配置文件运行。解决文件结构a#文件夹│a.py│__init__.pyb#文件夹│b.py│__init__.py├─c#文件夹│......
  • python之路43 JavaScript语法BOM与DOM jQuery对比
    前戏到目前为止,我们已经学过了JavaScript的一些简单的语法。但是这些简单的语法,并没有和浏览器有任何交互。也就是我们还不能制作一些我们经常看到的网页的一些交互,我们......
  • python高级
    ......
  • Python 实现海康机器人工业相机 MV-CU060-10GM 的实时显示视频流及拍照功能
    Python实现海康机器人工业相机MV-CU060-10GM的实时显示视频流及拍照功能 一、背景介绍1、最近项目中需要给客户对接海康机器人工业相机  MV-CU060-10GM;2、需要......
  • 使用pycharm or vscode来编写python代码?
    pycharm社区版可用于商业项目pycharm社区版可用于商业项目,来源于官方的回答:CanIuseCommunityEditionsofJetBrainsIDEsfordevelopingcommercialproprietarysof......
  • python 集合常用操作
    集合的特性无序、不重复、可迭代常用api创建一个集合需要显式地使用set()方法来声明,如果使用字面量{}来声明解析器会认为这是一个字典。add()往集合中添加一个元素......
  • Pythontext_9
    1#-*-coding:utf-8-*-2importsys#导入sys模块3importpygame#导入pygame模块45pygame.init()#初始化pygame6size=width,height=1000......
  • Python如何动态监控跟踪文件内容?
    需求:Python如何动态监控跟踪文件内容?写个小工具模仿linux中的tail来监控文件更新的内容?解答:利用文件的指针f.seek(0,2)importtimewithopen("a.txt",mode="r......
  • python制作简单的查询工具
    前言:利用python的flask框架制作简单的手机号码归属地查询工具。首先需要做两个页面,第一个页面收集用户的输入信息,点击“查询”按钮后,跳转到第二个页面,显示查询到的信息。一......