python装饰器原理及应用

227 阅读8分钟
原文链接: mp.weixin.qq.com

在python编程中,我们经常看到下面的函数用法:

with open("test.txt", "w") as f:  f.write("hello world!")

习惯了java开发的python初学者,心里不免犯嘀咕:

  • 文件open操作之后,为什么没有close,不怕文件描述符资源耗尽吗?

  • 文件write操作没有异常捕获,不怕中断程序主流程吗?如果您也有同样的忧虑,那太正常不过了,起码说明您是一位有“开发原则”的人,同时也说明您对其背后的原理了解存在盲区。如果是这种情况,本文强烈建议您耐心阅读完以下章节。为了系统的阐述其背后的奥秘,本文从最基本的函数讲起。

关于函数

在Python中,一切皆为对象,包括函数。

def foo(num):    return num + 1    value = foo(3)print(value)def bar():    print("bar")    foo = barfoo()

上面简单的函数例子中,可以总结几点信息:

  1. 函数名字foo可以作为变量名字,指向函数对象

  2. 函数名字foo作为对象,可以赋值给变量value

  3. 函数名字foo可以作为变量名字,指向其他函数bar

  4. 函数名字(函数对象)通过括号调用函数 不仅如此,作为对象的函数也具有一般对象的特性,比如:

  • 函数作为参数

def foo(num):    return num + 1def bar(fun):    return fun(3)value = bar(foo)print(value)
  • 函数作为返回值

def foo():    return 1def bar():    return foo #注意这里没有括号print(bar()) # <function foo at 0x10a2f4140>print(bar()()) # 1# 等价于print(foo()) # 1
  • 函数嵌套

def outer():    x = 1    def inner():        print(x)    inner() # 注意这里有括号,直接被调用outer() #
  • 闭包

def outer(x):    def inner():        print(x)    return inner #没括号,不被直接调用closure = outer(1) # closure就是一个闭包closure()

同样是嵌套函数,只是稍改动一下,把局部变量 x 作为参数了传递进来,嵌套函数不再直接在函数里被调用,而是作为返回值返回,这里的 closure就是一个闭包,本质上它还是函数,闭包是引用了自由变量(x)的函数(inner)。

  • 装饰器

def outer(func):    def inner():        print("before call fun")        func()        print("after call fun")    return innerdef foo():    print("foo")new_foo = outer(foo)new_foo()

outer 函数其实就是一个装饰器:一个带有函数作为参数并返回一个新函数的闭包.本质上装饰器也是函数,outer 函数的返回值是 inner 函数。

注:上面示例中的装饰器函数调用,可以用语法糖@简写为:

@outerdef foo():    print("foo")foo()

我们进一步抽象装饰器:

def decorator(func):    def wrapper(*args, **kw):        return func()    return wrapper@decoratordef function():    print("hello, decorator")

可见,通过装饰器,可以让代码更加简练、优雅、可读性更强。

装饰器进阶

  • 类装饰器 基于类装饰器的实现,必须实现 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)@loggerdef say(something):    print("say {}!".format(something))say("hello")
  • 装饰类的装饰器 装饰器不仅可以装饰函数,还可以装饰类,比如如果想改写类的方法的部分实现,除了通过类继承重载,还可以通过装饰器,实现如下:

def log_getattribute(cls):    # Get the original implementation    orig_getattribute = cls.__getattribute__    # Make a new definition    def new_getattribute(self, name):        print('getting:', name)        return orig_getattribute(self, name)    # Attach to the class and return    cls.__getattribute__ = new_getattribute    return cls# Example use@log_getattributeclass A:    def __init__(self,x):        self.x = x    def spam(self):        pass        a = A(42)print(a.x)

示例中,通过装饰器函数log_getattribute修改原有类的属性方法__getattribute__的指向来达到目的:通过指向新的方法new_getattribute,在新的方法中在调用原来方法之前,添加额外逻辑。

  • 偏函数 使用装饰器的前提是装饰器必须是可被调用的对象,比如函数、实现了__call__ 函数的类等,即将介绍的偏函数其实也是 callable 对象。在了解偏函数之前,先举个例子:计算 100 加任意个数字的和。我们用parital函数解决这个问题:

from functools import partialdef add(*args):    return sum(args)add_100 = partial(add, 100)print(add_100(1, 2))  # 103print(add_100(1, 2, 3))  # 106

跟上面的例子那样,偏函数作用和装饰器一样,它可以扩展函数的功能,但又不完全等价于装饰器。通常应用的场景是当我们要频繁调用某个函数时,其中某些参数是已知的固定值,可以将这些固定值“固定”,然后用其他的参数参与调用。类似偏导数计算那样,固定几个变量,对剩下的变量求导。我们看下partial的函数参数定义:

