[Python3] 带失败重试及日志功能的装饰器

写在前面

有疑问可在评论处留言,欢迎大家一起交流探讨。考虑到网页我可能不经常看,若问题较为紧急,可关注微信公众号「燃烧吧数据」(id:data-fired),聊天框输入暗号:江湖救急,即可召唤小姐姐一枚~

一、需求背景

http请求的时候,能够实现每次请求不管是失败还是成功,都能输出一条日志。并且失败的请求能够按照指定的重试次数,进行重试(因为每次重试都是一次请求,所以重试的时候也要输出日志)。

二、python代码实现带失败重试及日志功能的装饰器

如果你对装饰器的概念还懵懵懂懂,可以先看第三部分关于装饰器的介绍,然后再回过头来看这一部分。
剖析一下需求:首先,日志应该是每一次执行都要输出的。其次,因为要进行失败重试,那么重试的每一次都应该有日志输出。所以逻辑上应该,日志功能的装饰器在里层,失败重试的装饰器在外层。再次,因为失败重试一般需要指定重试次数,所以失败重试的装饰器应该是个带参数的装饰器,参数就是失败重试的次数。

  • python版本:python3.6.6
  • 代码实现:
from functools import wraps


def generate_log(func):
    """
    输出日志的装饰器
    :param func:
    :return:
    """
    @wraps(func)
    def wrapper(a):
        result = func(a)  # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
        print(f"这是一条日志:{func.__name__} is running")
        return result

    return wrapper
# generate_log() 相当于 wrapper
# wrapper() 相当于fun()
  
  
def fail_retry(try_times):
    """
    失败重试的装饰器
    :param try_times:
    :return:
    """
    def decorator(func):
        @wraps(func)
        def wrapper(a):
            current_result = func(a)
            if current_result:
                print('结果为True,即将进行失败重试')
                n = 1
                while n <= try_times:
                    a -= 1
                    print(f"{'*' * 5}第{n}次重试:参数值为:{a}{'*' * 5}")
                    retry_result = func(a)
                    if not retry_result:
                        break
                    n += 1
                return retry_result
            else:
                print('结果为False,无须失败重试')
                return current_result

        return wrapper

    return decorator
# fail_retry(try_times) 相当于 decorator
# decorator(func) 相当于 wrapper
# wrapper(a) 相当于fun(a)
# @fail_retry(try_times) 相当于 func = fail_retry(try_times)(func)
# 所以 func(a) 相当于 fail_retry(try_times)(func)(a)
  • 运行
@fail_retry(try_times=3)
@generate_log
def foo(a):
    if a > 3:
        return True
    else:
        return False
# @generate_log 相当于 foo = generate_log(foo)
# 因为 generate_log 装饰器在 fail_retry 里面,所以 fail_retry 装饰器中的 func 相当于 @generate_log
# 所以 @fail_retry(try_times=3) 相当于 foo = fail_retry(try_times=3)(func) = fail_retry(try_times=3)(@generate_log) = fail_retry(try_times=3)(generate_log(foo))

foo(6)  # 等价于执行fail_retry(try_times=3)(generate_log(foo))(6)
>>>
  这是一条日志:foo is running
  结果为True,即将进行失败重试
  *****第1次重试:参数值为:5*****
  这是一条日志:foo is running
  *****第2次重试:参数值为:4*****
  这是一条日志:foo is running
  *****第3次重试:参数值为:3*****
  这是一条日志:foo is running  

三、关于装饰器

此部分内容转载自微信公众号「Python之禅」(id:vttalk),文章链接:https://mp.weixin.qq.com/s/dTMQ-5wORWnC0gvBcnkC4Q,侵删。

个人觉得,这篇文章介绍的很明白,很形象,很有逻辑。强推。

讲 Python 装饰器前,我想先举个例子,虽有点污,但跟装饰器这个话题很贴切。

每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,让它变得更厚更长,这样一来,它不仅有遮羞功能,还能提供保暖,不过有个问题,这个内裤被我们改造成了长裤后,虽然还有遮羞功能,但本质上它不再是一条真正的内裤了。于是聪明的人们发明长裤,在不影响内裤的前提下,直接把长裤套在了内裤外面,这样内裤还是内裤,有了长裤后宝宝再也不冷了。装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。

谈装饰器前,还要先要明白一件事,Python 中的函数和 Java、C++不太一样,Python 中的函数可以像普通变量一样当做参数传递给另外一个函数,例如:

def foo():
      print("foo")
def bar(func):
      func()

bar(foo)

正式回到我们的主题。装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

先来看一个简单例子,虽然实际代码可能比这复杂很多:

def foo():
      print('i am foo')

现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:

def foo():
    print('i am foo')
    logging.info("foo is running")

如果函数 bar()、bar2() 也有类似的需求,怎么做?再写一个 logging 在 bar 函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志 ,日志处理完之后再执行真正的业务代码

def use_logging(func):
      logging.warn("%s is running" % func.__name__)
      func()
    
    
def foo():
      print('i am foo')
      
use_logging(foo)

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo 函数,而是换成了 use_logging 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo 函数作为参数传递给 use_logging 函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。

简单装饰器

import logging

def use_logging(func):
      def wrapper():
              logging.warn("%s is running" % func.__name__)
              return func()  # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
      return wrapper


def foo():
      print('i am foo')


foo = use_logging(foo)  # 因为装饰器use_logging(foo)返回的是函数对象wrapper,这条语句相当于foo = wrapper, 即装饰器返回的是个函数对象
foo()  # 执行foo()就相当于执行 wrapper()

use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_logging 装饰了一样,use_logging 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程。

@ 语法糖

如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。

def use_logging(func):
      def wrapper():
              logging.warn("%s is running" % func.__name__)
              return func()

      return wrapper


@use_logging
def foo():
      print("i am foo")


foo()

如上所示,有了 @ ,我们就可以省去foo = use_logging(foo)这一句了,直接调用 foo() 即可得到想要的结果。你们看到了没有,foo() 函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。

args、*kwargs

可能有人问,如果我的业务逻辑函数 foo 需要参数怎么办?比如:

def foo(name):
      print("i am %s" % name)

我们可以在定义 wrapper 函数的时候指定参数:

def use_logging(func):
      def wrapper(name):
              logging.warn("%s is running" % func.__name__)
              return func(name)
    return wrapper

这样 foo 函数定义的参数就可以定义在 wrapper 函数中。这时,又有人要问了,如果 foo 函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道 foo 到底有多少个参数时,我们可以用 *args 来代替:

def use_logging(func):
      def wrapper(*args):
              logging.warn("%s is running" % func.__name__)
              return func(*args)
      return wrapper

如此一来,甭管 foo 定义了多少个参数,我都可以完整地传递到 func 中去。这样就不影响 foo 的业务逻辑了。这时还有读者会问,如果 foo 函数还定义了一些关键字参数呢?比如:

def foo(name, age=None, height=None):
      print("I am %s, age %s, height %s" % (name, age, height))

这时,你就可以把 wrapper 函数指定关键字函数:

def use_logging(func):
      def wrapper(*args, **kwargs):
      # args是一个数组,kwargs一个字典
              logging.warn("%s is running" % func.__name__)
              return func(*args, **kwargs)
      return wrapper

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper
    return decorator


@use_logging(level="warn")
def foo(name='foo'):
      print("i am %s" % name)


foo()

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@use_logging(level="warn")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。

@use_logging(level="warn")等价于foo =use_logging(level="warn")(foo)= decorator(foo) = @decorator

类装饰器

没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的call方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

class Foo(object):
     def __init__(self, func):
             self._func = func
     def __call__(self):
             print ('class decorator runing')
             self._func()
             print ('class decorator ending')
@Foo
def bar():
     print ('bar')
bar()

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstringname、参数列表,先看例子:

# 装饰器
def logged(func):
      def with_logging(*args, **kwargs):
          print func.__name__      # 输出 ‘with_logging
          print func.__doc__       # 输出 None
          return func(*args, **kwargs)
      return with_logging


# 函数
@logged
def f(x):
    """does some math"""
    return x + x * x
logged(f)

不难发现,函数 f 被with_logging取代了,当然它的docstringname就是变成了with_logging函数的信息了。好在我们有functools.wrapswraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

from functools import wraps


def logged(func):
     @wraps(func)
     def with_logging(*args, **kwargs):
             print func.__name__      # 输出 'f'
             print func.__doc__       # 输出 'does some math'
             return func(*args, **kwargs)
     return with_logging
  
  
@logged
def f(x):
    """does some math"""
    return x + x * x    

装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

@a
@b
@c
def f ():
    pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于:

f = a(@b) = a(b(@c)) = a(b(c(f)))

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