盛杨 發表於 2019-7-22 10:24:00

Python装饰器与闭包

<p>闭包是Python装饰器的基础。要理解闭包,先要了解Python中的变量作用域规则。</p>
<h3>变量作用域规则</h3>
<p>首先,在函数中是能访问全局变量的:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> a <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">'</span>global var<span class="pl-pds">'</span></span>

<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> <span class="pl-k">def</span> <span class="pl-en">foo</span>():
        <span class="pl-c1">print</span>(a)

<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> foo()
<span class="pl-k">global</span> var</pre>
</div>
<p>然后,在一个嵌套函数中,内层函数能够访问在外层函数中定义的局部变量:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> <span class="pl-k">def</span> <span class="pl-en">foo</span>():
        a <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">'</span>free var<span class="pl-pds">'</span></span>
        <span class="pl-k">def</span> <span class="pl-en">bar</span>():
          <span class="pl-c1">print</span>(a)
        <span class="pl-k">return</span> bar

<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> foo()()
free var</pre>
</div>
<h3>闭包</h3>
<p>上面的嵌套函数就是闭包。<strong>闭包</strong>是指延伸了作用域的函数,在其中能够访问未在函数定义体中定义的非全局变量。未在函数定义体中定义的非全局变量一般都是在嵌套函数中出现的。</p>
<p>上述示例中的变量a就是一个并未在函数bar中定义的非全局变量。对于bar来说,它有个专业名字,叫做<strong>自由变量</strong>。</p>
<p>自由变量的名称可以在字节码对象中查看:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> bar <span class="pl-k">=</span> foo()
<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> bar.<span class="pl-c1">__code__</span>.co_freevars
(<span class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span>,)</pre>
</div>
<p>自由变量的值绑定在函数的__closure__属性中:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> bar.__closure__
(<span class="pl-k">&lt;</span>cell at <span class="pl-c1"><span class="pl-k">0x</span>000001CB2912DF48</span>: <span class="pl-c1">str</span> <span class="pl-c1">object</span> at <span class="pl-c1"><span class="pl-k">0x</span>000001CB291D3D70</span><span class="pl-k">&gt;</span>,)</pre>
</div>
<p>其中保存了对应自由变量的cell对象的序列,cell对象的cell_contents属性保存了变量的值:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> bar.__closure__[<span class="pl-c1">0</span>].cell_contents
<span class="pl-s"><span class="pl-pds">'</span>free var<span class="pl-pds">'</span></span></pre>
</div>
<p>这与JavaScript中闭包的行为是类似的,JavaScript中嵌套函数会将外层函数的活动对象添加到它的作用域链中。但与JavaScript不同的是,当Python函数中的全局变量或者自由变量是不可变对象(数字、字符串、元组等)时,是只能读取,无法更新的:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> a <span class="pl-k">=</span> <span class="pl-c1">1</span>
<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> <span class="pl-k">def</span> <span class="pl-en">foo</span>():
        <span class="pl-c1">print</span>(a)
        a <span class="pl-k">+=</span> <span class="pl-c1">1</span>

<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> foo()
<span class="pl-c1">UnboundLocalError</span>: local variable <span class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span> referenced before assignment

<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> <span class="pl-k">def</span> <span class="pl-en">foo</span>():
        a <span class="pl-k">=</span> <span class="pl-c1">1</span>
        <span class="pl-k">def</span> <span class="pl-en">bar</span>():
          <span class="pl-c1">print</span>(a)
          a <span class="pl-k">+=</span> <span class="pl-c1">1</span>
        <span class="pl-k">return</span> bar

<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> foo()()
<span class="pl-c1">UnboundLocalError</span>: local variable <span class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span> referenced before assignment</pre>
</div>
<p>两种情况下,都会报错。这并不是缺陷,而是Python的设计选择。Python不要求声明变量,但是会假定在函数定义体中赋值的变量是局部变量,以避免在不知情的情况下修改全局变量。</p>
<p><code>a += 1</code>与<code>a = a + 1</code>相同,编译函数的定义体时,会将a当做局部变量,不会当做自由变量保存。然后尝试获取a的值时,发现a并没有绑定值,于是报错。</p>
<p>解决这个问题的办法,一是将变量置于一些可变对象,如列表、字典中:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">foo</span>():
    ns <span class="pl-k">=</span> {}
    ns[<span class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span>] <span class="pl-k">=</span> <span class="pl-c1">1</span>
    <span class="pl-k">def</span> <span class="pl-en">bar</span>():
      ns[<span class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span>] <span class="pl-k">+=</span> <span class="pl-c1">1</span>
      <span class="pl-c1">print</span> (ns[<span class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span>])
    <span class="pl-k">return</span> bar</pre>
