装饰器

装饰器引入

初期及问题诞生

假如现在在一个公司,有A B C三个业务部门,还有S一个基础服务部门,目前呢,S部门提供了两个函数,供其他部门调用,函数如下:

def f1():
    print('f1 called')


def f2():
    print('f2 called')

在初期,其他部门这样调用是没有问题的,随着公司业务的发展,现在S部门需要对函数调用假如权限验证,如果有权限的话,才能进行调用,否则调用失败。考虑一下,如果是我们,该怎么做呢?

方案集合

  1. 让调用方也就是ABC部门在调用的时候,先主动进行权限验证
  2. S部门在对外提供的函数中,首先进行权限认证,然后再进行真正的函数操作

问题

  • 方案一,将本不该暴露给外层的权限认证,暴露在使用方面前,同时如果有多个部门呢,要每个部门每个人都要周知到,你还不缺定别人一定会这么做,不靠谱。。。
  • 方案二,看似看行,可是当S部门对外提供更多的需要进行权限验证方法时,每个函数都要调用权限验证,同样也实在费劲,不利于代码的维护性和扩展性
    那么,有没有一种方法能够遵循代码的开放闭合原则,来完美的解决此问题呢?

装饰器引入

答案肯定是有的,不然真的是弱爆了。先看代码

def w1(func):
    def inner():
        print('...验证权限...')
        func()

    return inner


@w1
def f1():
    print('f1 called')


@w1
def f2():
    print('f2 called')


f1()
f2()

输出结果为

...验证权限...
f1 called
...验证权限...
f2 called

可以通过代码及输出看到,在调用f1 f2 函数时,成功进行了权限验证,那么是怎么做到的呢?其实这里就使用到了装饰器,通过定义一个闭包函数w1,在我们调用函数上通过关键词@w1,这样就对f1 f2函数完成了装饰。

装饰器原理

  • 首先,开看我们的装饰器函数w1,该函数接收一个参数func,其实就是接收一个方法名,w1内部又定义一个函数inner,在inner函数中增加权限校验,并在验证完权限后调用传进来的参数func,同时w1的返回值为内部函数inner,其实就是一个闭包函数。

  • 然后,再来看一下,在f1上增加@w1,那这是什么意思呢?当python解释器执行到这句话的时候,会去调用w1函数,同时将被装饰的函数名作为参数传入(此时为f1),根据闭包一文分析,在执行w1函数的时候,此时直接把inner函数返回了,同时把它赋值给f1,此时的f1已经不是未加装饰时的f1了,而是指向了w1.inner函数地址。

  • 接下来,在调用f1()的时候,其实调用的是w1.inner函数,那么此时就会先执行权限验证,然后再调用原来的f1(),该处的f1就是通过装饰传进来的参数f1。

这样下来,就完成了对f1的装饰,实现了权限验证。

装饰器知识点

执行时机

了解了装饰器的原理后,那么它的执行时机是什么样呢,接下来就来看一下。
国际惯例,先上代码

def w1(fun):
    print('...装饰器开始装饰...')

    def inner():
        print('...验证权限...')
        fun()

    return inner


@w1
def test():
    print('test')

test()

输出结果为

...装饰器开始装饰...
...验证权限...
test

由此可以发现,当python解释器执行到@w1时,就开始进行装饰了,相当于执行了如下代码:

test = w1(test)

两个装饰器执行流程和装饰结果

当有两个或两个以上装饰器装饰一个函数时,那么执行流程和装饰结果是什么样的呢?同样,还是以代码来说明问题。

def makeBold(fun):
    print('----a----')

    def inner():
        print('----1----')
        return '<b>' + fun() + '</b>'

    return inner


def makeItalic(fun):
    print('----b----')

    def inner():
        print('----2----')
        return '<i>' + fun() + '</i>'

    return inner


@makeBold
@makeItalic
def test():
    print('----c----')
    print('----3----')
    return 'hello python decorator'


ret = test()
print(ret)

输出结果:

----b----
----a----
----1----
----2----
----c----
----3----
<b><i>hello python decorator</i></b>

可以发现,先用第二个装饰器(makeItalic)进行装饰,接着再用第一个装饰器(makeBold)进行装饰,而在调用过程中,先执行第一个装饰器(makeBold),接着再执行第二个装饰器(makeItalic)。

为什么呢,分两步来分析一下。

装饰时机 通过上面装饰时机的介绍,我们可以知道,在执行到@makeBold的时候,需要对下面的函数进行装饰,此时解释器继续往下走,发现并不是一个函数名,而又是一个装饰器,这时候,@makeBold装饰器暂停执行,而接着执行接下来的装饰器@makeItalic,接着把test函数名传入装饰器函数,从而打印’b’,在makeItalic装饰完后,此时的test指向makeItalic的inner函数地址,这时候有返回来执行@makeBold,接着把新test传入makeBold装饰器函数中,因此打印了’a’。
在调用test函数的时候,根据上述分析,此时test指向makeBold.inner函数,因此会先打印‘1‘,接下来,在调用fun()的时候,其实是调用的makeItalic.inner()函数,所以打印‘2‘,在makeItalic.inner中,调用的fun其实才是我们最原声的test函数,所以打印原test函数中的‘c‘,‘3‘,所以在一层层调完之后,打印的结果为<b><i>hello python decorator</i></b> 。

对无参函数进行装饰

