8.4 写一个简单的函数装饰器

    一个decorator函数是一种用于返回新函数的函数(或者可调用对象)。最简单的只需要一个参数:将被装饰的函数。装饰器的返回值是一个被包装的函数。基本上,额外的功能会加到原始功能之前或者之后,这是函数中两个现成的连接点。

    当定义一个装饰器时,我们会想确保装饰过的函数有原始函数的函数名和docstring。这些将被用于写装饰函数的属性应该由装饰器来设定。用functools.wraps来写装饰器简化了所需要做的工作,因为它已经为我们处理这些事情。

    为了展示两个可以插入新功能的地方,可以创建一个调试跟踪装饰器,它会将一个函数的参数和返回值写入日志。这个装饰器会在调用函数前和调用函数后分别插入新的功能。下面是我们想要封装的函数——some_function

    logging.debug( "function(", args, kw, ")" )
    result= some_function( args, *kw )
    logging.debug( "result = ", result )
    return result

    这段代码展示了如何用新的处理逻辑封装原来的函数。

    通过修改基础的code对象向一个定义好的函数中插入新功能是很困难的。只有极少数的情况下,似乎真的有必要向函数体中插入新功能,但是,通过将功能分别写到多个方法函数中将函数写成一个可调用对象会容易很多。然后,就可以利用mixin和子类而不用非常复杂的代码进行重写来完成我们的需求。下面是一个会在函数开始执行之前和执行之后插入日志的调试装饰器。

    def debug( function ):
      @functools.wraps( function )
      def loggedfunction( args, **kw ):
        logging.debug( "%s( %r, %r )", function.name, args, kw, )
        result= function(
    args, **kw )
        logging.debug( "%s = %r", function._name
    , result )
        return result
      return logged_function

    我们用了functools.wraps装饰器确保原始函数的函数名和docstring在新生成的函数中都会被保留。现在,我们可以用装饰器来生成丰富、详尽的调试信息了。例如,我们将这个装饰器应用于一些函数上,如ackermann(),如下所示。

    @debug
    def ackermann( m, n ):
      if m == 0: return n+1
      elif m > 0 and n == 0: return ackermann( m-1, 1 )
      elif m > 0 and n > 0: return ackermann( m-1, ackermann( m, n-1 ) )

    这段代码包装了ackermann()函数,它用logging模块将调试信息写入到根记录器中。会用下面的方式配置日志记录器。

    logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)

    我们会在第14章“Logging和Warning模块”中再回来讲解日志的具体内容。当执行ackermann(2,4)时,会看到下面这种结果。

    DEBUG:root:ackermann( (2, 4), {} )
    DEBUG:root:ackermann( (2, 3), {} )
    DEBUG:root:ackermann( (2, 2), {} )
    .
    .
    .
    DEBUG:root:ackermann( (0, 10), {} )
    DEBUG:root:ackermann = 11
    DEBUG:root:ackermann = 11
    DEBUG:root:ackermann = 11

    创建独立的日志记录器

    作为对日志的优化,我们可能希望对每一个封装的函数都使用一个特定的日志记录器,而不是过分使用根记录器来记录这种调试信息。我们会在第14章“Logging和Warning模块”中再讲解日志记录器。下面是装饰器为每个函数创建一个独立的日志记录器的代码。

    def debug2( function ):
      @functools.wraps( function )
      def loggedfunction( args, *kw ):
        log= logging.getLogger( function._name
    )
        log.debug( "call( %r, %r )", args, kw, )
        result= function( args, *kw )
        log.debug( "result %r", result )
        return result
      return logged_function

    这个版本的输出类似下面这样。

    DEBUG:ackermann:call( (2, 4), {} )
    DEBUG:ackermann:call( (2, 3), {} )
    DEBUG:ackermann:call( (2, 2), {} )
    .
    .
    .
    DEBUG:ackermann:call( (0, 10), {} )
    DEBUG:ackermann:result 11
    DEBUG:ackermann:result 11
    DEBUG:ackermann:result 11

    函数名现在是日志记录器的名字。这个可以用来优化调试信息。现在可以启用针对每个函数的日志记录。我们无法仅仅通过简单地修改装饰器并期望被装饰的函数也对应地更改。

    我们需要将修改后的装饰器应用于函数上。这意味着调试和测试装饰器不能简单地通过>>>命令行来完成。在修改了装饰器后,必须重新加载函数。这可能包括一系列的复制粘贴,或者可能包括重新运行定义了装饰器的脚本、函数,然后运行测试或者演示脚本以确定一切都正常工作。