</div>
<p>另外的方法就是使用<strong>global</strong>或者<strong>nonlocal</strong>将变量声明为全局变量或者自由变量:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> <span class="pl-k">def</span> <span class="pl-en">foo</span>():
        a <span class="pl-k">=</span> <span class="pl-c1">1</span>
        <span class="pl-k">def</span> <span class="pl-en">bar</span>():
          <span class="pl-k">nonlocal</span> a
          a <span class="pl-k">+=</span> <span class="pl-c1">1</span>
          <span class="pl-c1">print</span>(a)
        <span class="pl-k">return</span> bar

<span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> foo()()
<span class="pl-c1">2</span></pre>
</div>
<p>当自由变量本身是可变对象时,是可以直接进行操作的:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">make_avg</span>():
    ls <span class="pl-k">=</span> []
    <span class="pl-k">def</span> <span class="pl-en">avg</span>(<span class="pl-smi">x</span>):
      ls.append(x)
      <span class="pl-c1">print</span>(<span class="pl-c1">sum</span>(ls)<span class="pl-k">/</span><span class="pl-c1">len</span>(ls))
    <span class="pl-k">return</span> avg</pre>
</div>
<h3>装饰器</h3>
<p>装饰器是可调用对象,参数一般是另一个函数。装饰器可以以某种方式增强被装饰函数的行为,然后返回被装饰的函数或者将其替换成一个新的函数。</p>
<p>一个最简单的不做任何额外行为的装饰器:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">decorate</span>(<span class="pl-smi">func</span>):
    <span class="pl-k">return</span> func</pre>
</div>
<p><code>decorate</code>函数就是一个最简单的装饰器,使用方法:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">target</span>():
    <span class="pl-k">pass</span>

target <span class="pl-k">=</span> decorate(target)</pre>
</div>
<p>Python为装饰器的使用提供了语法糖,可以简便的写为:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-en">@decorate</span>
<span class="pl-k">def</span> <span class="pl-en">target</span>():
    <span class="pl-k">pass</span></pre>
</div>
<h4>导入时运行</h4>
<p>装饰器一个很重要的特性是它是导入时(加载模块时)运行的:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">decorate</span>(<span class="pl-smi">func</span>):
    <span class="pl-c1">print</span>(<span class="pl-s"><span class="pl-pds">'</span>running decorator when import<span class="pl-pds">'</span></span>)
    <span class="pl-k">return</span> func

<span class="pl-en">@decorate</span>
<span class="pl-k">def</span> <span class="pl-en">foo</span>():
    <span class="pl-c1">print</span>(<span class="pl-s"><span class="pl-pds">'</span>running foo<span class="pl-pds">'</span></span>)
    <span class="pl-k">pass</span>

<span class="pl-k">if</span> <span class="pl-c1">__name__</span> <span class="pl-k">==</span> <span class="pl-s"><span class="pl-pds">'</span>__main__<span class="pl-pds">'</span></span>:
    <span class="pl-c1">print</span>(<span class="pl-s"><span class="pl-pds">'</span>start foo<span class="pl-pds">'</span></span>)
    foo()</pre>
</div>
<p>结果:</p>
<div class="highlight highlight-source-python">
<pre>running decorator when <span class="pl-k">import</span>
start foo
running foo</pre>
</div>
<p>可以看到,装饰器是导入时运行的,而被装饰的函数是明确调用时运行的。</p>
<p>装饰器可以返回被装饰的函数本身,和运行时导入的特性结合起来,可以实现简单的注册器功能:</p>
<div class="highlight highlight-source-python">
<pre>view_registry <span class="pl-k">=</span> []

<span class="pl-k">def</span> <span class="pl-en">register</span>(<span class="pl-smi">func</span>):
    view_registry.append(func)
    <span class="pl-k">return</span> func

<span class="pl-en">@register</span>
<span class="pl-k">def</span> <span class="pl-en">view1</span>():
    <span class="pl-k">pass</span>

<span class="pl-en">@register</span>
<span class="pl-k">def</span> <span class="pl-en">view2</span>():
    <span class="pl-k">pass</span>

<span class="pl-k">def</span> <span class="pl-en">main</span>():
    <span class="pl-c1">print</span>(view_registry)


<span class="pl-k">if</span> <span class="pl-c1">__name__</span> <span class="pl-k">==</span> <span class="pl-s"><span class="pl-pds">'</span>__main__<span class="pl-pds">'</span></span>:
    main()</pre>
</div>
<h4>返回新函数</h4>
<p>上述装饰器的例子都返回了被装饰的原函数,但装饰器的典型行为还是返回一个新函数:把被装饰的函数替换成新函数,新函数接受与原函数相同的参数,并且返回原函数本该返回的值。写法类似于:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">deco</span>(<span class="pl-smi">func</span>):
    <span class="pl-k">def</span> <span class="pl-en">new_func</span>(<span class="pl-k">*</span><span class="pl-smi">args</span>, <span class="pl-k">**</span><span class="pl-smi">kwargs</span>):
      <span class="pl-k">return</span> func(<span class="pl-k">*</span>args, <span class="pl-k">**</span>kwargs)
    <span class="pl-k">return</span> new_func</pre>
