python 裝飾器(一)

來源:《流暢的Python》

1. 裝飾器基礎知識

@decorate
def target():
    print('running target()')

等價於:html

def target():
    print('running target()')

target = decorate(target)

最終結果是一樣的,可是target所指向的對象可能會發生變化。python

提示 @只不過是一個語法糖,它的做用正如上面的代碼所展現的一樣。那麼,帶參數的裝飾器是什麼個情況呢?
@out(args)  # 首先執行out(args)函數,返回裏面的函數,接下來跟普通的裝飾器一樣。
def func():
    pass

裝飾器的一大特性是,能把被裝飾的函數替換成其餘函數。第二個特性是,裝飾器在加載模塊時當即執行。緩存

2. 什麼時候執行裝飾器

在導入包時,進行初始化,而不用直接對其調用。閉包

3. 變量做用域規則

In [1]: def f1(a):
   ...:     print(a)
   ...:     print(b)
   ...:

In [2]: f1(3)
3
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-2-db0f80b394ed> in <module>()
----> 1 f1(3)

<ipython-input-1-c1318c6d0711> in f1(a)
      1 def f1(a):
      2     print(a)
----> 3     print(b)
      4

NameError: name 'b' is not defined

In [5]: b = 6

In [6]: f1(3)
3
6
In [7]: b = 6

In [8]: def f2(a):
   ...:     print(a)
   ...:     print(b)
   ...:     b = 9
   ...:

In [9]: f2(3)
3
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-9-ddde86392cb4> in <module>()
----> 1 f2(3)

<ipython-input-8-2304a03d7bfd> in f2(a)
      1 def f2(a):
      2     print(a)
----> 3     print(b)
      4     b = 9
      5

UnboundLocalError: local variable 'b' referenced before assignment

爲什麼會出現這種情況呢?這是因爲:app

Python 不要求聲明變量,可是假定在函數定義體中 賦值的變量是局部變量。賦值包含: =, +=等。

4. 閉包

閉包指延伸了做用域的函數,其中包含函數定義體中引用、可是不在定義體中定義的非全局變量。函數是否是匿名的沒有關係,關鍵是它能訪問定義體以外定義的非全局變量。模塊化

下面兩者是等價的:函數

class Averager():

    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)
def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)

    return averager
  1. series 是 make_averager 函數的局部變量;
  2. 調用 avg(10) 時,make_averager 函數已經返回了,而它的本地做用域也一去不復返了。
  3. 在 averager 函數中,series 是自由變量(free variable)。
  4. 這也是一個優化的技巧。由於類的開銷太大了,對於只有一個函數和一些變量的類,能夠改爲函數來實現。
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

>>> avg.__code__.co_freevars
('series',)
# avg.__closure__ 中的各個元素對應於 avg.__code__.co_freevars 中的一個名稱。
>>> avg.__closure__ 
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

5. nonlocal聲明

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

這段代碼有問題,有看出來問題在哪兒碼?優化

Python 不要求聲明變量,可是假定在函數定義體中賦值的變量是局部變量。
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

當 count 是數字或任何不可變類型時,count += 1 語句的做用其實與 count= count + 1 同樣。所以,咱們在 averager 的定義體中爲 count 賦值了,這會把 count 變成局部變量。total 變量也受這個問題影響。spa

可是對數字、字符串、元組等不可變類型來講,只能讀取,不能更新。若是嘗試從新綁定,例如 count = count + 1,其實會隱式建立局部變量 count。這樣,count 就不是自由變量了,所以不會保存在閉包中。3d

爲了解決這個問題,Python 3 引入了 nonlocal 聲明。它的做用是把變量標記爲自由變量,即便在函數中爲變量賦予新值了,也會變成自由變量。若是爲 nonlocal 聲明的變量

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

賦予新值,閉包中保存的綁定會更新。

6. functools.wraps 裝飾器

若是不加該裝飾器,則不支持關鍵字參數,並且遮蓋了被裝飾函數的 __name____doc__ 屬性。

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
        arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
    return result
return clocked

7. 使用functools.lru_cache作備忘

這是一項優化技術,它把耗時的函數的結果保存起來,避免傳入相同的參數時重複計算。LRU三個字母是「Least Recently Used」的縮寫,代表緩存不會無限制增加,一段時間不用的緩存條目會被扔掉。

from clockdeco import clock


@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
    print(fibonacci(6))

輸出:

[0.00000143s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00006199s] fibonacci(2) -> 1
...
[0.00026441s] fibonacci(6) -> 8
8

import functools

from clockdeco import clock


@functools.lru_cache()  # <1>
@clock  # <2>
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
    print(fibonacci(6))

輸出:

[0.00000072s] fibonacci(0) -> 0
[0.00000143s] fibonacci(1) -> 1
[0.00006676s] fibonacci(2) -> 1
[0.00000215s] fibonacci(3) -> 2
[0.00009227s] fibonacci(4) -> 3
[0.00000143s] fibonacci(5) -> 5
[0.00011635s] fibonacci(6) -> 8
8

lru_cache 可使用兩個可選的參數來配置。它的簽名是:

functools.lru_cache(maxsize=128, typed=False)

其中:

  • maxsize:應爲2的賠數。
  • typed:爲True時,f(1)於f(1.0)視爲不一樣的情況。

8. 單分派泛函數

from functools import singledispatch
from collections import abc
import numbers
import html


@singledispatch  # <1>
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)


@htmlize.register(str)  # <2>
def _(text):  # <3>
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)


@htmlize.register(numbers.Integral)  # <4>
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)


@htmlize.register(tuple)  # <5>
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

@singledispatch 不是爲了把 Java 的那種方法重載帶入 Python。在一個類中 爲同一個方法定義多個重載變體,比在一個函數中使用一長串 if/elif/elif/elif 塊要更好。可是這兩種方案都有缺陷,由於它們讓代碼單元(類或函數)承擔的職責太多。@singledispath的優勢是支持模塊化擴展: 各個模塊能夠爲它支持的各個類型註冊一個專門函數。

這個機制最好的文檔是「PEP 443 — Single-dispatch genericfunctions

9. 參數化裝飾器

建立一個裝飾器工廠函數,把參數傳給它,返回一個裝飾器,而後再把它應用到要裝飾的函數上。
registry = set()  # <1>


def register(active=True):  # <2>
    def decorate(func):  # <3>
        print('running register(active=%s)->decorate(%s)'
              % (active, func))
        if active:  # <4>
            registry.add(func)
        else:
            registry.discard(func)  # <5>

        return func  # <6>

    return decorate  # <7>


@register(active=False)  # <8>等於 @decorate
def f1():
    print('running f1()')


@register()  # <9>
def f2():
    print('running f2()')


def f3():
    print('running f3()')

import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'


def clock(fmt=DEFAULT_FMT):  # <1>
    def decorate(func):  # <2>
        def clocked(*_args):  # <3>
            t0 = time.time()
            _result = func(*_args)  # <4>
            elapsed = time.time() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)  # <5>
            result = repr(_result)  # <6>
            print(fmt.format(**locals()))  # <7>
            return _result  # <8>

        return clocked  # <9>

    return decorate  # <10>


if __name__ == '__main__':

    @clock()  # <11>
    def snooze(seconds):
        time.sleep(seconds)


    for i in range(3):
        snooze(.123)
相關文章
相關標籤/搜索