8.6 创建方法函数装饰器

    一个类中方法函数的装饰器和一个单独的函数的装饰器是一样的,只是在不同的上下文中使用。这种上下文所带来的一个轻微的后果是必须经常显式地声明self变量。

    方法函数装饰器的一个应用是追踪对象状态的改变。商业应用程序经常会创建有状态的记录;通常,这些记录会作为关系型数据库中的行。我们会在第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”、第10章“用Shelve保存和获取对象”和第11章“用SQLite保存和获取对象”中详细讲解对象的表示。

    当我们有一些有状态的记录时,状态的改变应该是可以被追踪的。通过追踪可以确认改变已经被保存到记录中。为了能够追踪这些记录,必须可以用某种方式来获得每条记录在改变之前和改变之后的版本。状态数据库记录是一种长期使用的传统方法,但是不是任何情况下都必需的。不可变的数据库记录是另外一种可选择的设计。

    当设计一个有状态的类时,任何setter方法都会带来状态的改变。这些setter方法通常会用@property装饰器,这样它们就可以被当作简单的属性来使用。如果我们这么做,我们可以添加一个@audit装饰器,这样就能合理地追踪所有的改变。我们会通过logging模块创建追踪日志。我们会用repr()方法函数生成一个可以用来浏览所有修改的完整文本,下面是一个追踪装饰器。

    def audit( method ):
      @functools.wraps(method)
      def wrapper( self, args, **kw ):
        audit_log= logging.getLogger( 'audit' )
        before= repr(self)
        try:
          result= method( self,
    args, **kw )
          after= repr(self)
        except Exception as e:
          auditlog.exception(
            '%s before %s\n after %s', method.qualname, before, after )
          raise
        auditlog.info(
            '%s before %s\n after %s', method.__qualname
    , before, after )
        return result
      return wrapper

    我们创建了一个对象被修改前的文本表示。然后,调用原始的方法函数。如果有异常,会生成一个包含异常信息的追踪日志。否则,会在日志中生成一个INFO条目,它包含方法的全名、修改前的文本表示和修改后的文本表示。下面是一个修改过的Hand类,它会向我们展示要如何使用这个装饰器。

    class Hand:
      def init( self, *cards ):
        self.cards = list(cards)
      
    @audit
      def iadd( self, card ):
        self.cards.append( card )
        return self
      def repr( self ):
        cards= ", ".join( map(str,self.cards) )
        return "{class.name}({cards})".format(_
    class
    =self.__class
    , cards=cards)

    这个定义修改了iadd()方法函数,这样添加一张牌就成为一个可追踪的事件。这个装饰器会执行追踪操作,在进行操作前和完成操作后会保存Hand的文本表示。

    方法装饰器的这种使用方式相当于对某个方法函数进行正式声明,这样会大量地修改状态。我们可以直接使用代码审查,确保所有符合要求的方法函数都像这个一样,被标记为是可追踪的。有一个没有解决的问题是追踪对象的创建过程。目前尚不清楚是否需要追踪对象的创建过程。有一些人会说对象的创建并不是一种状态改变。

    在我们想要追踪对象创建的情境中,我们不可以在init()方法函数中使用这个audit装饰器。因为在执行init()之前什么都没有。我们可以通过以下两种方式补救这个问题。

    • 可以添加一个new()方法用于确保一个空的__cards属性会以空集合的方法存在于类中。
    • 可以修改audit()装饰器,当init()被执行时,接受程序抛出的AttributeError异常。

    第2种方式相对来说更加灵活一些,我们可以像下面这样做。

    try:
      before= repr(self)
    except AttributeError as e:
      before= repr(e)

    这段代码会记录类似AttributeError: 'Hand' object has no attribute '_cards'这样的信息作为初始化前的状态。