</div>
<p>这种情况下装饰器就使用到了闭包。JavaScript中的防抖与节流函数就是这种典型的装饰器行为。新函数一般会使用外部装饰器函数中的变量当做自由变量,对函数作出某种增强行为。</p>
<p>举个例子,我们知道,当Python函数的参数是个可变对象时,会产生意料之外的行为:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">foo</span>(<span class="pl-smi">x</span>, <span class="pl-smi">y</span><span class="pl-k">=</span>[]):
    y.append(x)
    <span class="pl-c1">print</span>(y)

foo(<span class="pl-c1">1</span>)
foo(<span class="pl-c1">2</span>)
foo(<span class="pl-c1">3</span>)</pre>
</div>
<p>输出:</p>
<div class="highlight highlight-source-python">
<pre>[<span class="pl-c1">1</span>]
[<span class="pl-c1">1</span>, <span class="pl-c1">2</span>]
[<span class="pl-c1">1</span>, <span class="pl-c1">2</span>, <span class="pl-c1">3</span>]</pre>
</div>
<p>这是因为,函数的参数默认值保存在__defaults__属性中,指向了同一个列表:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">&gt;&gt;</span><span class="pl-k">&gt;</span> foo.<span class="pl-c1">__defaults__</span>
([<span class="pl-c1">1</span>, <span class="pl-c1">2</span>, <span class="pl-c1">3</span>],)</pre>
</div>
<p>我们就可以用一个装饰器在函数执行前取出默认值做深复制,然后覆盖函数原先的参数默认值:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">import</span> copy

<span class="pl-k">def</span> <span class="pl-en">fresh_defaults</span>(<span class="pl-smi">func</span>):
    defaults <span class="pl-k">=</span> func.<span class="pl-c1">__defaults__</span>
    <span class="pl-k">def</span> <span class="pl-en">deco</span>(<span class="pl-k">*</span><span class="pl-smi">args</span>, <span class="pl-k">**</span><span class="pl-smi">kwargs</span>):
      func.<span class="pl-c1">__defaults__</span> <span class="pl-k">=</span> copy.deepcopy(defaults)
      <span class="pl-k">return</span> func(<span class="pl-k">*</span>args, <span class="pl-k">**</span>kwargs)
    <span class="pl-k">return</span> deco

<span class="pl-en">@fresh_defaults</span>
<span class="pl-k">def</span> <span class="pl-en">foo</span>(<span class="pl-smi">x</span>, <span class="pl-smi">y</span><span class="pl-k">=</span>[]):
    y.append(x)
    <span class="pl-c1">print</span>(y)

foo(<span class="pl-c1">1</span>)
foo(<span class="pl-c1">2</span>)
foo(<span class="pl-c1">3</span>)</pre>
</div>
<p>输出:</p>
<pre><code>


</code></pre>
<h4>接收参数的装饰器</h4>
<p>装饰器除了可以接受函数作为参数外,还可以接受其他参数。使用方法是:创建一个装饰器工厂,接受参数,返回一个装饰器,再把它应用到被装饰的函数上,语法如下:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">deco_factory</span>(<span class="pl-k">*</span><span class="pl-smi">args</span>, <span class="pl-k">**</span><span class="pl-smi">kwargs</span>):
    <span class="pl-k">def</span> <span class="pl-en">deco</span>(<span class="pl-smi">func</span>):
      <span class="pl-c1">print</span>(args)
      <span class="pl-k">return</span> func
    <span class="pl-k">return</span> deco

<span class="pl-en">@deco_factory</span>(<span class="pl-s"><span class="pl-pds">'</span>factory<span class="pl-pds">'</span></span>)
<span class="pl-k">def</span> <span class="pl-en">foo</span>():
    <span class="pl-k">pass</span></pre>
</div>
<p>在Web框架中,通常要将URL模式映射到生成响应的view函数,并将view函数注册到某些中央注册处。之前我们曾经实现过一个简单的注册装饰器,只是注册了view函数,却没有URL映射,是远远不够的。</p>
<p>在Flask中,注册view函数需要一个装饰器:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-en">@app.route</span>(<span class="pl-s"><span class="pl-pds">'</span>/hello<span class="pl-pds">'</span></span>)
<span class="pl-k">def</span> <span class="pl-en">hello</span>():
    <span class="pl-k">return</span> <span class="pl-s"><span class="pl-pds">'</span>Hello, World<span class="pl-pds">'</span></span></pre>
