Python装饰器

本文详细探讨了Python装饰器的实现原理,从函数闭包开始,逐步改进,实现了统计执行时间和日志记录的功能。通过实例展示了如何使用装饰器语法糖,并解释了带参数的装饰器和类装饰器的用法。此外,还介绍了property装饰器的应用,用于增强类属性的访问控制。通过对装饰器的深入理解,可以更好地进行函数和类的扩展与优化。

Python装饰器

一、函数闭包

  • 一个实例:输出0~100之间所有奇数,并统计函数执行时间
def print_odds():
    """
    输出0~100之间所有奇数,并统计函数执行时间
    """
    start_time = time.clock()   # 起始时间
    # 查找并输出所有奇数
    for i in range(100):
        if i % 2 == 1:
            print(i)
    end_time = time.clock()     # 结束时间
    print("it takes {} s to find all the olds".format(end_time - start_time))

if __name__ == '__main__':
    print_odds()

​ 分析:函数逻辑(查找奇数)和辅助功能(记录时间)耦合在一起了.不方便修改,容易引发bug

  • 改进1:将辅助功能(记录时间)抽离成一个辅助函数count_time,在辅助函数count_time中调用主要功能函数print_odds.
def count_time(func):
    """
    统计某个函数的运行时间
    """
    start_time = time.clock()  # 起始时间
    func()  # 执行函数
    end_time = time.clock()  # 结束时间
    print("it takes {} s to find all the olds".format(end_time - start_time))


def print_odds():
    """
    输出0~100之间所有奇数,并统计函数执行时间
    """
    for i in range(100):
        if i % 2 == 1:
            print(i)


if __name__ == '__main__':
    # print_odds()
    count_time(print_odds)

​ 分析:仍有缺点,客户端需要调用的是print_odds函数,最好不要显式地要求客户端去调用count_time函数

  • 改进2:在count_time函数中新定义一个函数,这个函数用于包装print_odds,给其添加计时的功能。count_time返回这个新定义的函数。
    • 可以将这个返回的函数赋值给一个print_odds变量从而使其指向被包装后的函数
    • 再次调用print_odds时,调用的就是包装好后的函数
def count_time_wrapper(func):
    """
    闭包,用于增强函数func: 给函数func增加统计时间的功能
    """

    def improved_func():
        start_time = time.clock()   # 起始时间
        func()                      # 执行函数
        end_time = time.clock()     # 结束时间
        print("it takes {} s to find all the olds".format(end_time - start_time))

    return improved_func


if __name__ == '__main__':
    # 调用count_time_wrapper增强函数
    print_odds = count_time_wrapper(print_odds)
    print_odds()# improved
    print_odds()# improved
    print_odds()# improved
    print_odds()# improved
    print_odds()# improved
  • 函数闭包的概念:

    • 函数闭包本质上是一个函数
    • 函数闭包传入的参数和返回值都是一个函数
    • 函数闭包返回值的函数是对传入参数增强后的结果
    • 例如上述改进2中的count_time_wrapper就是一个函数闭包
  • 带参数的函数闭包

    • 错误的实例

    • 
      def count_time_wrapper(func):
          """
          闭包,用于增强函数func: 给函数func增加统计时间的功能
          """
      
          def improved_func():
              start_time = time.clock()   # 起始时间
              func()                      # 执行函数
              end_time = time.clock()     # 结束时间
              print("it takes {} s to find all the olds".format(end_time - start_time))
      
      
          return improved_func
      
      
      def count_odds(lim=100):
          """
          输出0~lim之间所有奇数,并统计函数执行时间
          """
          cnt = 0
          for i in range(lim):
              if i % 2 == 1:
                  cnt+=1
          return cnt
      if __name__ == '__main__':
          print('增强前')
          print(count_odds(lim=10000))         # 装饰前函数能正常返回,能接收参数
          print('----------------------')
          print('增强后')
          count_odds = count_time_wrapper(count_odds)
          print(count_odds(lim=10000))         # 装饰后函数不能正常返回,不能接收参数
      
    • 显然这段程序不能正常执行。由于count_odds实际上是improved_func,没有任何参数

    • 简单的修改方法是给improved_func加上与原包装函数相同的参数。但是这样做是不太可取的。应该将improved_func接收到的所有参数传给被包装的函数func。防止参数的遗漏。更改方法如下:

      def count_time_wrapper(func):
          """
          闭包,用于增强函数func: 给函数func增加统计时间的功能
          """
      
          def improved_func(*args, **kwargs):     # 增强函数应该把就饿收到的所有参数传给原函数
              start_time = time.clock()   # 起始时间
              ret = func(*args, **kwargs)                     # 执行函数
              end_time = time.clock()     # 结束时间
              print("it takes {} s to find all the olds".format(end_time - start_time))
              return ret      # 增强函数的返回值应该是原函数的返回值
      
          return improved_func
      

