Python 装饰器最佳实践

Python 的 Decorator在使用上和Java/C#的Annotation很相似,就是在方法名前面加一个@XXX注解来为这个方法装饰一些东西。但是,Java/C#的Annotation很让人望而却步,要使用它,需要先了解一堆Annotation的类库文档,让人感觉就是在学另外一门语言。
而Python使用了一种相对于Annotation来说非常优雅的方法,这种方法不需要我们去掌握Annotation的各种类库规定,完全就是语言层面的玩法:一种函数式编程(“描述我们想干什么,而不是描述我们要怎么去实现”的编程方式)的技巧。

装饰器基础知识

函数对象

要理解decorator,首先必须理解函数在Python中的作用。这有重要的影响。让我们用一个简单的例子来看看为什么:

def shout(word='yes'):
    return word.capitalize()

print(shout())
# outputs : 'Yes'

scream = shout

# 注意,我们不使用括号:我们没有调用函数,而是将`shout`函数赋给变量`scream`,
# 这意味着您可以从`scream`中调用`shout`:

print(scream())
# outputs : 'Yes'

# 不仅如此,这还意味着您可以删除`shout`,并且该函数仍然可以从`scream`调用

del shout
try:
    print(shout())
except NameError as e:
    print(e)
    #outputs: "name 'shout' is not defined"

print(scream())
# outputs: 'Yes'

Python函数的另一个有趣的特性是 可以在另一个函数中定义它们!

def talk():

    # 您可以在`talk`中动态定义一个函数: ...
    def whisper(word='yes'):
        return word.lower()

    # ... 并立即使用!

    print(whisper())

# 您每次调用`talk`时都会定义`whisper`,然后`whisper`在`talk`中被调用。
talk()
# outputs: "yes"

# 但是`talk`之外不存在`whisper`:

try:
    print(whisper())
except NameError as e:
    print(e)
    # outputs : "name 'whisper' is not defined"

函数引用

你已经知道函数是对象,因此,函数:

  • 可以给变量赋值
  • 可以在另一个函数中定义

这意味着一个函数可以返回另一个函数。看一下!

def getTalk(kind='shout'):

    # 我们动态地定义函数
    def shout(word='yes'):
        return word.capitalize()

    def whisper(word='yes'):
        return word.lower()

    # 然后我们返回其中一个
    if kind == 'shout':
        # 我们不用'()'。我们没有调用函数;相反,我们返回函数对象
        return shout  
    else:
        return whisper

# 获取函数并将其赋值给变量
talk = getTalk()      

# 你可以看到`talk`在这里是一个函数对象:
print(talk)
#outputs : <function shout at 0xb7ea817c>

print(talk())
# outputs : 'Yes'

# 你甚至可以直接使用它:
print(getTalk('whisper')())
# outputs : 'yes'

既然你可以返回一个函数,那么你也可以将函数作为参数传递给另一个函数:

def doSomethingBefore(func): 
    print('I do something before then I call the function you gave me')
    print(func())

doSomethingBefore(shout)
# outputs: 
# I do something before then I call the function you gave me
# Yes

现在您已经具备了了解装饰器的一切条件。在Python中,函数是一类对象,这意味着:

  • 函数是对象,它们可以被引用,传递给变量并从其他函数返回。
  • 可以在另一个函数中定义函数inner function ,也可以将其作为参数传递给另一个函数。

手动实现装饰器

您已经看到函数与Python中的任何其他对象一样,现在让我们手动实现一个装饰器,来看一下Python装饰器的魔力。

# 装饰器是期望另一个函数作为参数的函数
def my_decorator(my_func):

    # 在内部,decorator动态地定义了一个函数:wrapper。
    # 这个函数将被封装在原始函数上,这样它就可以在原始函数之前和之后执行代码。
    def my_wrapper():

        # 在调用原始函数之前,将需要执行的代码放在这里
        print('Before the function runs')

        # 调用这里的函数(使用括号)
        my_func()

        # 将您希望在调用原始函数后执行的代码放在这里
        print('After the function runs')

    # 此时,`my_func`还没有被执行。
    # 我们返回刚刚创建的`my_wrapper`函数。
    # `my_wrapper`包含`my_func`函数和要执行的前后代码。
    return wrapper

# 现在假设您创建了一个不想再做任何修改的函数
def my_func():
    print('I am a stand alone function, don’t you dare modify me')

my_func() 
# outputs: I am a stand alone function, don't you dare modify me

# 只要将它传递给装饰器,它就会动态地将它包装在您想要的任何代码中,并返回一个准备使用的新函数:

my_func_decorator = my_decorator(my_func)
my_func_decorator()
# outputs:
# Before the function runs
# I am a stand alone function, don't you dare modify me
# After the function runs

