Python装饰器(Decorator)完全指南-高级篇

引言

通过前面两篇文章(前两篇文章见基础篇, 进阶篇),读者们已经了解了到了python中的装饰器背后的实现逻辑,如何理解python中以@标记的带装饰器函数,以及如何构造带参数函数的装饰器和动态生成装饰器。在接下来这篇文章中,我们将应用之前的知识来研究一个比较复杂的问题——如何构造装饰器的装饰器,以及装饰器的最佳实践。

构造装饰器的装饰器

在进阶篇中我们已经知道了如何构造一个能接受任意参数的装饰器函数,同时我们还能够通过构造装饰器工厂的方式来动态根据输入参数生成装饰器。接下来我们将应用这些知识来实现装饰器的装饰器。这一装饰器能够用来装饰其他装饰器函数,从而使得被装饰的装饰器函数能够接收任意输入参数(请认真读一读前面这句逻辑不太直观的句子,确认你已经理解了下面代码的目的)。

这一代码的有用之处在于,我们能够动态地将我们的任意一个装饰器变为一个能够接收参数的装饰器工厂。如进阶篇中所述,由于装饰器的函数签名是固定的——def decorator_func(func_to_decorate),我们无法在使用@调用装饰器函数的时候动态传入参数,所以只能先定义一个装饰器工厂,来替我们接收参数并返回包含了参数的闭环(也就是装饰器)。而下面的装饰器将这一功能抽象了出来,使得其可以复用。

代码如下所示。

def decorator_for_decorator(decorator_to_enhance):
    """
    这一函数用来作为一个装饰器工厂来动态生成装饰器。
    生成的装饰器能够被用来装饰其他装饰器函数,使得被装饰的装饰器函数变为能够任意接收参数的装饰器。
    """
    # 为了实现参数的传递,我们在这里动态生成了一个装饰器工厂,并作为返回值
    # 这一装饰器工厂将返回一个闭环作为装饰器,其中包装了外界传入的装饰器参数
    def decorator_maker(*args, **kwargs):
        def decorator_wrapper(func):
            # 这里使用了闭环来保证参数的传递
            return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper
    return decorator_maker

有了这一装饰器之后,我们就可以动态改变其他装饰器了。

# 注意为了保证我们所包装的装饰器能够正确接收参数,我们需要保证其函数签名包含我们想要传入的参数
# 但这样的装饰器函数是无法直接用来装饰其他函数的
@decorator_for_decorator
def decorator_func(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print('Received arguments as {}, {}'.format(args, kwargs))
        print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
        return func(function_arg1, function_arg2)
    return wrapper

# 此时,我们就可以给我们的装饰器传入参数啦
@decorator_func(3, 5)
def func(function_arg1, function_arg2):
    print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))

func('ice cream', 'pizza')
# output:
# Received arguments as (3, 5), {}
# $3 per ice cream, $5 per pizza, what would you like to have?
# Hello, I would like to have ice cream and pizza

上面的代码可能逻辑上不是那么直观。我们接下来详细分析。

首先我们定义了一个叫做decorator_maker的装饰器函数。这一函数实质上是一个能够返回工厂函数的函数。它返回一个能够返回装饰器的工厂函数。也就是说,被这一装饰器装饰过之后,原有的装饰器函数将变为一个装饰器工厂函数。而这一工厂函数,正如我们在进阶篇中所述,用来接收我们想要传入的参数并通过返回一个动态生成的闭环作为装饰器的方式来实现将参数传入装饰器中的目的。紧接着我们就可以带参数通过调用这个工厂函数的方式来实现装饰一个函数的过程。

通过展开装饰器的方式来理解装饰的过程

进一步地,我们总是可以通过展开装饰器的方式来理解装饰的过程发生了什么。这一方法可以用来分析所有的装饰器装饰过的函数。以上面的代码为例。

# 我们使用@decorator_for_decorator的方法装饰了decorator_func函数。可以展开如下
def decorator_func(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print('Received arguments as {}, {}'.format(args, kwargs))
        print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
        return func(function_arg1, function_arg2)
    return wrapper
decorator_func = decorator_for_decorator(decorator_func)
# 经过上面的步骤,decorator_func引用所指向的其实已经是decorator_for_decorator所返回的decorator_maker函数了。这一函数是一个装饰器工厂函数。

# 紧接着我们带参数调用这一工厂函数(@decorator_func(3,5))并使用其返回的装饰器函数来装饰另一个函数(func)。这一过程展开如下。
true_decorator = decorator_func(3,5)
# 上面的true_decorator引用指向的是decorator_maker所返回的闭环decorator_wrapper。

def func(function_arg1, function_arg2):
    print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))