func = functools.partial(func, *args, **keywords)func: 需要被扩展的函数,返回的函数其实是一个类 func 的函数*args: 需要被固定的位置参数**kwargs: 需要被固定的关键字参数# 如果在原来的函数 func 中关键字不存在,将会扩展,如果存在,则会覆盖# 同样是刚刚求和的代码,不同的是加入的关键字参数def add(*args, **kwargs):    # 打印位置参数    for n in args:        print(n)    print("-"*20)    # 打印关键字参数    for k, v in kwargs.items():       print('%s:%s' % (k, v))    # 暂不做返回,只看下参数效果,理解 partial 用法    # 普通调用add(1, 2, 3, v1=10, v2=20)add_partial = partial(add, 10, k1=10, k2=20)add_partial(1, 2, 3, k3=20)
  • 偏函数与装饰器 我们再看看如何使用类和偏函数结合实现装饰器,如下所示,DelayFunc 是一个实现了__call__ 的类,delay 返回一个偏函数,在这里 delay 就可以做为一个装饰器:

import timeimport functoolsclass DelayFunc:    def __init__(self,  duration, func):        self.duration = duration        self.func = func    def __call__(self, *args, **kwargs):        print(f'Wait for {self.duration} seconds...')        time.sleep(self.duration)        return self.func(*args, **kwargs)def delay(duration):    """    装饰器:推迟某个函数的执行。    """    # 此处为了避免定义额外函数,    # 直接使用 functools.partial 帮助构造 DelayFunc 实例    return functools.partial(DelayFunc, duration) @delay(duration=2)def add(a, b):    return a+b    

wraps

继续深入函数装饰器,首先打印被装饰的函数function的名字:

def decorator(func):    def wrapper(*args, **kw):        return func()    return wrapper@decoratordef function():    print("hello, decorator")    print(function.__name__) #wrapper

输出发现是wrapper,其实这也好理解,因为decorator返回的就是wrapper。但有时我们需要返回function的本来名字,那怎么做呢?python 的functools模块提供了一系列的高阶函数以及对可调用对象的操作,比如reduce,partial,wraps等。其中partial作为偏函数,在前面已经介绍过,warps旨在消除装饰器对原函数造成的影响,即对原函数的相关属性(比如__name__)进行拷贝,以达到装饰器不修改原函数(属性)的目的:

from functools import wrapsdef decorator(func):    @wraps(func)    def wrapper(*args, **kw):        print(func.__name__)        return func()    return wrapper@decoratordef function():    print("hello, decorator")function()print(function.__name__)

注意代码中return func(),括号表示调用执行函数。作为对比,请看下面的调用:

from functools import wrapsdef decorator(func):    @wraps(func)    def wrapper(*args, **kw):        print(func.__name__)        return func    return wrapper@decoratordef function():    print("hello, decorator")#因为装饰返回func,不会发生调用,因此需要两对括号,其中function()返回的是函数定义。print(function())function()()print(function.__name__)

装饰器应用之contextmanager

contextmanager是python中一个使用广泛的上下文管理器,(实际上也是装饰器)经常跟with语句一起使用,用于精确地控制资源的分配和释放。回忆以下常规代码结构:

def controlled_execution(callback):    try:        #比如环境初始化、资源分配等        set things up        callback(thing)    finally:        #比如资源回收、事物提交等        tear things downdef my_function(thing):    #执行具体的业务逻辑    do somethingcontrolled_execution(my_function)

以上为了防止业务逻辑出现异常,导致一些必须要执行的操作无法执行,通常使用try...finally语句,保证必要操作一定被执行。但是如果代码中大量使用这种语句,又导致程序逻辑冗余,可读性变差。但是结合with,并将以上语句稍作改动:将try...finally的逻辑拆分成两个函数,分别执行比如资源的初始化和释放,封装在一个class中:

class controlled_execution:    def __enter__(self):        set things up        return thing    def __exit__(self, type, value, traceback):        tear things downwith controlled_execution() as thing:    # code body    do something

其中with expression [as variable],用来简化 try / finally 语句。当执行with语句、进入代码块前,调用__enter__方法,代码块执行结束之后执行__exit__方法。需要注意的是可以根据__exit__方法的返回值来决定是否抛出异常,如果没有返回值或者返回值为 False ,则异常由上下文管理器处理,如果为 True 则由用户自己处理。上述代码可以通过contextmanager进一步简化:

@contextmanagerdef controlled_execution():    #set things up    yield thing    #tear things down    with controlled_execution() as t:    print(t)

引入yield将函数变成生成器,yield将函数体分为两部分:yield之前的语句在执行with代码块之前执行,yield之后的代码块在with代码块之后执行。到此为止,相信大家能够理解文章开篇提到的代码块了,然后基于此,我们也可以自定义一个open函数:

from contextlib import contextmanager@contextmanagerdef my_open(name):    f = open(name, 'w')    yield f    f.close()with my_open('some_file') as f:    f.write('hola!')

有关contextmanager的原理分析参考“阅读全文”。