Python Decorator(装饰器)

RicardoJack 6年前
   <p>今天来说说 Python 里的装饰器 (decorator)。它不难,但却几乎是 “精通” Python 的路上的第一道关卡。让我们来看看它到底是什么东西,为什么我们需要它。</p>    <h2>手写装饰器</h2>    <p>现在我们要写一个函数:</p>    <pre>  <code class="language-python">def add(x, y=10):   return x + y  </code></pre>    <p>然后我们想看看运行的结果,于是写了几个 print 语句:</p>    <pre>  <code class="language-python">print("add(10)", add(10))  print("add(20, 30)", add(20, 30))  print("add('a', 'b')", add('a', 'b'))    # Results:  # add(10) 20  # add(20, 30) 50  # add('a', 'b') ab  </code></pre>    <p>现在我们想看看测试这个函数的性能,于是我们加上这个代码:</p>    <pre>  <code class="language-python">from time import time    before = time()  print("add(10)", add(10))  after = time()  print("time taken: {}".format(after - before))    before = time()  print("add(20, 30)", add(20, 30))  after = time()  print("time taken: {}".format(after - before))    before = time()  print("add('a', 'b')", add('a', 'b'))  after = time()  print("time taken: {}".format(after - before))    # Results  # add(10) 20  # time taken: 0.00017189979553222656  # add(20, 30) 50  # time taken: 9.751319885253906e-05  # add('a', 'b') ab  # time taken: 0.00012969970703125  </code></pre>    <p>代码马上变得很复杂。但最重要的是,我们得写一堆代码(复制粘贴),程序员是懒惰的,所以我们就想到一些更简单的方法,与其写这么多次,我们可以只写一次代码:</p>    <pre>  <code class="language-python">from time import time  def add(x, y=10):   before = time()   result = x + y   after = time()   print('elapsed: ', after - before)   return result    print("add(10)", add(10))  print("add(20, 30)", add(20, 30))  print("add('a', 'b')", add('a', 'b'))    # Results  # elapsed: 1.9073486328125e-06  # add(10) 20  # elapsed: 9.5367431640625e-07  # add(20, 30) 50  # elapsed: 1.9073486328125e-06  # add('a', 'b') ab  </code></pre>    <p>不论是代码的修改量还是代码的美观程度,都比之前的版本要好!</p>    <p>但是,现在我们写了另一个函数:</p>    <pre>  <code class="language-python">def sub(x, y=10):   return x - y  </code></pre>    <p>我们必须再为 sub 函数加上和 add 相同的性能测试代码:</p>    <pre>  <code class="language-python">def sub(x, y=10):   before = time()   result = x - y   after = time()   print('elapsed: ', after - before)   return result  </code></pre>    <p>作为一个懒惰的程序员,我们立马就发现了,有一个 “模式” 反复出现,即执行一个函数,并计算这个函数的执行时间。于是我们就可以把这个模式抽象出来,用函数:</p>    <pre>  <code class="language-python">from time import time    def timer(func, x, y = 10):   before = time()   result = func(x, y)   after = time()   print("elapsed: ", after - before)   return result    def add(x, y = 10):   return x + y    def sub(x, y = 10):   return x - y    print("add(10)", timer(add, 10))  print("add(20, 30)", timer(add, 20, 30))  </code></pre>    <p>但这样还是很麻烦,因为我们得改到所有的测试用例,把 add(20, 30) 改成 timer(add, 20, 30) 。于是我们进一步改进,让 timer 返回函数:</p>    <pre>  <code class="language-python">def timer(func):   def wraper(x, y=10):   before = time()   result = func(x, y)   after = time()   print("elapsed: ", after - before)   return result   return wraper    def add(x, y = 10):   return x + y  add = timer(add)    def sub(x, y = 10):   return x - y  sub = timer(sub)    print("add(10)", add(10))  print("add(20, 30)", add(20, 30))  </code></pre>    <p>这里的最后一个问题是,我们的 timer 包装的函数可能有不同的参数,于是我们可以进一步用 *args, **kwargs 来传递参数:</p>    <pre>  <code class="language-python">def timer(func):   def wraper(*args, **kwargs):   before = time()   result = func(*args, **kwargs)   after = time()   print("elapsed: ", after - before)   return result   return wraper  </code></pre>    <p>这里的 timer 函数就是一个 “装饰器”,它接受一个函数,并返回一个新的函数。在装饰器的内部,对原函数进行了“包装”。</p>    <p>注:上面的例子取自 <a href="/misc/goto?guid=4959754837674995155" rel="nofollow,noindex">What Does it Take to Be an Expert At Python</a> 。</p>    <h2>@ 语法糖</h2>    <p>上一节是一个懒惰的程序员用原生的 Python 写的装饰器,但在装饰器的使用上,用的是这个代码:</p>    <pre>  <code class="language-python">def add(x, y = 10):   return x + y  add = timer(add) # <- notice this    def sub(x, y = 10):   return x - y  sub = timer(sub)  </code></pre>    <p>上面这个语句里,我们把 add 的名字重复了 3 次,如果函数改了名字,我们就得改 3 处。懒惰的程序员就想了一个更“好”的方法,提供了一个语法来替换上面的内容:</p>    <pre>  <code class="language-python">@timer  def add(x, y=10):   return x + y  </code></pre>    <p>这就是我们最常见的装饰器的形式了,这两种写法完全等价,只是 @ 写法更简洁一些。</p>    <h2>带参数的装饰器</h2>    <p>我们知道下面两种代码是等价的:</p>    <pre>  <code class="language-python">@dec  def func(...):   ...    func = dec(func)  </code></pre>    <p>我们可以把它当成是纯文本的替换,于是可以是这样的:</p>    <pre>  <code class="language-python">@dec(arg)  def func(...):   ...    func = dec(arg)(func)  </code></pre>    <p>这也就是我们看到的“带参数”的装饰器。可见,只要 dec(arg) 的返回值满足 “装饰器” 的定义即可。(接受一个函数,并返回一个新的函数)</p>    <p>这里举一个例子( <a href="/misc/goto?guid=4959754837763881055" rel="nofollow,noindex">来源</a> ):</p>    <pre>  <code class="language-python">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)  </code></pre>    <p>先不管 use_logging 长什么样,先关心它的返回值 decorator ,看到 decorator 本身是一个函数,并且参数是函数,返回值是函数,于是确认 decorator 是一个 “装饰器”。于是上面这种“带参数的装饰器”的作用也就很直接了。</p>    <h2>类作为装饰器</h2>    <p>如果说 Python 里一切都是对象的话,那函数怎么表示成对象呢?其实只需要一个类实现 __call__ 方法即可。</p>    <pre>  <code class="language-python">class Timer:   def __init__(self, func):   self._func = func   def __call__(self, *args, **kwargs):   before = time()   result = self._func(*args, **kwargs)   after = time()   print("elapsed: ", after - before)   return result    @Timer  def add(x, y=10):   return x + y  </code></pre>    <p>也就是说把类的构造函数当成了一个装饰器,它接受一个函数作为参数,并返回了一个对象,而由于对象实现了 __call__ 方法,因此返回的对象相当于返回了一个函数。因此该类的构造函数就是一个装饰器。</p>    <h2>小结</h2>    <p>装饰器中还有一些其它的话题,例如装饰器中元信息的丢失,如何在类及类的方法上使用装饰器等。但本文里我们主要目的是简单介绍装饰器的原因及一般的使用方法,能用上的地方就大胆地用上吧!</p>    <h2>扩展阅读</h2>    <ul>     <li><a href="/misc/goto?guid=4959754837856903340" rel="nofollow,noindex">PEP 0318 – Decorators for Functions and Methods</a></li>     <li><a href="/misc/goto?guid=4959754837932685274" rel="nofollow,noindex">Python Decorator in Detail</a></li>     <li><a href="/misc/goto?guid=4959754837674995155" rel="nofollow,noindex">What Does it Take to Be an Expert At Python</a></li>     <li><a href="/misc/goto?guid=4959754837763881055" rel="nofollow,noindex">理解 Python 装饰器看这一篇就够了</a></li>     <li><a href="/misc/goto?guid=4959754838056817333" rel="nofollow,noindex">How you implemented your Python decorator is wrong</a></li>    </ul>    <p> </p>    <p>来自:http://lotabout.me/2017/Python-Decorator/</p>    <p> </p>