func = true_decorator(func)
# 到上面这一步为止,我们已经完成了装饰func的任务,并且将参数通过闭环的方式传入了我们所使用的装饰器中。

装饰器的最佳实践

  • 装饰器实在python2.4中被引入的。所以要使用装饰器,需要确保我们使用的python版本>=2.4。
  • 使用装饰器会减慢调用函数的速度。
  • 一旦一个函数被装饰过之后,我们就无法在运行时再调用未装饰过的原函数了。
  • 装饰器事实上只是一个接受函数作为输入的函数,并返回一个对原函数的包装函数。这一包装过程可能会使得debug过程更加复杂和困难。但在2.5(含)之后的python版本中我们可以使用functools.wraps()来降低装饰器对debug的影响。

python从2.5开始引入了functools模块(module),其中包含的装饰器函数functools.wraps()能够保证被装饰函数的函数名,模块名,以及文档字符串(docstring)被传入装饰器返回的包装函数中,从而保证了抛出的错误信息中能够包含正确的函数名,改善了debug体验。如下面的代码所示。

# python的stacktrace信息中包含函数的__name__属性来帮助debug
def foo():
    pass
print(foo.__name__)
# output: foo

# 但是用了装饰器之后,__name__属性会发生变化
def bar(func):
    def wrapper():
        return func()
    return wrapper

@bar
def foo():
    pass
print(foo.__name__)
# output: wrapper

# 通过使用functools来改变这一状况
import functools
def bar(func):
    @functools.wraps(func)
    def wrapper():
        return func()
    return wrapper

@bar
def foo():
    pass

print(foo.__name__)
# output: foo

那么,装饰器到底有什么用呢?

装饰器的用法多种多样。举例来说,我们如果想要扩展一个第三方库中附带的函数的功能,但我们又无法修改该函数源代码的时候,我们就可以使用装饰器来实现这一目的。或者我们在debug的时候,为了避免对源代码进行多次修改,就可以用装饰器来附加我们想要的逻辑。换句话说,我们可以用装饰器实现所谓的“干修改”(Dry Change)。

import time
import functools


def benchmark(func):
    """
    这是一个能够计算并打印一个函数运行时间的装饰器
    """
    def wrapper(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        end_time = time.time()
        print('{} completed in {} seconds'.format(func.__name__,  end_time - start_time))
        return res
    return wrapper


def logging(func):
    """
    这是一个能够附加log功能的装饰器
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print('{} executed with args: {} and kwargs: {}'.format(func.__name__, args, kwargs))
        return res
    return wrapper


def counter(func):
    """
    这是一个能够对函数被调用次数进行计数的装饰器
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print('{} has been called for {} times'.format(func.__name__, wrapper.count))
        return res
    wrapper.count = 0
    return wrapper


@counter
@logging
@benchmark
def reverse_string(string):
    return ''.join(reversed(string))


reverse_string('Tough times do not last, tough people do.')
# output:
# reverse_string completed in 3.814697265625e-06 seconds
# reverse_string executed with args: ('Tough times do not last, tough people do.',) and kwargs: {}
# reverse_string has been called for 1 times
# '.od elpoep hguot ,tsal ton od semit hguoT'

reverse_string('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.')
# reverse_string completed in 5.9604644775390625e-06 seconds
# reverse_string executed with args: ('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.',) and kwargs: {}
# reverse_string has been called for 2 times
# '.esrevinu eht tuoba erus ton ma I dna ;ytidiputs namuh dna esrevinu eht :etinifni era sgniht owT'

实际上,python自身也提供了一些常用的装饰器供大家调用,例如propertystaticmethod,等等。与此同时,一些常用的python后端框架,例如DjangoPyramid也使用装饰器来管理缓存以及视图(view)访问权限等。另外,装饰器有时候也用来在测试中来虚构异步请求。

总之,装饰器在实际开发中可以有许多灵活的应用。读者朋友们今后可以多加尝试。

Reference
文中部分内容翻译自如下文章。翻译部分版权归原作者所有。
https://gist.github.com/Zearin/2f40b7b9cfc51132851a

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