天天看點

Python裝飾器進階文法學以緻用一、應用案例二、參考資料

文章目錄

  • 一、應用案例
    • 1. 再議為代碼減速
    • 2. 建立單例的對象
    • 3. 緩存函數傳回值
    • 4. 添加機關資訊
    • 5. 驗證JSON資料
  • 二、參考資料

在Python裝飾器文法進階中,我們學習了Python裝飾器的進階文法,那麼這些看上去花裡胡哨的文法究竟有何用途,如何才能加深對這些文法的了解并應用于實際的代碼中,這是本文即将要探讨的問題。

一、應用案例

1. 再議為代碼減速

在Python裝飾器入門與簡單應用的代碼減速案例中,裝飾器

@slow_down

總是讓被裝飾函數休眠一秒,而在Python裝飾器文法進階中我們知道了如何為裝飾器添加參數,是以下面重寫

@slow_down

代碼,使之可以接收一個可選參數

rate

,該參數用以指定被裝飾函數

countdown()

的休眠時間長度:

import functools
import time


def slow_down(_func=None, rate=1):
    """使得程式在被調用之前休眠由rate指定的時間"""

    def decorator_slow_down(func):
        @functools.wraps(func)
        def wrapper_slow_down(*args, **kwargs):
            time.sleep(rate)
            return func(*args, **kwargs)

        return wrapper_slow_down

    if _func is None:
        return decorator_slow_down
    else:
        return decorator_slow_down(_func)  # 1


@slow_down(rate=2)
def countdown(from_number):
    if from_number < 1:
        print("發射!")
    else:
        print(from_number)
        countdown(from_number - 1)


def main():
    countdown(5)


if __name__ == '__main__':
    main()

           

對于上述代碼,需要說明的是,當不為裝飾器傳參數,即使用

@slow_down

直接裝飾函數時,此文法等價于

countdown = slow_down(countdown)

,則被裝飾函數的引用會被傳遞至

_func

處,此時

_func

不為

None

,則

slow_down()

函數會在

# 1

處傳回,此時

# 1

相當于

return wrapper_slow_down

,即使用

@slow_down

和使用

@decorator_slow_down

實作的效果一樣。

至于為什麼不能直接在

# 1

處寫成

return wrapper_slow_down

,原因在于:當使用

@slow_down

執行裝飾操作時,

wrapper_slow_down()

函數因為在最内部還未被定義,故此時編譯器無法找到該變量。

2. 建立單例的對象

所謂單例是指這樣一個類,該類隻有一個執行個體,即無論使用該類建立多少次執行個體對象,傳回的都是同一個執行個體。

實際上,在Python中你經常使用幾個單例,如:

None

True

以及

False

,同時也因為

None

是一個單例,是以你才可以使用

is

關鍵字來比較

None

和另外一個對象,而隻有當待比較的兩個對象是統一個類的執行個體時,

is

關鍵字的傳回值才為

True

下面的

@singleton

裝飾器就将一個類變成了一個單例,其實作方式為:

  • 将該類的第一個執行個體儲存為内層函數對象

    wrapper_singleton

    的一個屬性;
  • 後續建立執行個體時都傳回上述屬性中儲存的執行個體的引用。
import functools


def singleton(cls):
    @functools.wraps(cls)
    def wrapper_singleton(*args, **kwargs):
        if not wrapper_singleton.instance:
            wrapper_singleton.instance = cls(*args, **kwargs)  # 4
        return wrapper_singleton.instance

    wrapper_singleton.instance = None  # 1
    print("-" * 30)  # 2
    return wrapper_singleton  # 3


@singleton  # TheOne = singleton(TheOne) = wrapper_singleton
class TheOne(object):
    pass


def main():
    first_one = TheOne()
    second_one = TheOne()

    print(first_one is second_one)
    print("id(first_one) = ", id(first_one))
    print("id(second_one) = ", id(second_one))


if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

------------------------------

first_one is second_one = True

id(first_one) = 139627414325064

id(second_one) = 139627414325064

由上述運作結果可知,

@singleton

裝飾器的确将類

TheOne

變成了一個單例。