二、语法糖

  • 看下面的代码,通过装饰器进行函数增强,只是一种语法糖,本质上跟上个程序完全一致.
def count_time_wrapper(func):
    """
    闭包,用于增强函数func: 给函数func增加统计时间的功能
    """

    def improved_func():
        start_time = time.clock()  # 起始时间
        func()  # 执行函数
        end_time = time.clock()  # 结束时间
        print(
            "it takes {} s to find all the olds".format(end_time - start_time))

    return improved_func


@count_time_wrapper
def print_odds():
    """
    输出0~100之间所有奇数,并统计函数执行时间
    """
    for i in range(100):
        if i % 2 == 1:
            print(i)


if __name__ == '__main__':
    
    print_odds()

上述main函数等价于执行(如果去掉16行的count_time_warpper)

if __name__ == '__main__':
 
    print_odds = count_time_wrapper(print_odds)
    print_odds()
  • 多个语法糖:将其转化为和函数闭包相同的形式。是很好分析的。

    • def count_time_wrapper(func):
          """
          闭包,用于增强函数func: 给函数func增加统计时间的功能
          """
      
          def improved_func():
              start_time = time.clock()  # 起始时间
              func()  # 执行函数
              end_time = time.clock()  # 结束时间
              print("it takes {} s to find all the olds".format(end_time - start_time))
      
          return improved_func
      
      
      def log_wrapper(func):
          """
          闭包,用于增强函数func: 给func增加日志功能
          """
      
          def improved_func():
              start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 起始时间
              func()  # 执行函数
              end_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))  # 结束时间
              print("Logging: func:{} runs from {} to {}".format(func.__name__, start_time, end_time))
      
          return improved_func
      
      
      @count_time_wrapper
      @log_wrapper
      def count_odds():
          """
          输出0~100之间所有奇数,并统计函数执行时间
          """
          cnt = 0
          for i in range(100):
              if i % 2 == 1:
                  cnt += 1
          return cnt
      
      
      if __name__ == '__main__':
          count_odds()
      
    • 上述实例的包装过程如下:

      • 先使用log_warpper进行包装,返回一个improved_func,这个返回的实例具有了增强日志的功能。如果此时调用improved_func的话那么就会在执行count_odds函数的基础上打印出日志。但注意此时我们并没有调用这个improved_func
      • 再使用count_time_wrapper对返回的improved_func进行包装。也就是说count_time_warpper函数的func参数是这个improved_func(刚刚Log_warpper的返回值),函数执行后返回一个函数,这个函数对应于count_time_warpper里面的improved_func,这就是最后在主函数中调用count_odds所应该执行的函数
      • 根据以上的分析,很容易得出程序的输出顺序如下:打印0-100中所有的奇数->打印logging日志->打印计时信息
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXk843Il-1629372110030)(C:\Users\nth12\AppData\Roaming\Typora\typora-user-images\image-20210810120059055.png)]

三、利用装饰器语法糖构造多种不同类型的装饰器

