谈谈Python中的decorator装饰器,如何更优雅的重用代码

众所周知,Python本身有很多优雅的语法,让你能用一行代码写出其他语言很多行代码才能做的事情,比如:

最常用的迭代(eg: for i in range(1,10)), 列表生成式(eg: [ x*x for x in range(1,10) if x % 2 ==  0])

map()能让你把函数作用于多个元素, reduce()能让你把多个元素的结果按照你预想的方式组合在一起,filter()能让你快速筛选出复合条件的数据

以上具体用法可以参考https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014317793224211f408912d9c04f2eac4d2af0d5d3d7b2000

而我们这次要讨论的装饰器decorator,可以在不改变现有函数的前提下更有效率的重用代码

比如我们在实际工作当中,经常需要添加try...except来捕获异常,但是一个个加也太麻烦了,此时我们就可以用decorator装饰器来实现

比如我们有以下原始函数

def hello():
    print("Hello, world!")

def bye():
    print("Bye, world!")

正常情况下,如果都需要捕获异常的话,需要加两次try...except来做:

def main():
    try:
        hello()
    except Exception as e:
        print('except:', e)
    ....
    ....
    try:
        bye()
    except Exception as e:
        print('except:', e)
    ....
    ....

但是当我们有装饰器decorator的时候,一切都会变得特别优雅而简单,首先定义好我们的装饰器:

import functools

def decorator_try(func):
    @functools.wraps(func)
    def wrapper(*arg,**kw):
        try:
            func(*arg, **kw)
        except Exception as e:
            print('except:', e)
    return wrapper

然后只需要在原来的hello()和bye()函数定义之前添加一行语法就可以:

@decorator_try
def hello():
    print("Hello, world!")

@decorator_try
def bye():
    print("Bye, world!")

然后执行的时候任何东西都不用加

def main():
    hello()
    ....
    ....
    bye()

结果为:

>>> hello()
Hello, world!
>>> hello(1,2)
except: hello() takes 0 positional arguments but 2 were given

具体的关于decorator装饰器的语法解释可以参考https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014318435599930270c0381a3b44db991cd6d858064ac0000

另外要注意装饰器定义中 func(*arg, **kw) 和 return wrapper的区别,注意看一个是带参数,一个不带参数与括号,带参数表示执行这个函数,不带参数和括号代表把定义的函数作为一个参数传递了过去,这对理解decorator的语法是至关重要的。因为:

@decorator_try放到hello()函数的定义前,相当于执行了语句:

hello = decorator_try(hello)

 如果想更深入的了解decorator装饰器,推荐一篇博文https://www.cnblogs.com/zh605929205/p/7704902.html

下面再写一个例子:

比如我们有一个函数,下载图片,用装饰器实现timeout之后,自动重新下载一次。

import functools
import random

# 定义当timeout发生时要抛出的异常
class TimeOutError(Exception):
    pass

# 定义装饰器
def retry(func):
    @functools.wrap(func)
    def wrapper(*arg,**kwarg):
        try:
            print("first try...")
            func(*arg,**kwarg)
        except TimeOutError:
            print("timeout occurs, retrying...")
            func(*arg,**kwarg)
  return wrapper  
# 定义download函数
@retry def download(): print("downloading the photos...") download_time = random.ranint(1,2) if download_time>1: print("the download_time > 1s, time out") raise TimeOutError else: print("download finished.")

如果我们想要带参数的装饰器,则需要再多加一层函数嵌套:

#!/usr/bin/python
#-*- coding:utf-8 -*-

import functools
import random

# 定义当timeout发生时要抛出的异常
class TimeOutError(Exception):
    pass

# 定义装饰器
def decorator_download(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*arg,**kwarg):
            #try:
            #    print("first try...")
            #    func(*arg,**kwarg)
            #except TimeOutError:
            #    print("timeout occurs, retrying...")
            #    func(*arg,**kwarg)
            print(text)
            print("first try...")
            result = func(*arg,**kwarg)
            while result == False:
                print("will retry...")
                result = func(*arg,**kwarg)
        return wrapper
    return decorator

# 定义download函数
@decorator_download("retry until download finished successfully")
def download():
    print("downloading the photos...")
    download_time = random.randint(1,2)
    if download_time>1:
        print("the download_time > 1s, time out")
        #raise TimeOutError
        return False
    else:
        print("download finished.")
        return True
        
if __name__ == "__main__":
    download()