為了更好的了解上述代碼,有以下幾點需要說明:

  • 執行

    @singleton

    時,

    # 1

    # 2

    # 3

    處的代碼立即被執行,這也是為什麼上述運作結果會先列印出30個

    -

    符号;
  • 當通過

    TheOne()

    的方式建立執行個體對象時,由于裝飾器的作用,此時

    TheOne()

    等價于

    wrapper_singleton()

    • 當第一次建立執行個體時,由于

      if

      條件判斷為真,則建立執行個體,且該執行個體的引用賦給了

      wrapper_singleton.instance

    • 當後續再建立執行個體時,由于

      if

      條件判斷為假,則直接傳回屬性

      wrapper_singleton.instance

      指向的執行個體對象引用。

3. 緩存函數傳回值

裝飾器可以提供優秀的緩存機制。為進行具體闡述,下面代碼先通過遞歸方式定義了一個斐波那契數列函數,并使用類CountCalls作為裝飾器統計求取指定序号數列的值時斐波那契數列函數被調用的次數:

import functools


class CountCalls(object):
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)


@CountCalls  # fibonacci = CountCalls(fibonacci)
def fibonacci(num):
    if num < 2:
        return num  # 當num = 0時,fibonacci(0) = 0;當num = 1時,fibonacci(1) = 1;當num = 2時,fibonacci(2) = 1
    return fibonacci(num - 1) + fibonacci(num - 2)


def main():
    print(fibonacci(5))
    

if __name__ == '__main__':
    main()

           

上述代碼的運作結果為:

Call 1 of ‘fibonacci’

Call 2 of ‘fibonacci’

Call 3 of ‘fibonacci’

Call 4 of ‘fibonacci’

Call 5 of ‘fibonacci’

Call 6 of ‘fibonacci’

Call 7 of ‘fibonacci’

Call 8 of ‘fibonacci’

Call 9 of ‘fibonacci’

Call 10 of ‘fibonacci’

Call 11 of ‘fibonacci’

Call 12 of ‘fibonacci’

Call 13 of ‘fibonacci’

Call 14 of ‘fibonacci’

Call 15 of ‘fibonacci’

5

即為了計算斐波那契數列的第5項,上述fibonacci()函數被調用了15次,當計算第10項時,調用次數到了177,再當計算第15項時,調用次數竟然增加至了1973次,造成如此快速地次數增加,是因為上述代碼每次都會重新遞歸計算所有前序項,如下圖所示:

Python裝飾器進階文法學以緻用一、應用案例二、參考資料

下面通過對前序計算得出的數列項進行緩存來改善上面代碼的弊端:

import functools


class CountCalls(object):
	def __init__(self, func):  # 1.2 func = fibonacci
		functools.update_wrapper(self, func)
		self.func = func
		self.num_calls = 0

	def __call__(self, *args, **kwargs):  # 5
		self.num_calls += 1
		print(f"Call {self.num_calls} of {self.func.__name__!r}")
		return self.func(*args, **kwargs)  # 6


def cache(func):  # 2.2 此時func = CountCalls(fibonacci)
	"""緩存已計算過的斐波那契數列項的值"""

	@functools.wraps(func)
	def wrapper_cache(*args, **kwargs):
		cache_key = args + tuple(kwargs.items())
		if cache_key not in wrapper_cache.cache:
			wrapper_cache.cache[cache_key] = func(*args, **kwargs)  # 4.CountCalls(fibonacci)(*args, **kwargs)
		return wrapper_cache.cache[cache_key]

	wrapper_cache.cache = dict()
	return wrapper_cache


@cache  # 2.1 fibonacci = cache(CountCalls(fibonacci)) --> fibonacci = wrapper_cache
@CountCalls  # 1.1 fibonacci = CountCalls(fibonacci)
def fibonacci(num):
	if num < 2:
		return num  # 當num = 0時,fibonacci(0) = 0;當num = 1時,fibonacci(1) = 1;當num = 2時,fibonacci(2) = 1
	return fibonacci(num - 1) + fibonacci(num - 2)


def main():
	print(fibonacci(5))  # 3. wrapper_cache(5)


if __name__ == '__main__':
	main()

           

4. 添加機關資訊

5. 驗證JSON資料

二、參考資料

  • [1] Primer on Python Decorators