3.1 基础装饰器

  • 需要打印两个数的和。在该函数的开始和结束需要打印日志。打印日志由装饰器实现

  • #装饰器函数 
    def logger(func): 
        def wrapper(*args, **kw): 
            print('我准备开始执行:{} 函数了:'.format(func.__name__)) 
            # 真正执行的是这行。 
            func(*args, **kw) 
            print('执行完啦。') 
        return wrapper 
    @logger 
    def add(x, y): 
        print('{} + {} = {}'.format(x, y, x+y))
    
  • 如果此时调用add(20,5)的话,由第二章的分析,我们可知实际上的执行是如下的顺序:

    • add = logger(add) 
      add(20,5)
      
    • 结果是打印我准备开始执行,然后打印两数相加的结果,最后打印执行完毕的结果。第一行语句返回包装后的函数warpper,注意这里的warpper会有两个参数*args和**kw。*args会接收所有的非a = 参数值形式的参数;**kw会以字典形式接受 a = 参数值形式的参数。因此这里传入的20,5都被送入args(元组)中,没有东西被传入字典中

    • warpper函数(实际上是add函数)在执行过程中调用func也就是原先Logger中传入的add函数。*args表示对于args进行解包,**kw也是一样的。有关Python中*用法的更多细节,请参考python中*的多种用法

3.2 带参数的函数装饰器

  • 来看下面一个代码实例。

    • def say_hello(contry):
          def wrapper(func):
              def deco(*args, **kwargs):
                  if contry == "china":
                      print("你好!")
                  elif contry == "america":
                      print('hello.')
                  else:
                      return
      
                  # 真正执行函数的地方
                  return func(*args, **kwargs)
              return deco
          return wrapper
      
      @say_hello("china")
      def xiaoming():
          pass
      @say_hello("america")
      def jack():
          pass
      
    • 如果此时执行下述调用

    • xiaoming()
      
    • 那么主程序会先打印出你好。这等价于下面的函数闭包形式:

    • xiaoming = sayhello('china')(xiaoming)
      xiaoming()
      """
      	等价于下面的代码形式:
      	wrapper = sayhello('china')  #即sayhello函数的返回值warpper,sayhello函数接收装饰器语法糖中的参数'china'
      	
      	xiaoming = wrapper(xiaoming) #warpper函数的参数为原始未被装饰的函数。在warpper里面新定义了一个函数deco,这个函数除了调用warpper的参数外(即原始未被装饰的函数),还根据say_hello接收到的参数做了一些额外的操作。由于warpper函数返回了这个deco对象,因此现在xiaoming实际上就是deco;
      	xiaoming() #实际调用的是deco,这里比较特殊,xiaoming没有任何参数。如果xiaoming有参数,调用时直接传入参数即可。
      """
      
    • 对上面的函数闭包形式进行分析:

      • 首先,函数say_hello接收装饰器字符串,返回一个warpper函数对象。
      • warpper函数对象的参数为被装饰的函数,在此例中为xiaoming
      • warpper被调用后返回一个deco函数对象,这个函数对象即为经过装饰以后的xiaoming函数对象,由warpper函数被返回。
      • 最后实际调用的是deco函数对象。

3.3 类装饰器

3.3.1 不带参数的类装饰器
  • 基于类装饰器的实现,必须实现 __call____init__两个内置函数。 __init__ :接收被装饰函数 __call__ :实现装饰逻辑。

  • 看下面这个实例:

    • class logger(object):
          def __init__(self, func):
              self.func = func
      
          def __call__(self, *args, **kwargs):
              print("[INFO]: the function {func}() is running..."\
                  .format(func=self.func.__name__))
              return self.func(*args, **kwargs)
      
      @logger
      def say(something):
          print("say {}!".format(something))
      
      say("hello")
      
    • 打印输出INFO信息,再打印出say函数中的信息。

    • 该种装饰器的执行过程相当于执行下面的函数闭包形式:

    • say = logger(say)
      say(something)
      
    • 分析:

      • 函数的一个行语句调用logger类的构造函数,以原始函数say为参数构造一个logger类的具体对象
      • 这时say是一个logger对象,由于logger对象都有_call_方法,因此第二行的say相当于调用自己的call方法。