上面例子中的f1 f2都是对无参函数的装饰,不再单独举例

对有参函数进行装饰

在使用中,有的函数可能会带有参数,那么这种如何处理呢?
代码优先:

def w_say(fun):
    """
    如果原函数有参数,那闭包函数必须保持参数个数一致,并且将参数传递给原方法
    """

    def inner(name):
        """
        如果被装饰的函数有行参,那么闭包函数必须有参数
        :param name:
        :return:
        """
        print('say inner called')
        fun(name)

    return inner


@w_say
def hello(name):
    print('hello ' + name)


hello('wangcai')

输出结果为:

say inner called
hello wangcai

具体说明代码注释已经有了,就不再单独说明了。
此时,也许你就会问了,那是一个参数的,如果多个或者不定长参数呢,该如何处理呢?看看下面的代码你就秒懂了。

def w_add(func):
    def inner(*args, **kwargs):
        print('add inner called')
        func(*args, **kwargs)

    return inner


@w_add
def add(a, b):
    print('%d + %d = %d' % (a, b, a + b))


@w_add
def add2(a, b, c):
    print('%d + %d + %d = %d' % (a, b, c, a + b + c))


add(2, 4)
add2(2, 4, 6)

输出结果为:

add inner called
2 + 4 = 6
add inner called
2 + 4 + 6 = 12

利用python的可变参数轻松实现装饰带参数的函数。

对带返回值的函数进行装饰

下面对有返回值的函数进行装饰,按照之前的写法,代码是这样的

def w_test(func):
    def inner():
        print('w_test inner called start')
        func()
        print('w_test inner called end')
    return inner


@w_test
def test():
    print('this is test fun')
    return 'hello'


ret = test()
print('ret value is %s' % ret)

输出结果为:

w_test inner called start
this is test fun
w_test inner called end
ret value is None

可以发现,此时,并没有输出test函数的‘hello’,而是None,那是为什么呢,可以发现,在inner函数中对test进行了调用,但是没有接受不了返回值,也没有进行返回,那么默认就是None了,知道了原因,那么来修改一下代码:

def w_test(func):
    def inner():
        print('w_test inner called start')
        str = func()
        print('w_test inner called end')
        return str

    return inner


@w_test
def test():
    print('this is test fun')
    return 'hello'


ret = test()
print('ret value is %s' % ret)

输出结果:

w_test inner called start
this is test fun
w_test inner called end
ret value is hello

这样就达到预期,完成对带返回值参数的函数进行装饰。

带参数的装饰器

介绍了对带参数的函数和有返回值的函数进行装饰,那么有没有带参数的装饰器呢,如果有的话,又有什么用呢?
答案肯定是有的,接下来通过代码来看一下吧。

def func_args(pre='xiaoqiang'):
    def w_test_log(func):
        def inner():
            print('...记录日志...visitor is %s' % pre)
            func()

        return inner

    return w_test_log


# 带有参数的装饰器能够起到在运行时,有不同的功能

# 先执行func_args('wangcai'),返回w_test_log函数的引用
# @w_test_log
# 使用@w_test_log对test_log进行装饰
@func_args('wangcai')
def test_log():
    print('this is test log')


test_log()

输出结果为:

...记录日志...visitor is wangcai
this is test log

简单理解,带参数的装饰器就是在原闭包的基础上又加了一层闭包,通过外层函数func_args的返回值w_test_log就看出来了,具体执行流程在注释里已经说明了。
好处就是可以在运行时,针对不同的参数做不同的应用功能处理。

通用装饰器

介绍了这么多,在实际应用中,如果针对没个类别的函数都要写一个装饰器的话,估计就累死了,那么有没有通用万能装饰器呢,答案肯定是有的,废话不多说,直接上代码。

def w_test(func):
    def inner(*args, **kwargs):
        ret = func(*args, **kwargs)
        return ret

    return inner


@w_test
def test():
    print('test called')


@w_test
def test1():
    print('test1 called')
    return 'python'


@w_test
def test2(a):
    print('test2 called and value is %d ' % a)


test()
test1()
test2(9)

输出结果为:

test called
test1 called
test2 called and value is 9 

把上面几种示例结合起来,就完成了通用装饰器的功能,原理都同上,就不过多废话了。

类装饰器

装饰器函数其实是一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。
在python中,一般callable对象都是函数,但是也有例外。比如只要某个对象重写了call方法,那么这个对象就是callable的。

当创建一个对象后,直接去执行这个对象,那么是会抛出异常的,因为他不是callable,无法直接执行,但进行修改后,就可以直接执行调用了,如下

class Test(object):
    def __call__(self, *args, **kwargs):
        print('call called')


t = Test()
print(t())

输出为:

call called

下面,引入正题,看一下如何用类装饰函数。

class Test(object):
    def __init__(self, func):
        print('test init')
        print('func name is %s ' % func.__name__)
        self.__func = func

    def __call__(self, *args, **kwargs):
        print('装饰器中的功能')
        self.__func()


@Test
def test():
    print('this is test func')


test()

输出结果为:

test init
func name is test 
装饰器中的功能
this is test func

和之前的原理一样,当python解释器执行到到@Test时,会把当前test函数作为参数传入Test对象,调用init方法,同时将test函数指向创建的Test对象,那么在接下来执行test()的时候,其实就是直接对创建的对象进行调用,执行其call方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容