Python进阶 - 装饰器

函数进阶知识

函数名只是一个指向函数的变量

在python中,一切皆对象。函数名只是一个指向函数的变量,为了验证这一点,我们可以查看修改和删除函数名对程序的影响:

def someFunc():  # someFunc只是一个变量名,它指向了我们创建的函数
    print("Now in a function")


if __name__ == '__main__':
    anotherFunc = someFunc  # 用另一个变量名指向刚创建的函数
    anotherFunc()  # 输出Now in a function
    # 删除了一个指向函数的变量名,对函数本身并没有影响;
    # 因为还有一个变量anotherFunc指向函数,因此函数本体并不会被回收
    del someFunc
    anotherFunc()  # 还是输出Now in a function

可以看到,在python中函数名和一个普通变量一样,只不过它指向了一组语句。

嵌套函数和变量作用域

在函数内是可以定义另一个函数的,这叫做嵌套函数。作用域是程序运行时变量可被访问的范围,定义在函数内的变量是局部变量,局部变量的作用范围只能是函数内部范围内,它不能在函数外引用。根据这个原理,对于嵌套函数来说,在内层的函数可以访问外层函数定义的变量。

如下例所示:

def nested_func():
    msg = "This is a message"

    # 在函数内部定义一个函数,打印消息
    def print_message():
        # 在嵌套函数内层可以访问外层函数的变量
        print(msg)

    # 调用打印消息的函数
    print_message()  # output: This is a message


if __name__ == '__main__':
    nested_func()

将函数作为返回值

函数由于是一个对象,它是可以作为另一个函数的返回值或者作为参数传递给其他函数。

def nested_func(msg):
    # 在函数内部定义一个函数,打印消息
    def print_message():
        # 在嵌套函数内层可以访问外层函数的变量
        print(msg)

    return print_message


if __name__ == '__main__':
    msg = "This is a message"
    aFunction = nested_func(msg)  # 将aFunction指向了nested_Func(msg)的返回值,即print_message
    aFunction()  # 等同于调用了print_message()

这里将返回的函数赋值给另一个函数,这样可以实现对内部函数从外部进行传值。

闭包(Closure)

什么是闭包

两个嵌套的函数,内部函数使用外部函数的变量。这套变量+内部函数整体,就叫做闭包。

闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

为什么需要闭包

刚才提到过,闭包可以实现对内部函数从外部进行传值。实现这种功能,有两套方案:使用类和使用闭包。使用闭包的方案我们前面已经介绍过,我们下面看看如何用类实现这个功能:

class someClass(object):
    def __call__(self, msg):
        print(msg)


if __name__ == '__main__':
    msg = "This is a message"
    aFunction = someClass()
    aFunction(msg) # 输出 This is a message

这里就实现了和上面用嵌套函数一样的功能,在这里这个aFunction虽然是一个实例对象,但是我们定义了__call__方法之后,它也可以像函数一样被调用。

这种实现方式的缺陷在于,python3的类默认会继承object(Python2 略有不同,继承了object的叫做新式类,没有继承的叫做经典类),它里面自带了一系列我们用不到的方法,会占用相对较大的空间。

print(dir(aFunction))
# 输出结果:
# ['__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

而闭包则更加精简,占用的空间更小。

装饰器的内部执行原理

什么是装饰器

装饰器就是一种闭包,只是这个闭包的函数名等于原先的函数名,从而改变了原先函数的指向。

def my_decorator(func):
    def print_msg():
        print("new message has been added!")
        func()

    return print_msg


def plain_print_msg():
    print("entering plain print msg")


if __name__ == '__main__':
    # plain_print_msg作为实参被传给my_decorator
    # aFunc指向my_decorator的返回值,也就是print_msg
    aFunc = my_decorator(plain_print_msg)
    # 相当于在指向print_msg,其内部的func = plain_print_msg
    aFunc()
    print("-" * 10)

    # 将原先的函数名plain_print_msg直接指向my_decorator的返回值
    # 可以看到和上面部分的代码实现的功能是相同的
    plain_print_msg = my_decorator(plain_print_msg)
    plain_print_msg()

可以看到,执行下面两行代码之后,函数名称并没有变, 但是却指向了一个新的函数,在新的函数内,除了执行原函数之外,还加了一些其他的功能,这就是装饰器的实现原理。

在实际使用装饰器时,我们并不需要手动修改函数名的指向,而是可以由@运算符来进行代劳,上面的代码可以改写如下:

def my_decorator(func):
    def print_msg():
        print("new message has been added!")
        func()

    return print_msg


@my_decorator
def plain_print_msg():
    print("entering plain print msg")


if __name__ == '__main__':
    plain_print_msg()

这里的@my_decorator和我们上面手动实现的代码功能是一样的。

为什么需要使用装饰器

那么在开发过程中进行功能扩充时,为什么不直接修改函数源码,而要使用装饰器呢?