3.3.2 带参数的类装饰器
  • 这种带参数的类装饰器的工作机理与3.1中讲述的装饰器的工作原理是类似的。

  • 看下面的示例代码:

    • class logger(object):
          def __init__(self, level='INFO'):
              self.level = level
      
          def __call__(self, func): # 接受函数
              def wrapper(*args, **kwargs):
                  print("[{level}]: the function {func}() is running..."\
                      .format(level=self.level, func=func.__name__))
                  func(*args, **kwargs)
              return wrapper  #返回函数
      
      @logger(level='WARNING')
      def say(something):
          print("say {}!".format(something))
      
      say("hello")
      
    • 先输出log信息,再输出say信息。等价于下面的函数闭包形式

    • a = logger('WARNING')
      say = a(say)
      say('hello')
      
    • 分析

      • 第一行实例化一个logger对象a,参数为装饰器语法糖中给定的参数WARNNING;
      • 第二行相当于执行a对象的_call_方法,定义一个warpper函数并返回这个函数;此时的say就是返回的wrapper
      • 最后调用say,实际上调用的是返回的warpper;

3.4 能够装饰类的装饰器

  • 实际上是对类的生成过程加以控制。

  • 看下面的实例代码:

    • instances = {}
      
      def singleton(cls):#cls代表一个类
          def get_instance(*args, **kw):
              cls_name = cls.__name__ #获取类名
              print('===== 1 ====')
              if not cls_name in instances:
                  print('===== 2 ====')
                  instance = cls(*args, **kw)
                  instances[cls_name] = instance
              return instances[cls_name]
          return get_instance
      
      @singleton
      class User:
          _instance = None
      
          def __init__(self, name):
              print('===== 3 ====')
              self.name = name
      User('abc')
      
    • 和之前的情况类似,只不过singletion函数中的装饰器换成了一个类。

3.5 property装饰器

  • Motivation:观察下面的代码示例

    class Student(object):
        def __init__(self, name, age=None):
            self.name = name
            self.age = age
    
    # 实例化
    xiaoming = Student("小明")
    
    # 添加属性
    xiaoming.age=25
    
    # 查询属性
    xiaoming.age
    
    # 删除属性
    del xiaoming.age
    
    • 上述代码的健壮性是非常差的。不合法的age输入无法检测。
    • 可以考虑将字段设为私有,同时增加set_age和get_age函数,在里面检测异常的发生。
    • 但是我们更希望直接按照上述方法,但又能使程序检测我们输入age的合法性。这就需要用到property装饰器了。
  • 定义:class `property`(fget=None, fset=None, fdel=None, doc=None)

    • fget 是获取属性值的函数。 fset 是用于设置属性值的函数。 fdel 是用于删除属性值的函数。并且 doc 为属性对象创建文档字符串。
  • 使用:看如下的例子

    • 典型用法:

    • class C:
          def __init__(self):
              self._x = None
      
          def getx(self):
              return self._x
      
          def setx(self, value):
              self._x = value
      
          def delx(self):
              del self._x
      
          x = property(getx, setx, delx, "I'm the 'x' property.")
          #如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。
      
    • 装饰器用法:

    • class C:
          def __init__(self):
              self._x = None
      
          @property
          def x(self):
              """I'm the 'x' property."""
              return self._x
      
          @x.setter
          def x(self, value):
              self._x = value
      
          @x.deleter
          def x(self):
              del self._x
          #如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。
      
    • 装饰器用法:

    • class C:
          def __init__(self):
              self._x = None
      
          @property
          def x(self):
              """I'm the 'x' property."""
              return self._x
      
          @x.setter
          def x(self, value):
              self._x = value
      
          @x.deleter
          def x(self):
              del self._x
          #如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。
      
  • 底层实现

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值