Python装饰器(Decorator)完全指南-进阶篇

Decorator进阶指南

在[python装饰器完全指南基础篇中],我们已经知道了python中的装饰器本质上只是一个接受一个函数对象作为输入,在该函数前后增加一些新逻辑,并返回包装过后的新函数对象的函数。

问题一: 有输入参数的函数的装饰器

在基础篇中,我们所讨论的所有被装饰器装饰的函数都是不接收输入参数的函数。那么对于有输入参数的函数,我们应该如何定义适用于它们的装饰器呢?请看如下代码片段。

def decorator_for_func_with_arguments(func_to_decorate):
    def wrapper_that_passes_through_arguments(arg1, arg2):
        print('I got args! Look: {} {}'.format(arg1, arg2))
        func_to_decorate(arg1, arg2)
    return wrapper_that_passes_through_arguments

@ decorator_for_func_with_arguments
def print_full_name(first_name, last_name):
    print('I am {} {}'.format(first_name, last_name))

print_full_name('Tony', 'Stark')
# output: 
# I got args! Look: Tony Stark
# I am Tony Stark

可以看出,由于装饰器事实上返回了一个新的函数来代替我们需要装饰的函数,所以我们只需要确保在装饰器中返回的新的函数与原函数所接受的参数格式一致即可。

问题二:类方法的装饰器

Python中的类方法事实上与函数一样,只是固定接收当前实例的引用作为第一个参数,一般标记为self。那么能够装饰类方法的装饰器事实上也可以用与问题一中一致的方法实现,只不过我们总是要确保返回的函数所接收的第一个参数也是当前实例的引用(self)即可。如下所示。

def decorator_for_instance_method(method_to_decorate):
    def wrapper(self, bonus):
        # 升职加薪,奖金增加一倍 d=====( ̄▽ ̄*)b
        bonus = bonus * 2
        return method_to_decorate(self, bonus)
    return wrapper

class Salary(object):
    def __init__(self):
        self.base = 666

    @decorator_for_instance_method
    def total_compensation(self, bonus):
        print('Congrats! You got a total compensation of {}'.format(self.base * 12 + bonus))

salary_instance = Salary()
salary_instance.total_compensation(2048)
# output: Congrats! You got a total compensation of 12088

类似地,我们也可以用python中的args和*kwargs来实现一个能够装饰接收任意数目参数函数的装饰器。如下所示。

def decorator_passing_arbitrary_arguments(function_to_decorate):
    def wrapper_with_arbitrary_arguments(*args, **kwargs):
        print('Received arguments as following')
        print(args)
        print(kwargs)

        function_to_decorate(*args, **kwargs)

    return wrapper_with_arbitrary_arguments

@decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print('This function does not have any argument')

function_with_no_argument()
# output:
# Received arguments as following
# ()
# {}
# This function does not have any argument

@decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
    print('This function has arguments')

function_with_arguments(1,2,3)
# output:
# Received arguments as following
# (1, 2, 3)
# {}
# This function has arguments

@decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, name)
    print('{}, {}, {}'.format(a, b, c))
    print('{}'.format(name))

function_with_named_arguments(1, 2, 3, name='robot')
# output:
# Received arguments as following
# (1, 2, 3)
# {'name': 'robot'}
# 1, 2, 3
# robot

class Salary(object):
    def __init__(self):
        self.base = 666

    @decorator_passing_arbitrary_arguments
    def total_compensation(self, bonus):
        print('Congrats! You got a total compensation of {}'.format(self.base * 12 + bonus))

salary = Salary()
salary.total_compensation(2048)
# salary.total_compensation(2048)
# Received arguments as following
# (<__main__.Salary object at 0x1070b5f28>, 2048)
# {}
# Congrats! You got a total compensation of 10040

问题三:给装饰器传入参数

上面我们讨论了装饰器所装饰的函数参数有关的问题。接下来我们讨论如何实现能够接收参数的装饰器。

看过之前文章内容的读者相比已经知道,所谓装饰器其实就是接收一个函数作为输入,并返回另一个函数的函数。这种情况下,由于装饰器的函数签名已经固定,所以我们无法直接传入除输入函数之外的参数。如下面代码所示。

# 装饰器就是接收一个函数作为输入的函数
def my_decorator(func):
    print('This is an ordinary function')
    def wrapper():
        print('This is the wrapper function that will be returned')
        func()

# 只有上述格式签名的函数才能作为装饰器
# 下面代码等于
# lazy_function = my_decorator(lazy_function)
@my_decorator
def lazy_function():
    print('zzzzz')

# output:
# This is an ordinary function

lazy_function()
# output:
# This is the wrapper function that will be returned
# zzzzz

上面代码说明当我们使用@my_decorator的时候,事实上就是执行了lazy_function = my_decorator(lazy_function)。因此在无法直接改变装饰器签名的情况下,我们需要采用一些别的办法来实现我们的目标——实现一个能够返回装饰器的函数来替装饰器接收参数,并使用闭包(对闭包概念不熟悉的读者,请参阅这篇文章
)的方法来将这些参数传递到装饰器中。换句话说,我们需要一个装饰器工厂函数来替我们动态生成装饰器。

