Last Updated: 2023-05-31 07:37:36 Wednesday
-- TOC --
Python编程语言的超级语法糖,装饰器,decorator,看着就很高大上。装饰器的基础,是闭包,closure!
下面是一个简单的装饰器代码:
def calling_trace(func):
def wrapper():
print('calling', func.__name__)
func()
return wrapper
@calling_trace
def test1():
print('test1 is runing...')
# test1 = calling_trace(test1)
test1()
Python解释器在最后调用test1之前,就已经运行了装饰器(wrapper记住了func),效果如注释掉的那一行。这段代码的运行效果:
$ python3 deco.py
calling test1
test1 is runing...
装饰器作用于函数,就是把这个函数包裹起来,在此函数执行前后,增加一些其它的代码!经过装饰器修饰的函数,虽然函数名称不变,但实际上调用的入口已经发生了变化。如上例,直接调用test1(),入口实际上是calling_trace中的wrapper!
在function前使用@
装饰器,与test=calling_trace(test1)
的效果一样!装饰器将function作为参数,同时返回另一个function!
或者可以这样理解,装饰器将函数作为参数,执行装饰器的过程,实际上是创建了另一个函数,在这个新创建的函数内,执行被装饰的函数,被装饰的函数的原有的执行方式已经不再存在。
装饰一个function的概念,就是在不影响此function的执行的前提下,在其前其后增加一些代码,即所谓装饰。前后加打印,是最简单的装饰:
def calling_trace(func):
def wrapper():
print('calling', func.__name__)
a = func()
print('this is the reture:', a)
return wrapper
@calling_trace
def test2():
print('test2 is runing...')
return 'cs.pynote.net'
# test2 = calling_trace(test2)
test2()
执行结果:
$ python3 deco.py
calling test2
test2 is runing...
this is the reture: cs.pynote.net
装饰器就是一个返回函数的函数!
沿用上面的测试代码,稍作修改:
def calling_trace(func):
def wrapper():
print('calling', func.__name__)
a = func()
print('this is the reture:',a)
return wrapper
def test2():
print('test2 is runing...')
return 'cs.pynote.net'
tt = calling_trace(test2)
tt()
运行效果如下,与之前直接装饰test2的效果完全一样:
$ python3 deco.py
calling test2
test2 is runing...
this is the reture: cs.pynote.net
此时的tt,就是装饰过的test2!原生的test2还可以被使用,没有被装饰过。(使用装饰器就没有这样的效果)
可以看出closure的过程,就是调用一个可以接收函数作为参数的接口,通过返回得到一个新的接口。此时的tt,已经包含了test2这个函数参数在内,调用tt,相当于调用已经确定了参数的wrapper。
这就是python中的闭包的概念:把一个函数包起来,通过不同的外部参数,生成多个此函数的不同版本!这些外部参数,也成为被抱起来的函数接口的执行环境。(closure就是包起来,除了可以把函数接口包起来,还可以把class包起来。包起来还带来了一个封装的效果,被包起来的function或class,外部不能直接访问,比如通过函数调用返回后才能访问。class的__iter__
接口实现,就可以采用这个思路。)
>>> def addx(x):
... def inner(y):
... return y+x
... return inner
...
>>> add2 = addx(2)
>>> add2(1)
3
>>> add2(2)
4
>>> add2(3)
5
>>> add9 = addx(9)
>>> add9(1)
10
>>> add9(2)
11
>>> add9(3)
12
通过addx,生成了2个function,分别是add2和add9。注意这个case中的x入参,add2在执行的时候,x一直是2,而add9在执行的时候,x一直是9。我认为这是闭包的一个很重要的特性,它体现了add2和add9两个同源的接口的不同的执行环境。
功能上有点类似
from functools import partial
再看一个示例,模拟C语言的static variable:
def static_var():
dd = {}
def add_dd(a):
dd.update(a)
print(id(dd), dd)
return add_dd
a = static_var()
a({'a':1})
a({'b':2})
b = static_var()
b({'c':3})
a({'a':5})
b({'b':9})
以上代码执行效果如下:
$ python3 test.py
140246105463168 {'a': 1}
140246105463168 {'a': 1, 'b': 2}
140246105463424 {'c': 3}
140246105463168 {'a': 5, 'b': 2}
140246105463424 {'c': 3, 'b': 9}
可以看出,dd这个dict对象,对于a和b而言,是不一样的。
其实,在函数接口直接使用一个默认为可变对象的参数(比如list或dict),但在调用的时候,绝不给这个参数提供值,这就是Python实现static变量的方式。这是不是bug,看情况了。以上闭包的测试代码,是另一种static变量的实现方案,更安全,a和b作为生成的接口,没有入参对应static变量。
装饰器与闭包是一个东西,根据不同的使用方式,名称有了区别!
case one:
def calling_trace(func):
def wrapper(a,b,c=3):
print('calling', func.__name__)
a = func(a,b,c)
print('reture value:',a)
return wrapper
@calling_trace
def test3(a,b,c=3):
print('test3 is runing...')
return a+b+c
test3(1,2,5)
test3(1,2)
case two:
def calling_trace(func):
def wrapper(*args):
print('calling', func.__name__)
a = func(*args)
print('reture value:',a)
return wrapper
@calling_trace
def test4(*args):
print('test4 is runing...')
return sum(args)
test4(1,2,3,4,5,6,7,8)
test4(23,34,45,56)
wrapper与要装饰的函数,参数列表必须相同!
给带参数的函数加装饰器,还有一种更通用的参数写法,这种写法在修改函数参数和调用处时,又可以反过来保持装饰器部分代码不变。示例如下:
def calling_trace(func):
def wrapper(*args, **kw):
print('calling', func.__name__)
a = func(*args, **kw)
print('reture value:',a)
return wrapper
@calling_trace
def test5(a,b,c=3):
print('test5 is runing...')
return a+b+c
test5(1,2)
test5(1,2,c=8)
这种写法,要求key/value参数要放在后面,Python函数接口定义也有这个要求。
本文以上示例,都是不带参数的装饰器,在使用@语法的时候,没有参数。而装饰器本身也可以带参数。示例如下:
def calling_trace(run):
def deco(func):
def wrapper(*args, **kw):
print('calling', func.__name__)
if run == 1:
a = func(*args, **kw)
print('reture value:',a)
else:
print('not allow to run')
return wrapper
return deco
# test5 = calling_trace(run=1)(test5)
@calling_trace(run=1)
def test5(a,b,c=3):
print('test5 is runing...')
return a+b+c
@calling_trace(run=0)
def test6(*args):
print('test6 is runing...')
return sum(args)
test5(1,2)
test5(1,2,c=8)
test6(23,34,45,56)
calling_trace(run=1)返回的deco成为了真正装饰test5和test6的装饰器!
可以同时使用多个装饰器!多个装饰器的效果,就相当于对函数进行了多层的封装包裹,而不同的装饰器对函数执行的功能影响,完全独立。比如有个装饰器用来控制函数是否能够被执行,另一个装饰器控制函数的所有raise出来的异常。
@a
@b
@c
def tt(): pass
# tt = a(b(c(tt)))
从装饰器引出了一个很厉害的概念,叫做面向切面的编程!
面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
但是人们也发现,在分散代码的同时,也增加了代码的重复性。
什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(AOP)。
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。
AOP思想:
在网上看到如下一个经典的case,略作修改:
flist = []
for i in range(3):
def func(x):
return x*i
flist.append(func)
for f in flist:
print(id(f), f(2))
这段代码的运行结果:
140623728098704 4
140623727591088 4
140623727591232 4
func函数在运行时,始终只能访问def定义之外的i。因此当执行f(2)的时候,得到的结果都是4,因为loop之后,i=2。另一个值得注意的细节是,每一次flist.append的func的地址都不同!func被重复定了3次。
如何修正上面的代码,使其提供闭包特性,下面提供多种不同的修正方式供参考:
# avoid closures and use default args which copy on function definition
for i in range(3):
def func(x, i=i):
return x*i
flist.append(func)
# or introduce an extra scope to close the value you want to keep around:
for i in range(3):
def makefunc(i):
def func(x):
return x*i
return func
flist.append(makefunc(i))
# the second can be simplified to use a single makefunc():
def makefunc(i):
def func(x):
return x*i
return func
for i in range(3):
flist.append(makefunc(i))
# if your inner function is simple enough, lambda works as well for either option:
for i in range(3):
flist.append(lambda x, i=i: x*i)
def makefunc(i):
return lambda x: x*i
for i in range(3):
flist.append(makefunc(i))
直接上代码:
class calling_trace():
def __init__(self, run):
self.run = run
def __call__(self, func):
def wrapper(*args, **kw):
print('calling', func.__name__)
if self.run == 1:
a = func(*args, **kw)
print('return value:', a)
else:
print('not allow to run')
return wrapper
@calling_trace(run=1)
def test5(a,b,c=3):
print('test5 is runing...')
return a+b+c
@calling_trace(run=0)
def test6(*args):
print('test6 is runing...')
return sum(args)
test5(1,2)
test5(1,2,c=8)
test6(23,34,45,56)
运行情况:
$ python3 deco.py
calling test5
test5 is runing...
return value: 6
calling test5
test5 is runing...
return value: 11
calling test6
not allow to run
在Python中,像
__call__
这样前后都带下划线(dunden)的方法在Python中被称为内置方法,有时候也被称为魔法方法(magic method)。重载这些魔法方法一般会改变对象的内部行为。这个例子就让一个类对象拥有了被直接像函数调用的的能力。
from functools import update_wrapper
这个接口用来更新wrapper,让它看起来更像wrapped function。它主要是改变wrapper的__name__
和__doc__
,修改之后,返回wrapper。
from functools import update_wrapper
def before(func):
def wrapper():
print('before...')
func()
return wrapper
def after(func):
def wrapper():
print('after...')
func()
return update_wrapper(wrapper, func)
@before
def test1():
"""fucntion test1"""
print('hello test1')
@after
def test2():
"""function test2"""
print('hello test2')
test1()
print('__name__', test1.__name__)
print('__doc__', test1.__doc__)
print('------------')
test2()
print('__name__', test2.__name__)
print('__doc__', test2.__doc__)
运行结果:
$ python3 test_uw.py
before...
hello test1
__name__ wrapper
__doc__ None
------------
after...
hello test2
__name__ test2
__doc__ function test2
这样看来,自定义的装饰器,几乎必须要使用update_wrapper接口了。
本文链接:https://cs.pynote.net/sf/python/202203091/
-- EOF --
-- MORE --