开放封闭原则:已经实现的功能代码不允许修改,但是可以被扩展。也即已经实现的功能代码块封闭,而对扩展开发开放。这是因为在比较大的系统中,可能有很多模块使用同一个函数,如果为了修改功能,直接对函数进行修改,那么其他地方对函数的引用可能会出错。而使用了装饰器之后,可以在装饰器内梳理代码逻辑,这样不该动其他地方的代码也可以正常加上新功能。

装饰器什么时候开始生效的

装饰器的生效不会等到函数进行调用,而是在解释器读到装饰符开始,就会对函数进行装饰。为了说明这一点,我们可以看下面这一段代码:

def my_decorator(func):
    print("entering my decorator!")

    def print_msg():
        print("new message has been added!")
        func()

    return print_msg


@my_decorator
def plain_print_msg():
    print("entering plain print msg")


if __name__ == '__main__':
    pass # 注意这里并没有执行函数调用

输出结果:

entering my decorator!

可以看到函数并没有被调用,但是装饰器已经被加装在函数之上了。

对带参数的函数进行装饰

通过上面对函数的学习,我们已经知道装饰器的工作原理了,因此如果需要对带参数的函数进行装饰,无非就是在闭包的内层函数上加上相应的参数。示例如下:

import time


def time_wrapper(func):
    def timer(cnt):
        start_time = time.time()
        func(cnt)
        end_time = time.time()
        print("time consumed by %s: %s second" % (func.__name__, str(end_time - start_time)))

    return timer


@time_wrapper
def count1(cnt):
    lst = []
    for i in range(cnt):
        lst.insert(0, i)


@time_wrapper
def count2(cnt):
    lst = []
    for i in range(cnt):
        lst.append(i)


if __name__ == '__main__':
    count1(10000)
    count2(10000)

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

在了解了装饰器的原理之后,我们就应该知道如果原函数带有返回值的话,我们需要在装饰器的什么地方进行返回了:

def my_decorator(func):
    print("entering my decorator!")

    def print_msg():
        print("new message has been added!")
        return func() # 将函数的返回值作为返回值

    return print_msg


@my_decorator
def plain_print_msg():
    print("entering plain print msg")
    return "OK"  # 函数带有返回值,指示打印状态


if __name__ == '__main__':
    # 这里给的实际上是print_msg()的返回值,也就是func()
    # 形参func = plain_print_msg,因此返回值和原函数plain_print_msg的返回值是相同的
    ret = plain_print_msg() 
    print(ret)

多个装饰器装饰同一个函数

当有多个装饰器装饰同一个函数时,最靠近函数的(位于程序最下端的)装饰器最先装饰,然后依次由近及远进行装饰。而调用闭包时,由远及近进行调用。

def decorator1(func):
    print("entering decorator1")

    def add_msg():
        print("using decorator1: add_msg")
        func()

    return add_msg


def decorator2(func):
    print("entering decorator2")

    def add_another_msg():
        print("using decorator2: add_another_msg")

    return add_another_msg


@decorator1  # 距离函数较远的装饰器后装饰,先调用(包在外层)
@decorator2  # 距离函数较近的装饰器先装饰,后调用(包在内层)
def plain_print_msg():
    print("entering plain print msg")
    return "OK"  # 函数带有返回值,指示打印状态


if __name__ == '__main__':
    plain_print_msg()

输出的结果:

entering decorator2
entering decorator1
using decorator1: add_msg
using decorator2: add_another_msg

用类实现装饰器

明白了装饰器的实现原理之后,就可以知道用类也是可以实现装饰器的。这就是我们之前讲的用类实现闭包功能的@装饰符版本。

class Test():
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print("----调用装饰器----")
        self._func(*args, **kwargs)


@Test
def my_fun(a, b, c):
    print(a, b, c)


if __name__ == '__main__':
    my_fun(10, 20, 'main')

输出结果:

----调用装饰器----
10 20 main

带参数的装饰器

当装饰器带有参数时,调用过程分为两步:

  • 调用装饰器函数(闭包外层的函数)并将参数作为实参传递
  • 用上一步的返回值当作装饰器对函数进行装饰

因此需要给装饰器加上参数时,我们要对闭包进行再一层包装,在最外层传递入装饰器中传进来的参数。

一个对操作进行权限检查的例子如下:

def get_level(level):
    def level_check_wrapper(func):
        def level_check():
            print("正在检查权限:")
            if level == 1:
                print("权限等级为1")
                return func()
            elif level == 2:
                print("权限等级为2")
                return func()

        return level_check

    return level_check_wrapper


@get_level(1)
def low_level_op():
    print("低级权限操作")
    print([i for i in range(5)])


@get_level(2)
def high_level_op():
    print("高级权限操作")
    print([item for item in "abcdefgh"])


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

推荐阅读更多精彩内容