再谈python装饰器

人生苦短,我用Python

__ 文:郑元春__


1. 一切都是参数

首先,在python中什么都可以传递,不只是一般的变量,对象,python还具有面向函数编程的思想,所以你写的任何的函数也可以当做是参数传递给另一个函数。只要记住,在python中什么都能传递。

函数的作用当然是实现功能了,所以作为另一个函数的参数的时候就把当前函数的功能带过去了(只要调用就行,和一般调用的区别就是这是软编码的,并不是硬调用的),同时还可以增添一些其他的功能。这就是在不改变原来函数的情况下,我们只需要编写额外的功能在另一个函数中,实现动态“对功能打补丁”的效果。


2. 一般的调用

一般的调用就是硬编码调用,在另一个函数中直接实现对当前函数的调用,这种调用只能实现一对一的调用。

def func1():
    print ("func1")

def func2():
    print("before")
    func1()
    print("after")

我们在func2函数中调用了func1函数,但这是硬编码的,当我们有了另外一个函数func11的时候,我们要想实现func2的之前和之后输出的时候,我们还得写另外一个函数来包装,所以这种方式是一个比较“硬”的编码方式。


3. 面向函数编程思想

既然在python中可以将函数都当做参数传递,那么我们可以尝试着做如下的改变。

def func1():
    print("func1")

def func2():
    print("func2")

def wrapper(func):
    print("before")
    func()
    print("after")
    return func  #这里的return很重要,要不然调用一次wrapper之后原先的func就成了空了
func1=wrapper(func1)
func2=wrapper(func2)
f=wrapper(func1)    #这里f就是func1函数

这里编写了wrapper函数之后,直接将需要“打补丁”的函数当做参数传递进去就可以,注意这里的wrapper最后将传递进来的函数又return回去了,也就是说wrapper只做了增添功能的动作,最后并返回了原先的函数,这样我们使用func1=wrapper(func1)操作的时候,func1还是原来的函数并没有做任何的改变,如果最后没有这一句return的话,那么使用func1=wrapper(func1)就对func1进行了重写,重写的结果是func1变成了空,这不是我们想要的结果,所以一定要记住这个返回语句。

我们可以这样写,但是这样写并不能体现出python的牛逼来,所以python中有一个语法糖是这么实现的。我们称它为装饰器(decorator)。


4. 神奇的decorator

python使用装饰器将上面的函数重写变成了另一种十分简洁的方式:

def wrapper(func):
    print("before")
    func()
    print("after")
    return func

@wrapper
def f()
    print("call f")

如果你将上面的脚本保存成python文件并运行你可以看到输出以下内容:

before
call f
after

其实装饰器此时会将代码转换成:f=wrapper(f),完成的实际上函数的重写(增添了wrapper的功能),为了保持原来的函数的本质,此时一定不要忘记在wrapperreturn原来的函数。但是你会发现上面的脚本的输出方式并不是我们预计的输出结果,脚本首先自动输出了wrapper的结果,当我们显示调用的时候却只保持了原来函数的输出。我们分析代码,关键字@就是一个函数重写语句,相当于f=wrapper(f)所以此时调用了wrapper函数并在wrapper内部调用了f函数。此时完成了f的重写,还是原来的函数功能。

我们希望函数在加了装饰器之后能够保持住“打补丁”之后的升级功能,并不是指调用一次。一个自然而然的做法就是将wrapper函数在封装一层,函数重写的时候并不是重写的原来的函数,而是“打补丁”之后的函数,返回的是一个函数句柄,如果我们不去显示调用的话,不会执行任何功能。


5. 第一个具有实际功能的decorator

我们将装饰器再封装一层。返回的是真实功能的函数句柄。这样既能保持升级后的函数所有功能,还能防止自动执行封装器。

def wrapper(func):
    def inner():
        print("before")
        func()
        print("after")
    return inner

#这里相当于 f=wrapper(f),此时f重写为inner的函数句柄
@wrapper
def f():
    print("call f")

f()  #如果不显示的执行的话,装饰器并没有任何执行过程

这样上面写的封装器在内部又封装了一层,返回是一个inner函数的句柄,只要不显示的调用就不会执行。

同时,多个装饰器还可以叠加使用

def wrapper1(func):
    def inner():
        print("before wrapper 1")
        func()
        print("after wrapper 2")
    return inner

def wrapper(func):
    def inner():
        print("before wrapper 2")
        func()
        print("after wrapper 2")
    return inner

@wrapper1
@wrapper2
def f():
    print("call f")

f()

[out]:before wrapper 1
      before wrapper 2
      call f
      after wrapper 2
      after wrapper 1

6. 带参数函数的装饰器

上面的装饰器的例子是最简单的例子,并没有牵扯到任何的参数信息。但是一般的函数都是带着参数的,让我们看看带参函数怎么写装饰器。

首先我们分析,之前的wrapper函数返回的是inner函数的函数句柄,inner函数会在装饰器执行的时候重写到实际的函数中,所以说可以将原先函数的参数交给inner函数来作为传递的桥梁。

def wrapper(func):
    def inner(a,b):
        print("before wrapper")
        func(a,b)
        print("after wrapper")
    return inner

@wrapper
def f(a,b):
    print ("call f: a+b=%d"% (a+b) )

f(2,3)

上面的装饰器相当于f=wrapper(f)=inner,然后可以将参数再加进去,就"相当于"参数传递了。


7. 参数个数不确定

有的时候我们在写装饰器之前是不知道将要装饰的函数到底有几个参数的,所以就不能使用基于位置的参数表示方式,我们使用(*args, **kwargs)来自动适应变参和命名参数

#coding:utf-8
def wrapper(func):
    def inner(*args, **kwargs):
        print("before %s"%func.__name__)
        result=func(*args,**kwargs)
        print("result of %s is %d"%(func.__name__,result))
    return inner

@wrapper
def f1(a,b):
    print("call f1")
    return a+b
@wrapper
def f2(a,b,c):
    print("call f2")
    return a+b+c

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

推荐阅读更多精彩内容

  • Python进阶框架 希望大家喜欢,点赞哦首先感谢廖雪峰老师对于该课程的讲解 一、函数式编程 1.1 函数式编程简...
    Gaolex阅读 5,484评论 6 53
  • 要点: 函数式编程:注意不是“函数编程”,多了一个“式” 模块:如何使用模块 面向对象编程:面向对象的概念、属性、...
    victorsungo阅读 1,443评论 0 6
  • 两本不错的书: 《Python参考手册》:对Python各个标准模块,特性介绍的比较详细。 《Python核心编程...
    静熙老师哈哈哈阅读 3,350评论 0 80
  • 微信名:洋洋洒洒 作业1:好好回想一下,小时候呆呆看过什么,或者有什么经常见到的事情(如广场舞),挑选一个印象或一...
    奥利维亚的畅想生活阅读 370评论 3 0
  • 1. “我后天有个酒要喝,你自己吃饭吧。” “操,那家酒楼的菜难吃的一逼,酒更难喝。”胖子坐在客厅啐了一口瓜子壳,...
    谷谷阅读 671评论 13 7