流暢的python讀書筆記-第七章-函數裝飾器和閉包

函數裝飾器和閉包

嚴格來講,裝飾器只是語法糖。如前所示,裝飾器能夠像常規的可調用
對象那樣調用,其參數是另外一個函數。有時,這樣作更方便,尤爲是作
元編程(在運行時改變程序的行爲)時。html

Python什麼時候執行裝飾器

它們在被裝飾的函數定義以後當即運行。這一般是在導入時(即 Python 加載模塊時)python

registry = []

def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func

@register
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

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

def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__=='__main__':
    main()

把 registration.py 看成腳本運行獲得的輸出以下:編程

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

若是導入 registration.py 模塊(不做爲腳本運行),輸出以下:

>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

此時查看 registry 的值,獲得的輸出以下:緩存

>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]

裝飾器在真實代碼中的經常使用方式

裝飾器函數與被裝飾的函數在同一個模塊中定義。實際狀況是,裝
飾器一般在一個模塊中定義,而後應用到其餘模塊中的函數上。閉包

使用裝飾器改進「策略」模式

promos = []


def promotion(promo_func):
    promos.append(promo_func)
    return


@promotion
def fidelity(order):
    """爲積分爲1000或以上的顧客提供5%折扣"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


@promotion
def bulk_item(order):
    """單個商品爲20個或以上時提供10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def large_order(order):
    """訂單中的不一樣商品達到10個或以上時提供7%折扣"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0


def best_promo(order):
    """選擇可用的最佳折扣"""
    return max(promo(order) for promo in promos)
  • promotion 把 promo_func 添加到 promos 列表中,而後原封不動地將其返回。
  • 被 @promotion 裝飾的函數都會添加到 promos 列表中。

與 6.1 節給出的方案相比,這個方案有幾個優勢。

  • 促銷策略函數無需使用特殊的名稱(即不用以 _promo 結尾)。
  • @promotion 裝飾器突出了被裝飾的函數的做用,還便於臨時禁用
  • 某個促銷策略:只需把裝飾器註釋掉。
  • 促銷折扣策略能夠在其餘模塊中定義,在系統中的任何地方都行,只要使用 @promotion 裝飾便可。

變量做用域規則

神奇的例子app

>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
b = 9###### 它判斷 b 是局部變量,由於在函數中給它賦值了
  • 可事實是,Python 編譯函數的定義體時,它判斷 b 是局部變量,由於在函數中給它賦值了。
  • 生成的字節碼證明了這種判斷,Python 會嘗試從本地環境獲取 b。
  • 後面調用 f2(3) 時, f2 的定義體會獲取並打印局部變量 a 的值,可是嘗試獲取局部變量 b 的值時,發現 b 沒有綁定值。

爲何會這樣

  • 這不是缺陷,而是設計選擇:Python 不要求聲明變量,可是假定在函數定義體中賦值的變量是局部變量。
  • 這比 JavaScript 的行爲好多了,JavaScript 也不要求聲明變量,可是若是忘記把變量聲明爲局部變量使用 var),可能會在不知情的狀況下獲取全局變量。

利用global就能夠啦ide

>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6

閉包

人們有時會把閉包和匿名函數弄混。
這是有歷史緣由的:在函數內部定義函數不常見,直到開始使用匿名函數纔會這樣作,模塊化

注意:

  • 函數是否是匿名的沒有關係,關鍵是它能訪問定義體以外定義的非全局變量。
  • 只有涉及嵌套函數時纔有閉包問題。所以,不少人是同時知道這兩個概念的。

案例

假若有個名爲 avg 的函數,它的做用是計算不斷增長的系列值的均值;函數

初學者可能會這樣性能

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)
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

函數式實現,使用高階函數 make_averager。

def make_averager():
    series = []

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

    return averager

重要概念

在 averager 函數中,series 是自由變量(free variable)。這是一個
技術術語,指未在本地做用域中綁定的變量.

clipboard.png

審查 make_averager(見示例 7-9)建立的函數

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
  • series 的綁定在返回的 avg 函數的 closure 屬性中。
  • avg.__closure__ 中的各個元素對應於avg.__code__.co_freevars 中的一個名稱。
  • 這些元素是 cell 對象,有個 cell_contents 屬性,保存着真正的值。
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

小總結

  • 綜上,閉包是一種函數,它會保留定義函數時存在的自由變量的綁定,
  • 這樣調用函數時,雖然定義做用域不可用了,可是仍能使用那些綁定。
  • 注意,只有嵌套在其餘函數中的函數纔可能須要處理不在全局做用域中的外部變量。

nonlocal聲明

計算移動平均值的高階函數,不保存全部歷史值,但有
缺陷
def make_averager():
    count = 0
    total = 0

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

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

注意

示例 7-9 沒遇到這個問題,由於咱們沒有給 series 賦值,咱們只是調
用 series.append,並把它傳給 sum 和 len。也就是說,咱們利用了
列表是可變的對象這一事實。

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

解決這個問題

爲了解決這個問題,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

對付沒有 nonlocal 的 Python 2

基本上,這種處理方式是把內部函數須要修改
的變量(如 count 和 total)存儲爲可變對象(如字典或簡單的
實例)的元素或屬性,而且把那個對象綁定給一個自由變量。

實現一個簡單的裝飾器

import time


def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        args_str = ''.join(repr(arg) for arg in args)

        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, args_str, result))
        return result

    return clocked


@clock
def snooze(seconds):
    time.sleep(seconds)


@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)


if __name__ == "__main__":
    print("*" * 40)
    snooze(0.123)
    print("*" * 40)
    factorial(6)

    ## 這裏的函數對象變成了從clocked
    print(factorial.__name__)

這是裝飾器的典型行爲:把被裝飾的函數替換成新函數,兩者接受相同的參數,並且(通

常)返回被裝飾的函數本該返回的值,同時還會作些額外操做。

上面實現的 clock 裝飾器有幾個缺點:不支持關鍵字參數,並且遮蓋了被裝飾函
數的 namedoc 屬性。

使用 functools.wraps 裝飾器把相關的屬性從 func 複製到 clocked 中。此外,這個新版還能正確處理關鍵字參數。
import time
import functools


def clock(func):
    @functools.wraps(func)  ###這裏 保留__name__ 和 __doc__ 屬性
    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

標準庫中的裝飾器

functools.lru_cache 是很是實用的裝飾器,它實現了備忘(memoization)功能。
就是更加利用緩存幹活

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





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


if __name__ == '__main__':
    print(fibonacci(6))
浪費時間的地方很明顯:fibonacci(1) 調用了 8 次,fibonacci(2) 調用了 5 次……可是,若是增長兩行代碼,使用 lru_cache,性能會顯著改善,
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


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


if __name__ == '__main__':
    print(fibonacci(6))
❶ 注意,必須像常規函數那樣調用 lru_cache。這一行中
有一對括
號:@functools.lru_cache()。這麼作的緣由是,lru_cache 能夠接受配置參數,稍
後說明。

lru_cache 可使用兩個可選的參數來配置。

functools.lru_cache(maxsize=128, typed=False)
  • maxsize 參數指定存儲多少個調用的結果。
  • 緩存滿了以後,舊的結果會被扔掉,騰出空間。
  • 爲了獲得最佳性能,maxsize 應該設爲 2 的冪。typed 參數若是設爲 True,把不一樣
  • 參數類型獲得的結果分開保存,即把一般認爲相等的浮點數和整數參數(如 1 和 1.0)區

分開。

functools.singledispatch 裝飾器 讓Python強行支持重載方法

由於 Python 不支持重載方法或函數,因此咱們不能使用不一樣的簽名定義 htmlize 的變體,也沒法使用不一樣的方式處理不一樣的數據類型。

使用 @singledispatch 裝飾的普通函數會變成泛函數(generic function):根據第一個參數的類型,以不一樣方式執行相同操做的一組函數。

讓Python強行支持重載方法,再也不使用一串 if/elif/elif,調用專門的函數

singledispatch 建立一個自定義的 htmlize.register 裝飾器,把多

個函數綁在一塊兒組成一個泛函數

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


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


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


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


@htmlize.register(tuple)
@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>'

print(htmlize({1, 2, 3}))

print(htmlize(abs))

print(htmlize('Heimlich & Co.\n- a game'))
print(htmlize(42))

##這個強啊!!!
print(htmlize(['alpha', 66, {3, 2, 1}]))

❷ 各個專門函數使用 @«base_function».register(«type») 裝飾。
❸ 專門函數的名稱可有可無;_ 是個不錯的選擇,簡單明瞭。
爲每一個須要特殊處理的類型註冊一個函數。numbers.Integral 是 int 的虛擬超類。
❺ 能夠疊放多個 register 裝飾器,讓同一個函數支持不一樣類型。

只要可能,註冊的專門函數應該處理抽象基類(如 numbers.Integral 和
abc.MutableSequence),不要處理具體實現(如 int 和 list)。
這樣,代碼支持的兼容類型更普遍。

例如,Python 擴展能夠子類化 numbers.Integral,使用固定的位數實
現 int 類型。

注意:

  1. @singledispatch 不是爲了把 Java 的那種方法重載帶入 Python。在一個類中 爲同一個方法定義多個重載變體,
  2. @singledispath 的優勢是支持模塊化擴展:各個模塊能夠爲它支持的各個類型註冊一個專門函數。

疊放裝飾器

@d1
@d2
def f():
    print('f')

等同於

def f():
    print('f')
f = d1(d2(f))

一個參數化的註冊裝飾器

爲了便於啓用或禁用 register 執行的函數註冊功能,咱們爲它提供一個可選的 active
參數,設爲 False 時,不註冊被裝飾的函數。

registry = set()


def register(active=True):
    def decorate(func):
        print('running register(active=%s)->decorate(%s)'
              % (active, func))
        if active:
            registry.add(func)
        else:
            registry.discard(func)
            return func

    return decorate


@register(active=False)
def f1():
    print('running f1()')

@register()
def f2():
    print('running f2()')

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

if __name__ =="__main__":
    print(registry)
參數化裝飾器一般會把被裝飾的函數替換掉,並且結構上須要多一層嵌套。

參數化clock裝飾器

import time

DEFAULT_FMT = '花費時間:[{elapsed:0.5f}s] 程序名:{name}  參數:({args}) -> 結果:{result}'


def clock(fmt=DEFAULT_FMT):
    def decorate(func):
        def clocked(*_args):
            t0 = time.time()
            _result = func(*_args)

            ### locals() 局部變量
            elapsed = time.time() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
                # 這裏不知道他爲何這麼能用
            print(fmt.format(**locals()))

            return _result

        return clocked

    return decorate


if __name__ == '__main__':

    # ## 第一種狀況
    # @clock()
    # def snooze(seconds):
    #     time.sleep(seconds)


    ## 第二種狀況
    # @clock('程序名:{name}: 花費時間:{elapsed}s')
    # def snooze(seconds):
    #     time.sleep(seconds)

    ## 第三種狀況
    @clock('程序名:{name}  參數:({args})  花費時間:dt={elapsed:0.3f}s')
    def snooze(seconds):
        time.sleep(seconds)



    snooze(0.123)

clock 是參數化裝飾器工廠函數
❷ decorate 是真正的裝飾器。
❸ clocked 包裝被裝飾的函數。
❹ _result 是被裝飾的函數返回的真正結果

這裏的locals()是啥不知道

def runnoob(arg:'int'):
    z = 1
    print(arg + 1)

    # 返回字典類型的局部變量。
    print('==='*30)
    print(locals())

    # 返回字典類型的所有變量。
    print('=' * 50)
    print(globals())



num = 8
runnoob(num)

小總結

  • 嚴格來講,裝飾器只是語法糖。
  • 它們在被裝飾的函數定義以後當即運行。這一般是在導入時(即 Python 加載模塊時)
  • 裝飾器改進了策略模式

閉包

  • 閉包是一種函數,它會保留定義函數時存在的自由變量的綁定
  • Python 3 引入了 nonlocal 聲明。它的做用是把變

量標記爲自由變量

  • functools.wraps 裝飾器把相關的屬性從 func 複製到 clocked 中。此外,這個新版還能正確處理關鍵字參數

functools.lru_cache 是很是實用的裝飾器,它實現了備忘(memoization)功能。就是更加利用緩存幹活

functools.singledispatch 裝飾器 讓Python強行支持重載方法

locals() globals()

  • *locals
  • ** locals()
相關文章
相關標籤/搜索