</div>
<p>原理就是使用了装饰器工厂,可以简单的模拟一下实现:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">class</span> <span class="pl-en">App</span>:
    <span class="pl-k">def</span> <span class="pl-c1">__init__</span>(<span class="pl-smi"><span class="pl-smi">self</span></span>):
      <span class="pl-c1">self</span>.view_functions <span class="pl-k">=</span> {}

    <span class="pl-k">def</span> <span class="pl-en">route</span>(<span class="pl-smi"><span class="pl-smi">self</span></span>, <span class="pl-smi">rule</span>):
      <span class="pl-k">def</span> <span class="pl-en">deco</span>(<span class="pl-smi">view_func</span>):
            <span class="pl-c1">self</span>.view_functions <span class="pl-k">=</span> view_func
            <span class="pl-k">return</span> view_func
      <span class="pl-k">return</span> deco
         
app <span class="pl-k">=</span> App()

<span class="pl-en">@app.route</span>(<span class="pl-s"><span class="pl-pds">'</span>/<span class="pl-pds">'</span></span>)
<span class="pl-k">def</span> <span class="pl-en">index</span>():
    <span class="pl-k">pass</span>

<span class="pl-en">@app.route</span>(<span class="pl-s"><span class="pl-pds">'</span>/hello<span class="pl-pds">'</span></span>)
<span class="pl-k">def</span> <span class="pl-en">hello</span>():
    <span class="pl-k">pass</span>

<span class="pl-k">for</span> rule, view <span class="pl-k">in</span> app.view_functions.items():
    <span class="pl-c1">print</span>(rule, <span class="pl-s"><span class="pl-pds">'</span>:<span class="pl-pds">'</span></span>, view.<span class="pl-c1">__name__</span>)</pre>
</div>
<p>输出:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">/</span> : index
<span class="pl-k">/</span>hello : hello</pre>
</div>
<p>还可以使用装饰器工厂来确定view函数可以允许哪些HTTP请求方法:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">def</span> <span class="pl-en">action</span>(<span class="pl-smi">methods</span>):
    <span class="pl-k">def</span> <span class="pl-en">deco</span>(<span class="pl-smi">view</span>):
      view.allow_methods <span class="pl-k">=</span>
      <span class="pl-k">return</span> view
    <span class="pl-k">return</span> deco
         
<span class="pl-en">@action</span>([<span class="pl-s"><span class="pl-pds">'</span>GET<span class="pl-pds">'</span></span>, <span class="pl-s"><span class="pl-pds">'</span>POST<span class="pl-pds">'</span></span>])
<span class="pl-k">def</span> <span class="pl-en">view</span>(<span class="pl-smi">request</span>):
    <span class="pl-k">if</span> request.method.lower() <span class="pl-k">in</span> view.allow_methods:
      <span class="pl-c1">...</span></pre>
</div>
<h4>重叠的装饰器</h4>
<p>装饰器也是可以重叠使用的:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-en">@d1</span>
<span class="pl-en">@d2</span>
<span class="pl-k">def</span> <span class="pl-en">foo</span>():
    <span class="pl-k">pass</span></pre>
</div>
<p>等同于:</p>
<div class="highlight highlight-source-python">
<pre>foo <span class="pl-k">=</span> d1(d2(foo))</pre>
</div>
<h4>类装饰器</h4>
<p>装饰器的参数也可以是一个类,也就是说,装饰器可以装饰类:</p>
<div class="highlight highlight-source-python">
<pre><span class="pl-k">import</span> types

<span class="pl-k">def</span> <span class="pl-en">deco</span>(<span class="pl-smi"><span class="pl-smi">cls</span></span>):
    <span class="pl-k">for</span> key, method <span class="pl-k">in</span> <span class="pl-c1">cls</span>.<span class="pl-c1">__dict__</span>.items():
      <span class="pl-k">if</span> <span class="pl-c1">isinstance</span>(method, types.FunctionType):
            <span class="pl-c1">print</span>(key, <span class="pl-s"><span class="pl-pds">'</span>:<span class="pl-pds">'</span></span>, method.<span class="pl-c1">__name__</span>)
    <span class="pl-k">return</span> <span class="pl-c1">cls</span>

<span class="pl-en">@deco</span>
<span class="pl-k">class</span> <span class="pl-en">Test</span>:
    <span class="pl-k">def</span> <span class="pl-c1">__init__</span>(<span class="pl-smi"><span class="pl-smi">self</span></span>):
      <span class="pl-k">pass</span>

    <span class="pl-k">def</span> <span class="pl-en">foo</span>(<span class="pl-smi"><span class="pl-smi">self</span></span>):
      <span class="pl-k">pass</span></pre>
</div><br><br>
来源:https://www.cnblogs.com/linxiyue/p/11224322.html
頁: [1]
查看完整版本: Python装饰器与闭包