首先我们实现一个装饰器工厂函数,如下面的代码所示。

def decorator_maker():
    print('This is a factory generating decorators on the fly. This function is called once we want a fresh decorator.')

    def my_decorator(func):
        print('This is the decorator generated on the fly. This function is called when the decoration happens.')
        # 类似地,我们还是在装饰器中定义一个wrapper还包装原函数
        def wrapper():
            print('This is the wrapper around the decorated function. This function is called once the decorated function is called.')
            return func()

        return wrapper

    print('The decorator function created on the fly is returned.')
    return my_decorator

def func():
    print('This is the function decorated.')

fresh_decorator = decorator_maker()
# output:
# This is a factory generating decorators on the fly. This function is called once we want a fresh decorator.
# The decorator function created on the fly is returned.

func = fresh_decorator(func)
# output:
# This is the decorator generated on the fly. This function is called when the decoration happens.

func()
# output:
# This is the wrapper around the decorated function. This function is called once the decorated function is called.
# This is the function decorated.

基于如上代码,我们可以用更加pythonic的方式去使用这个装饰器工厂函数。如下所示。

@decorator_maker()
def func():
    print('This is the function decorated.')
# output:
# This is a factory generating decorators on the fly. This function is called once we want a fresh decorator.
# The decorator function created on the fly is returned.
# This is the decorator generated on the fly. This function is called when the decoration happens.

func()
# output:
# This is the wrapper around the decorated function. This function is called once the decorated function is called.
# This is the function decorated.

上面的例子说明,我们是事实上可以用一个装饰器工厂返回的函数作为@语法中的装饰器使用。既然如此,我们自然也就可以给这个工厂函数传入参数。如下面代码所示。

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):
    print('This is the decorator factory. Input arguments are: {}, {}.'.format(decorator_arg1, decorator_arg2))

    def my_decorator(func):
        # 利用闭包特性来将工厂函数接收的参数传递给动态生成的装饰器
        print('This is the decorator function created on the fly.')
        print('Arguments are passed in from outer function using closure: {}, {}.'.format(decorator_arg1, decorator_arg2))

        # 注意这里wrapper函数接收的参数是被该装饰器装饰的函数
        # 不要和上面工厂函数接收的参数混淆
        def wrapper(function_arg1, function_arg2):
            print('This is the wrapper around the decorated function.')
            print('This function can access all the variables.')
            print('Variables from the decorator factory: {}, {}.'.format(decorator_arg1, decorator_arg2))
            print('Variables from the decorated function: {}, {}.'.format(function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapper
    return my_decorator

@decorator_maker_with_arguments('Avengers', 'Justice League')
def func(function_arg1, function_arg2):
    print('This is the function decorated.')
    print('It accepts two arguments: {}, {}.'.format(function_arg1, function_arg2))

# output:
# This is the decorator factory. Input arguments are: Avengers, Justice League.
# This is the decorator function created on the fly.
# Arguments are passed in from outer function using closure: Avengers, Justice League.

func('Captain America', 'Bat Man')
# output:
# This is the wrapper around the decorated function.
# This function can access all the variables.
# Variables from the decorator factory: Avengers, Justice League.
# Variables from the decorated function: Captain America, Bat Man.
# This is the function decorated.
# It accepts two arguments: Captain America, Bat Man.

上面例子中,我们将字符串常量作为参数传递给了装饰器工厂函数。与平常的python函数一样,这一函数也可以接收变量作为参数。如下面所示。

a1 = 'Avenagers'
a2 = Justice League'
b1 = 'Captain America'
b2 = 'Bat Man'

@decorator_maker_with_arguments(a1, a2)
def func(function_arg1, function_arg2):
    print('This is the function decorated.')
    print('It accepts two arguments: {}, {}.'.format(function_arg1, function_arg2))

# output:
# This is the decorator factory. Input arguments are: Avengers, Justice League.
# This is the decorator function created on the fly.
# Arguments are passed in from outer function using closure: Avengers, Justice League.

func(b1, b2)
# output:
# This is the wrapper around the decorated function.
# This function can access all the variables.
# Variables from the decorator factory: Avengers, Justice League.
# Variables from the decorated function: Captain America, Bat Man.
# This is the function decorated.
# It accepts two arguments: Captain America, Bat Man.

通过上述讨论,可以看出对于装饰器工厂函数而言,调用它与调用普通函数完全相同。我们甚至也可以使用*args**kwargs来接受可变长度的参数。但有一点需要注意,那就是装饰器装饰函数的过程只发生一次,那就是python编译器import当前代码文件的时候。因此一旦文件已经被import过,那我们就无法修改装饰器工厂接收的参数,也无法修改已经生成的装饰器装饰过的函数了。

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

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

推荐阅读更多精彩内容