Python裝飾器----應用示例(一)

寫裝飾器

裝飾器只不過是一種函數,接收被裝飾的可調用對象作爲它的唯一參數,然後返回一個可調用對象(就像前面的簡單例子)
注意重要的一點,當裝飾器被應用到被裝飾函數上時,裝飾器代碼本身就會運行,而不是當被裝飾函數被調用時.理解這個很關鍵,接下來的幾個例子的講解過程也會變得很清楚

第一個例子: 函數註冊

看下面簡單的函數註冊:

registry = []
def register(decorated):
  registry.append(decorated)
  return decorated

註冊器方法是一個簡單的裝飾器。它追加位置參數,也就是被裝飾函數到registry變量中,然後不做改變地返回被裝飾方法。任何接受register裝飾器的方法會把它自己追加到registry變量上。

@register
def foo():
  return 3
@register
def bar():
  return 5

如果你訪問了registry,可以很容易地在上面迭代並執行裏面的函數。

answers = []
for func in registry:
   answers.append(func())

answers 列表現在回包含 [3, 5]. 這是因爲函數已按次序執行,並且它們的返回值被追加到 answers中.
對於現有的函數註冊,有幾類簡單的應用,例如添加“鉤子(hooks)”到代碼中,這樣的話自定義的功能在條件事件之前或之後運行。 下面的Registry類能夠處理這種情況:

class Registry(object):
  def __init__(self):
    self._functions = []
  def register(self, decorated):
    self._functions.append(decorated)
    return decorated
  def run_all(self, *args, **kwargs):
     return_values = []
     for func in self._functions:
       return_values.append(func(*args, **kwargs))
     return return_values

這個類裏的register方法讓然像之前一樣按同樣方法工作。用一個綁定(bound)的方法作爲裝飾器完全沒問題。它接收self作爲第一參數(像任何綁定方法一樣),並且需要一個額外的位置參數,那就是被裝飾函數,通過創建幾個不同的 registry實例,你可以擁有一些完全分開的註冊器。使用相同函數並且,用超過一個註冊器註冊它也是可行的,像下面展示的一樣 :

a = Registry()
b = Registry()
@a.register
def foo(x=3):
  return x
@b.register
def bar(x=5):
  return x
@a.register
@b.register
def baz(x=7):
  return x

運行兩個註冊器的run_alll方法,得到如下結果:

a.run_all() # [3, 7]
b.run_all() # [5, 7]

注意,run_all 方法能夠使用參數,當它們運行時會把參數傳給內部函數

a.run_all(x=4) # [4, 4]

運行時包裝代碼

以上這些裝飾器都很簡單,因爲被裝飾方法被傳遞後未經更改。然而,有些時候當被裝飾方法執行時,你想要運行額外的功能。你通過返回一個添加了相關功能並且在它執行過程中調用被裝飾方法的不同的可調用對象來實現。

簡單的類型檢查

這有一個簡單的裝飾器,確保函數接收到的每一個參數都是整數,否則進行報告:

def requires_ints(decorated):
  def inner(*args, **kwargs):
    #獲取任何可能被髮送的關鍵值參數
    kwarg_values = [i for i in kwargs.values()]
    #在發送給被裝飾方法的每個值上面進行迭代,確保每一個都是整數;
    #如果不是拋 TypeError 
    for arg in list(args) + kwarg_values:
      if not isinstance(arg, int):
        raise TypeError('%s only accepts integers as   
             arguments.' % decorated.__name__)                
       #運行被裝飾方法,返回結果
      return decorated(*args, **kwargs)
  return inner

發生了什麼?
裝飾器是 requires_ints. 它接受一個參數,即被裝飾的可調用對象。這個裝飾器做的唯一事情是返回一個新的可調用對象,一個內部的本地函數。這個函數替代了被裝飾的可調用對象。你可以看到它如何發揮作用,聲明一個函數並且用requires_ints來裝飾

@requires_ints
def foo(x, y):
"""Return the sum of x and y."""
  return x + y

注意如果你運行 help(foo)獲取的:

Help on function inner in module __main__:
inner(*args, **kwargs)
(END)

inner 函數已被指派了名字foo,而不是初始的,已定義了的函數。如果你運行 foo(3, 5), inner 函數會用這些參數來運行,inner函數進行類型檢查,然後運行被裝飾函數,因爲inner函數調用它,使用decorated(*args, **kwargs),返回8.沒有這個調用,被裝飾方法會被忽略。
保留helpPreserving the help
一般不想讓裝飾器破壞你的函數的docstring或者操縱help輸出。
因爲裝飾器是用來添加通用的和可重用功能的工具,他們有必要更泛化些。
並且,通常來說如果有人使用一個函數試圖在上面運行help,他想要的是關於函數內臟(guts)的信息,而不是外殼(shell)的信息。解決這個問題的方法實際上應用到了 … 仍然是裝飾器. Python 實現了一個叫做 @functools.wraps 的裝飾器,它複製一個函數的內部元素到另一個函數。它把一個函數的重要的內省元素(introspection
elements)複製給另一個函數。
這是同一個@requires_ints 裝飾器, 但添加了@functools.wraps的使用:

import functools
def requires_ints(decorated):
  @functools.wraps(decorated)
  def inner(*args, **kwargs):
    #獲取可能已作爲鍵值參數發送的任何值
    kwarg_values = [i for i in kwargs.values()]
    #迭代發送給被裝飾函數的每個值, 並
    #確保每個參數都是整數,否則拋TypeError
    for arg in args + kwarg_values:
      if not isinstance(i, int):
        raise TypeError('%s only accepts integers as 
         arguments.' %decorated.__name__)
    #運行被裝飾函數然後返回結果
    return decorated(*args, **kwargs)
  return inner

裝飾器本身幾乎沒有改變,除了第二行給inner函數使用了@functools.wraps裝飾器。你現在必須導入functools(在標準庫中)。你也會注意到些額外語法。這個裝飾器實際上使用了一個參數(稍後會有更多)。

現在你可以應用這個裝飾器給相同的函數,像下面這樣:

@requires_ints
def foo(x, y):
"""Return the sum of x and y."""
  return x + y

現在當你運行help(foo)的結果:

Help on function foo in module __main__:
foo(x, y)
Return the sum of x and y.
(END)

你看到了 foo的docstring ,同時還有它的方法簽名,然而在蓋頭(hood)下面,@requires_ints裝飾器仍然被應用,並且 inner函數仍然正常運行 。取決於你使用的python版本,運行結果可能稍有不同,尤其當忽略函數簽名時。前面的輸出源自Python 3.4。然而在python 2,提供的函數簽名仍然有點隱祕(因此,是*args和**kwargs而不是x和y)

用戶認證

這個模式(即在運行被裝飾方法前進行過濾驗證)的通常使用場景是用戶認證。考慮一個需要user作爲它的第一個參數的方法,user應該是User和AnonymousUser類的實例:

class User(object):
"""A representation of a user in our application."""
  def __init__(self, username, email):
    self.username = username
    self.email = email
class AnonymousUser(User):
"""An anonymous user; a stand-in for an actual user that nonetheless
is not an actual user.
"""
  def __init__(self):
    self.username = None
    self.email = None
  def __nonzero__(self):
    return False

裝飾器在此成爲隔離用戶驗證的樣板代碼的有力工具。@requires_user裝飾器可以很輕鬆地認證你獲得了一個User對象並且不是匿名user

import functools
def requires_user(func):
  @functools.wraps(func)
  def inner(user, *args, **kwargs):
    """Verify that the user is truthy; if so, run the  
    decorated method,
    and if not, raise ValueError.
    """
    # Ensure that user is truthy, and of the correct type.
    # The "truthy"check will fail on anonymous users, since the
    # AnonymousUser subclass has a ‘__nonzero__‘ method that
    # returns False.
    if user and isinstance(user, User):
      return func(user, *args, **kwargs)
    else:
      raise ValueError('A valid user is required to run  
        this.')
  return inner

這個裝飾器應用了一個通用的,需要樣板化的驗證—-用戶是否登錄進系統的驗證。當你把它作爲裝飾器導入,它可重用且易於管理,它應用至函數上也清晰明瞭。注意這個裝飾器只會正確地包裝一個函數或者靜態方法,如果包裝一個類的綁定方法就會失敗,這是因爲裝飾器忽視了發送self作爲第一個參數到綁定方法的需要。

格式化輸出

除了過濾一個函數的輸入,裝飾器的另一個用處是過濾一個函數的輸出。當你用Python工作時,只要可能就希望使用Python本地對象。然而通常想要一個序列化的輸出格式(例如,JSON)
在每個相關函數的結尾手動轉換成JSON會顯得很笨(也不是個好主意)。
理想的你應該使用Python數據結構直到需要序列化,但在序列化前仍然可能有其他重複代碼。
裝飾器爲這個問題提供了一個出色的,輕便的解決方案。考慮下面的裝飾器,它採用python輸出,並序列化結果爲JSON

import functools
import json

def json_output(decorated):
  """Run the decorated function, serialize the result of  
  that function
  to JSON, and return the JSON string.
  """
  @functools.wraps(decorated)
  def inner(*args, **kwargs):
    result = decorated(*args, **kwargs)
    return json.dumps(result)
  return inner  

給一個 簡單函數應用@json_output 裝飾器 :

@json_output
def do_nothing():
  return {'status': 'done'}

在Python shell中運行這個函數:

>>> do_nothing()
'{"status": "done"}'

結果是一個包含JSON的字符串,而不是一個字典。
這個裝飾器的優美在於它的簡潔。把這個裝飾器應用到一個函數,本來返回python字典,列表或者其它對象的函數現在會返回它的JSON序列化的版本。你可能會問這有什麼價值?畢竟你加了一行裝飾器,實質上只移除了一行調用json.dumps的代碼。
然而,由於應用的需求會擴展,還是考慮一下擁有此裝飾器的價值。

例如,某種異常需要被捕獲,並以特定的格式化的json輸出,而不是讓異常上浮產生堆棧跟蹤,該怎麼做?因爲有裝飾器,這個功能很容易添加。

import functools
import json

class JSONOutputError(Exception):
  def __init__(self, message):
    self._message = message
  def __str__(self):
    return self._message
def json_output(decorated):
  """Run the decorated function, serialize the result of   
  that function
  to JSON, and return the JSON string.
  """
  @functools.wraps(decorated)
  def inner(*args, **kwargs):
    try:
      result = decorated(*args, **kwargs)
    except JSONOutputError as ex:
      result = {
      'status': 'error',
      'message': str(ex),
      }
    return json.dumps(result)
  return inner

通過使用錯誤處理增強@json_output裝飾器,你已經把該功能添加給了應用了這個裝飾器的任何函數。
這是讓裝飾器如此有價值的部分原因。對於代碼輕便化,可重用化而言,它們是非常有用的工具。
現在,如果一個用@json_output裝飾的函數拋出了JSONOutputError異常,就會有特別的錯誤處理:

@json_output
def error():
  raise JSONOutputError('This function is erratic.')

運行error 函數:

>>> error()
'{"status": "error", "message": "This function is erratic."}'

注意,只有JSONOutputError異常類(或它的子類)會獲得這種特別的錯誤處理。任何其它異常會正常通過,併產生堆棧跟蹤。

實質上,裝飾器是避免重複你自己的工具,並且它們的部分價值在於給未來的維護提供鉤子(hooks)。這些不用裝飾器也可以實現,考慮要求用戶登錄進系統的例子,寫一個函數並把它放在需要這項功能的函數的入口處就行了。裝飾器首先是一種語法糖(syntactic sugar)。然而是一種很有價值的語法糖。畢竟,相較於寫,代碼更多時候用來讀,而且你可以一眼定位到裝飾器的位置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章