深入理解Python装饰器(一)

目录摘要
零、前言
一、一段tornado的代码——接触装饰器
二、python装饰器要素

零、前言

本文一开始是在codeview时临场写下的笔记,所以开篇有tornado的源码的摘要,例证中也贴了一些robotframework的源码 。然后逐步补充成文的。可能作为入门文章有些欠直接。
在补缀成文的过程中,我围绕装饰器的是什么装饰器有什么用途以及装饰器应当怎么写三个问题力图把python这个语法特性让自己深入掌握,也希望能帮助有缘人。

错漏之处在所难免,本文也在不断补充修正之中。
笔者每有新体会会来回看文章看看以前有没有理解错误的地方。

关于文章的结构,我没有惯例式地使用一二三来“条分缕析”,因为我写的时候一直围绕上述的三大问题,一二三太丑。

一、一段tornado的代码——接触装饰器

装饰器的数学模型是复合函数 :

(f * g) (x) = f(g(x))

下面是摘自tornado中的一段代码:

class ComposeHandler(BaseHandler):
    @administrator
    def get(self):
        key = self.get_argument("key", None)
        entry = Entry.get(key) if key else None
        self.render("compose.html", entry=entry)

administrator 是一个装饰器,在给服务器发出get请求,路由到这个ComposeHandler的时候,需要先运行administrator的逻辑。这个类似Java中的拦截器语法(AOP)。
administrator也是一个函数,它的样子是这样的:

def administrator(method):
    """Decorate with this method to restrict to site admins."""
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        if not self.current_user:
            if self.request.method == "GET":
                self.redirect(self.get_login_url())
                return
            raise tornado.web.HTTPError(403)
        elif not self.current_user.administrator:
            if self.request.method == "GET":
                self.redirect("/")
                return
            raise tornado.web.HTTPError(403)
        else:
            return method(self, *args, **kwargs)
    return wrapper

这里使用了一个闭包函数wrapper(可调用的对象),它在函数内部被定义,当返回给上层调用者时,这个wrapper会在get函数调用之前运行起来,从而起到装饰的作用。回想一下,装饰模式的表述:用对客户透明的方式给一个对象动态地增加功能。python的装饰器展示的也是这种思想,但是python的装饰器强调的更多还是AOP得概念,用简洁的@语法抽象出公共功能,避免了一段“公共的客户代码”散落在代码各处的毛病,避免了维护的困难。
由于装饰器可以重叠装饰,所以它也符合了“动态增加功能的特点”,所以我把它理解成装饰模式的一种应用。

二、python装饰器要素

总结一下python装饰器的几大要点:
1、装饰器是一种特殊的函数
2、装饰器的参数通常有一个是函数对象
3、装饰器要返回一个函数对象

  • 第一点是因为
@deco
def f():
...

的调用等价于 g = deco(f) ,g(); deco必须是一个可调用的对象。

  • 第二点是因为装饰器的功能一般是对被修饰的对象的一种操作,所以它会接收一个函数对象进行操作,但这一点不是必须的,看这段代码(来自robotframework的对函数进行keword修饰的代码) 参数可以是其他数据类型:
@not_keyword
def keyword(name=None, tags=(), types=()):
    """
    [此处省略了源代码的文档说明]
       
    """
    if inspect.isroutine(name):
        return keyword()(name)

    def decorator(func):
        func.robot_name = name
        func.robot_tags = tags
        func.robot_types = types
        return func

    return decorator

第一个参数name可以是一个字符串,这种写法的keyword的用法可以有如下一些方式:

        @keyword
        def example():    
            # ...

        @keyword('Login as user "${user}" with password "${password}"',
                 tags=['custom name', 'embedded arguments', 'tags'])
        def login(user, password):
            # ...

        @keyword(types={'length': int, 'case_insensitive': bool})
        def types_as_dict(length, case_insensitive):
            # ...

        @keyword(types=[int, bool])
        def types_as_list(length, case_insensitive):
            # ...

        @keyword(types=None])
        def no_conversion(length, case_insensitive=False):
            # ...
  • 第三点是最重要的一点,装饰器一定要最终返回一个函数对象。因为不这么做的话,装饰器默认返回一个None,使用语法糖 @decorator的时候,解释器会抛出一个TypeError
TypeError: 'NoneType' object is not callable

第三条准则还需要注意的点是,最终返回,形如如下的代码:

def mydecorator(funcname, tags=[]):
    def wrapper(func):
        print("exe something")
    return wrapper

可以用作函数的装饰器吗?答案是否定的,**因为函数的最终返回还是None **,除非改成下面的样子:

def mydecorator(funcname, tags=[]):
    def wrapper(func):
        print("exe something")
        return func  #显示得返回函数对象,这个函数对象在使用语法糖 @mydecorator def f():.... 时将会是被修饰函数f自己
    return wrapper

那么什么是装饰器?python的文档给了一个言简意赅的定义: https://docs.python.org/zh-cn/3/glossary.html#term-decorator

返回值为另一个函数的函数,通常使用 @wrapper 语法形式来进行函数变换

装饰器语法只是一种语法糖,以下两个函数定义在语义上完全等价【文档中举的一个例子】:

def f(...):
    ...
f = staticmethod(f)
@staticmethod
def f(...):
    ...

函数式的装饰器例子:
假设我们要给一个模块里的函数都加上追踪调试信息,函数进入时输出一段时间戳。使用装饰器,先定义一个打印调试追踪信息的装饰器。

def log_deco(method):
    def wrapper():
        print time.strftime("%Y-%m-%d %H:%M:%S"),
        print "Call funcion : %s" % method.__name__
        return method() # method在这里调用 也可以不写return
    return wrapper

@log_deco
def func1():
    print "Processing something..."
    print "Calculating something ..."

@log_deco
def func2():
    print "Processing something2..."
    print "Calculating something2..."

if __name__ == '__main__':
    func1()
    func2()

样例输出:

2017-09-19 11:20:41 Call funcion : func1
Processing something...
Calculating something ...
2017-09-19 11:20:41 Call funcion : func2
Processing something2...
Calculating something2...

调用func1()的时候,会首先执行w = log_doco(func1),log_doco在内部定义了wrapper, 如果考察一下 w会发现,它就是log_deco内部定义的内部函数wrapper,只需要验证一下它的name和对象id就行;
@log_deco def ... 等价于

w = log_demo(func1)
w()

w()实际上就是调用wrapper()

内建装饰器 classmethod和staticmethod

classmethod和staticmethod是python的内建方法。它们是都是类。
在构造器中接受一个函数对象作参数,并返回一个函数对象的实例。

怎么在python里面写静态方法?函数声明的前面用 @staticmethod装饰一下就行。

class Foo:
    @staticmethod
    def func1():
          print("demo func1..")

调用时可以直接用类名访问,或者实例访问也行。静态方法是一个与实例无关的方法,和Java、C++的静态方法是一样的概念。

python中的类方法和静态方法有什么区别?

1、静态方法的概念和Java、C++一样,它不和类、类的实例绑定,通过类名调用,也可通过实例调用,结果都是一样的;
2、类方法有一个cls的实参传入,代表与方法绑定的类对象;类方法与类绑定,而一般的方法是与实例绑定。类方法可以通过类名调用,也可以通过类的实例调用。

三、装饰器的几种实现写法

写一个装饰器的目的本身是为了吃“语法糖”,让我们能在程序各处潇洒地用@wrapper的语法,那么围绕这个@wrapper的语法,我们总结一下有哪些可以实现装饰器的实践方式。

上面提到了,装饰器的定义是一个返回值为另一个函数的函数,我们在实现装饰器的时候,紧紧围绕这个点,然后作几个变化就会有多种写法

做一个脑图,大概列举一下装饰器的实现形式


装饰器的函数形式.png

Invalid的情形以举了例子,下面着重举出valid的情形中,类形式的装饰器,以及最常见的返回内部定义函数的情况(实际上,上面的keword装饰器就是属于这种)

1. 使用可调用对象做装饰器
2. 返回值在函数内部定义
3. 类装饰器

类的 __call__方法被定义时,这个类的实例也是可被调用的。
例子,将一个类的定义实现 _call_ 协议,那么所有该类的实例都是一个可调用对象。

class CallDemo:
    def __init__(self):
        print "This is a class for callable object"

    def __call__(self, *args):
        print("Call ... run here ...")
        return 100
    
c = CallDemo()
print(c())  # 实例调用

如果没有 重写 __call__方法 ,实例调用 c( ) 会导致解释器会报告一个AttributeError: CallDemo instance has no __call__ method 的错误

借助 __call__ ,写一个类装饰器, 为我的函数盖一个戳子:

class sign:
    def __init__(self, func):
        self.func = func  #  被修饰的函数对象从这里传进来

    def __call__(self, *args):
        print "== Dong hua's func =="
        self.func()  #  函数调用被移到了这里
        print "== leave func == "

@sign
def foo():
    print ("... This is foo function ...")

foo()

这里的类装饰器,需要在构造器上接收一个函数对象做参数,并重新定义 __call__函数。当调用foo()函数的时候,相当于

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

推荐阅读更多精彩内容