现在,您可能希望每次调用my_func时,my_func_decorator会被调用。这很简单,只需用my_decorator返回的函数覆盖my_func

my_func = my_decorator(my_func)
my_func
# outputs:
# Before the function runs
# I am a stand alone function, don’t you dare modify me
# After the function runs

装饰器揭秘

使用装饰器语法实现前面的例子:

@my_func_decorator
def my_another_func():
    print('Leave me alone')

my_another_func()  
# outputs:  
# Before the function runs
# Leave me alone
# After the function runs

是的,就是这么简单。根据我们前面的铺垫,您应该一下就能理解装饰器的语法,@decorator只是一个快捷方式:

my_another_func = my_decorator(my_another_func)

decorator只是decorator设计模式的python变体。Python中嵌入了一些经典的设计模式来简化开发(比如迭代器、生成器,感兴趣的同学可以看一下我前面关于迭代器和生成器的文章:Python中的三个“黑魔法”与“骚操作”

嵌套装饰器

当然,装饰器也可以嵌套:

def hello(func):
    def wrapper():
        print("Hello")
        func()
    return wrapper

def welcome(func):

    def wrapper():
        print("Welcome")
        func()
    return wrapper

def say():
    print("Good")
    
say = hello(welcome(say))
say()
# outputs:
# Hello 
# Welcome
# Good

# 使用Python decorator语法:
@hello
@welcome
say()
# outputs:
# Hello 
# Welcome
# Good

设置decorator的顺序很重要:decorator按照它们被列出的顺序执行。

带参装饰器

我们还可以装饰一个带有参数的函数。我们可以在包装器函数wrapper中使用*args**kwargs接收这些参数。

# 你只需要让`wrapper`传递参数:
def say(func):

    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
    return wrapper
    
# 因为在调用`decorator`返回的函数时,调用的是包装器`wrapper`,所以向包装器传递参数将让包装器将参数传递给修饰的函数
@say
def greet(name):
    print("Hello {}".format(name))

greet("xiaojing")
# outputs: Hello xiaojing

装饰器高手进阶

现在,你已经掌握了装饰器的概念和装饰器的基本用法,可以高兴地离开了,或者你也可以留下多动会脑子,看看装饰器的高级用途。

Introspection

在Python中,自省是指对象在运行时了解其自身属性的能力。例如,函数知道自己的name和doc。

print(greet.__name__)
# outputs: wrapper

但我们期望输出greet,而不是函数被装饰后丢失函数原始的信息。要解决这个问题,decorator应该在wrapper上使用@functools.wrapper包装器函数,它将保留关于原始函数的信息。

import functools
import time


def timer(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print("Finished {} in {} s".format(repr(func.__name__), round(run_time, 3)))
        return value

    return wrapper
    
@timer
def doubled_and_add(num):
    res = sum([i*2 for i in range(num)])
    print("Result : {}".format(res))

doubled_and_add(100000)
doubled_and_add(1000000)
# outputs:
# Result : 9999900000
# Finished ‘doubled_and_add’ in 0.0119 s
# Result : 999999000000
# Finished ‘doubled_and_add’ in 0.0897 s

装饰类

在类上使用装饰器有两种不同的方法。装饰类的方法或装饰整个类。

内置类装饰器

Python中内置的一些常用装饰器是@classmethod@staticmethod@property@classmethod@staticmethod装饰器用于在类名称空间内定义未连接到该类的特定实例的方法。@property装饰器用于自定义类属性的gettersetter方法。

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value >= 0:
            self._radius = value
        else:
            raise ValueError("Radius must be positive")

    @property
    def area(self):
        return self.pi() * self.radius**2

    def cylinder_volume(self, height):
        return self.area * height

    @classmethod
    def unit_circle(cls):
        """工厂方法 创建一个半径为1的圆"""
        return cls(1)

    @staticmethod
    def pi():
        return 3.1415926535

在这个类中:

  • .cylinder_volume() 是一个普通方法。
  • .radius 是一个可变的属性:它可以被设置为不同的值。但是,通过定义setter方法,我们可以进行一些错误验证,来确保它不会被设置为负数。
  • .area是一个不可变的属性:没有.setter()方法的属性是不能更改的。
  • .unit_circle() 是类方法。它不局限于一个特定的圆实例。类方法通常用作工厂方法,可以创建类的特定实例。
  • .pi() 是静态方法,它并不真正依赖于Circle类。静态方法可以在实例或类上调用。

控制台测试:

>>> c = Circle(5)
>>> c.radius
5

>>> c.area
78.5398163375

>>> c.radius = 2
>>> c.area
12.566370614

>>> c.area = 100
AttributeError: can't set attribute

>>> c.cylinder_volume(height=4)
50.265482456

>>> c.radius = -1
ValueError: Radius must be positive

>>> c = Circle.unit_circle()
>>> c.radius
1

>>> c.pi()
3.1415926535

>>> Circle.pi()
3.1415926535

装饰方法

Python的一个妙处是方法和函数实际上是一样的。惟一的区别是,方法期望它们的第一个参数是对当前对象(self)的引用,在这里,我们使用上面刚刚创建的计时器装饰器,我们还是举个例子简单过一下:

class Calculator:

    def __init__(self, num):
        self.num = num

    @timer
    def doubled_and_add(self):
        res = sum([i * 2 for i in range(self.num)])
        print("Result : {}".format(res))

c = Calculator(10000)
c.doubled_and_add()
# outputs:
# Result : 99990000
# Finished 'doubled_and_add' in 0.001 s

装饰整个类

@timer
class Calculator:

    def __init__(self, num):
        self.num = num
        import time
        time.sleep(2)

    def doubled_and_add(self):
        res = sum([i * 2 for i in range(self.num)])
        print("Result : {}".format(res))

c = Calculator(100)
# outputs: Finished 'Calculator' in 2.001 s

装饰类并不装饰它的方法。在这里,@timer只测量实例化类所需的时间。

带参数的装饰器

注意,带参数的装饰器和上面提到的带参装饰器可不是一回事儿。带参装饰器是装饰带参数的函数的装饰器,这个参数是函数的参数,通过包装器传递给函数。而带参数的装饰器是带有参数的装饰器,这个参数是装饰器自身的参数,是不是有点晕了,别急,我们一起看下例子就懂了:

def repeat(*args_, **kwargs_):

    def inner_function(func):

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(args_[0]):
                func(*args, **kwargs)
        return wrapper

    return inner_function


@repeat(4)
def say(name):
    print(f"Hello {name}")

say("World")
# outputs:
# Hello World
# Hello World
# Hello World
# Hello World

有状态的装饰器

我们可以使用一个装饰器来跟踪状态。作为一个简单的示例,我们将创建一个decorator来计算函数被调用的次数。

def count_calls(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.num_calls += 1
        print(f"Call {wrapper.num_calls} of {func.__name__!r}")
        return func(*args, **kwargs)

    wrapper.num_calls = 0
    return wrapper


@count_calls
def say():
    print("Hello!")

say()
say()
say()
say()
print(say.num_calls)
# outputs:
# Call 1 of 'say'
# Hello!
# Call 2 of 'say'
# Hello!
# Call 3 of 'say'
# Hello!
# Call 4 of 'say'
# Hello!
# 4

对函数的调用数量存储在包装器函数上的函数属性num_calls中。

类装饰器

注意,和刚才带参数的装饰器和带参装饰器类似,类装饰器装饰类的装饰器也是完全不同的两个概念。装饰类的装饰器是用来装饰类的方法和整个类的装饰器,是对类的装饰。而类装饰器是用类来作为函数的装饰器,类本身作为装饰器对函数进行装饰。我在说What?这次我自己都要绕晕了,还是借代码来翻译程序吧,例子一看您就明白了。

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)


@CountCalls
def say():
    print("Hello!")

say()
say()
say()
say()
print(say.num_calls)
# outputs:
# Call 1 of 'say'
# Hello!
# Call 2 of 'say'
# Hello!
# Call 3 of 'say'
# Hello!
# Call 4 of 'say'
# Hello!
# 4

维护状态的最佳方法是使用类。如果我们想使用class作为装饰器,则需要将func在其.__init__()方法中作为参数。此外,该类必须是可调用的,以便它可以代表被装饰的函数。对于可调用的类,我们需要实现特殊的.__call__()方法。

带参数的基于类的装饰器

我保证这是最后一个!解释不动了,直接上代码,代码是对程序语言最好的解释语言。

class ClassDecorator(object):

    def __init__(self, arg1, arg2):
        print("Arguements of decorator %s, %s" % (arg1, arg2))
        self.arg1 = arg1
        self.arg2 = arg2

    def __call__(self, func):
        functools.update_wrapper(self, func)

        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper

@ClassDecorator("arg1", "arg2")
def print_args(*args):
    for arg in args:
        print(arg)

print_args(1, 2, 3)
# outputs:
# Arguements of decorator arg1, arg2
# 1
# 2
# 3

总结

Python 的 Decorator是想要对一个已有的模块做一些“修饰工作”,所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去,上面我们用了大量的例子来说明了这一点。
推荐你们几篇比较不错的英文文章(锦上添花,不读也可):

终于完事儿了,恭喜你们,太牛了!如果本文对大家有帮助,欢迎大家对本文点赞收藏评论或关注我的主页,我会不定期更新当下主流技术文章。

相关文章

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