问题背景:之前学过 python
的装饰器的语法,但因当时接触的代码很少涉及装饰器的使用,所以很少使用以至于忘干净了。而今天接触的问题的确需要使用装饰器,果然还是任务驱动大法好。
问题背景:在 GUI
的开发中,有很多的 QLineEdit
对象,并在其中输入信息。假设某个输入框位置输入的是学号,学号作为学生数据表的主键插入,而字符串多一个空格会影响数据表内的存储,如 123
和 123{ }
是两个不一样的东西,即在数据库内存储两份,但实际是一个东西。{ }
表示有个空格占位符。
而在输入时普通用户可能会一不小心多加了一个空格,从开发良好软件的角度出发:后台应该把这些空格得去掉。而有很多的 QLineEdit()
对象时,又不想一个个的找到对象的位置,写很多次去除空格的函数(并不是很好的编码习惯),这个时候可能就需要装饰器了。
在含有 QLineEdit()
的函数中,调用装饰器给函数装饰一下,在不影响原来代码结构和语句的基础上,增加去除空格的装饰工作。省时省力,不破坏原有结构,值得推荐。今天:写个函数 return
回来不好么,写装饰器改全局变量还不够费劲(不太想装饰器里面 global
)。
那么举个好点的例子,在超大规模图融合时,融合分五个子函数完成,每处理一次就需要记录一次图的边割率、平衡率等日至信息。子函数之间传递的是图的信息,如果在不破坏子函数代码结构的情况下,可以使用装饰器来记录边割率、平衡率等日至信息。
装饰器 Decorators
是 Python
的一个重要部分,本质上是一个 Python
函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。
它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
Python的函数
函数作为参数
函数可以作为参数执行:Python 中的函数可以像普通变量一样当做参数传递给另外一个函数。比如把 foo
函数作为参数传递给 bar
函数,bar
函数体内执行这个参数:
1 | def foo(): |
输出:
1 | foo |
函数中定义与调用函数
在 Python
中我们可以在一个函数中定义另一个函数:
1 | def hi(name="yasoob"): |
输出:
1 | now you are inside the hi() function |
从函数中返回函数
注意与上述代码的差别:
1 | def hi(name="yasoob"): |
输出:
1 | <function hi.<locals>.greet at 0x7fb8b02579e0> |
再次看看这个代码。在 if/else
语句中我们返回 greet
和 welcome
,而不是 greet()
和 welcome()
。为什么这样?这是因为当你把一对小括号放在后面,这个函数就会执行;然而如果你不放括号在它后面,那它可以被到处传递,并且可以赋值给别的变量而不去执行它。
第一个装饰器和@语法糖
虽然可以通过函数传递的方式来完成函数执行前的修饰功能,如下。
1 | def use_logging(func): |
这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo
函数,而是换成了 use_logging
函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo
函数作为参数传递给 use_logging
函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。
如果函数 bar()
、bar2()
等函数都有类似的需求,怎么做?再写一个 logging
在 bar
函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志,日志处理完之后再执行真正的业务代码。
1 | def use_logging(func): |
如上所示,有了 @
,我们就可以省去 foo = use_logging(foo)
这一句了,直接调用 foo()
即可得到想要的结果。foo()
函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。
装饰器在 Python
使用如此方便都要归因于 Python
的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。
函数元信息问题
之前网上的文章都说在使用装饰器时,修饰函数的元信息会代替调用函数的元信息,但现在2019(快2020)年了,貌似没了这个问题。
1 | def logit(func): |
输出:
1 | addition_func was called |
之前(别人博客写的)的输出:
1 | with_logging was called |
仅供参考:
(我并没遇到此类问题):当然安全起见,可以使用下面的方法。使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring
、__name__
、参数列表,我们有functools.wraps
,wraps
本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func
函数中,这使得装饰器里面的 func
函数也有和原函数 foo
一样的元信息了。
1 | from functools import wraps |
输出:
1 | test----> |
带参数的装饰器
若不同方法对装饰器的需求有细微的差别,需要额外传入参数来完成差别的区分。如 @use_logging(level="warn")
等价于 @decorator
,因此里面多了一组函数。
1 | def use_logging(level): |
上面的 use_logging
是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。。当我们使用 @use_logging(level="warn")
调用的时候,python
能够发现这一层的封装,并把参数传递到装饰器的环境中。
1 | foo is running |
装饰器类
没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__
方法,当使用 @
形式将装饰器附加到函数上时,就会调用此方法。
假设有时你只想打日志到一个文件。而有时你想把引起你注意的问题发送到一个 email
,同时也保留日志,留个记录。这是一个使用继承的场景,但目前为止我们只看到过用来构建装饰器的函数。幸运的是,类也可以用来构建装饰器。那我们现在以一个类而不是一个函数的方式,来重新构建 logit
。
1 | from functools import wraps |
输出:
1 | myfunc1 was called |
继承
给 logit 创建子类,来添加 email 的功能(虽然 email 这个话题不会在这里展开)。
1 | class email_logit(logit): |
输出:
1 | --test1-- |
装饰器顺序
一个函数还可以同时定义多个装饰器,比如:
1 |
|
它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于f = a(b(c(f)))
。
练习
去除字符串空格
1 | from functools import wraps |
但是并不推荐这种global的写法,容易引起变量混乱。不如以下方式简洁明了:
1 | text = '201614420112 ' |
记录函数执行时间
1 | import time |
输出:
1 | use time: 8.249282836914062e-05 |
本文参考
更多的像是转载
https://www.runoob.com/w3cnote/python-func-decorators.html