Python 中級學習之函式裝飾器

ckxllf發表於2021-03-17

  七、函式裝飾器

  裝飾器(Decorators)在 Python 中,主要作用是修改函式的功能,而且修改前提是不變動原函式程式碼,裝飾器會返回一個函式物件,所以有的地方會把裝飾器叫做 “函式的函式”。

  還存在一種設計模式叫做 “裝飾器模式”,這個後續的課程會有所涉及。

  裝飾器呼叫的時候,使用 @,它是 Python 提供的一種程式設計語法糖,使用了之後會讓你的程式碼看起來更加 Pythonic。

  7.1 裝飾器基本使用

  在學習裝飾器的時候,最常見的一個案例,就是統計某個函式的執行時間,接下來就為你分享一下。

  計算函式執行時間:

  import time

  def fun():

  i = 0

  while i < 1000:

  i += 1

  def fun1():

  i = 0

  while i < 10000:

  i += 1

  s_time = time.perf_counter()

  fun()

  e_time = time.perf_counter()

  print(f"函式{fun.__name__}執行時間是:{e_time-s_time}")

  如果你希望給每個函授都加上呼叫時間,那工作量是巨大的,你需要重複的修改函式內部程式碼,或者修改函式呼叫位置的程式碼。在這種需求下,裝飾器語法出現了。

  先看一下第一種修改方法,這種方法沒有增加裝飾器,但是編寫了一個通用的函式,利用 Python 中函式可以作為引數這一特性,完成了程式碼的可複用性。

  import time

  def fun():

  i = 0

  while i < 1000:

  i += 1

  def fun1():

  i = 0

  while i < 10000:

  i += 1

  def go(fun):

  s_time = time.perf_counter()

  fun()

  e_time = time.perf_counter()

  print(f"函式{fun.__name__}執行時間是:{e_time-s_time}")

  if __name__ == "__main__":

  go(fun1)

  接下來這種技巧擴充套件到 Python 中的裝飾器語法,具體修改如下:

  import time

  def go(func):

  # 這裡的 wrapper 函式名可以為任意名稱

  def wrapper():

  s_time = time.perf_counter()

  func()

  e_time = time.perf_counter()

  print(f"函式{func.__name__}執行時間是:{e_time-s_time}")

  return wrapper

  @go

  def func():

  i = 0

  while i < 1000:

  i += 1

  @go

  def func1():

  i = 0

  while i < 10000:

  i += 1

  if __name__ == '__main__':

  func()

  在上述程式碼中,注意看 go 函式部分,它的引數 func 是一個函式,返回值是一個內部函式,執行程式碼之後相當於給原函式注入了計算時間的程式碼。在程式碼呼叫部分,你沒有做任何修改,函式 func 就具備了更多的功能(計算執行時間的功能)。

  裝飾器函式成功擴充了原函式的功能,又不需要修改原函式程式碼,這個案例學會之後,你就已經初步瞭解了裝飾器。

  7.2 對帶引數的函式進行裝飾

  直接看程式碼,瞭解如何對帶引數的函式進行裝飾:

  import time

  def go(func):

  def wrapper(x, y):

  s_time = time.perf_counter()

  func(x, y)

  e_time = time.perf_counter()

  print(f"函式{func.__name__}執行時間是:{e_time-s_time}")

  return wrapper

  @go

  def func(x, y):

  i = 0

  while i < 1000:

  i += 1

  print(f"x={x},y={y}")

  if __name__ == '__main__':

  func(33, 55)

  還有一種情況是裝飾器本身帶有引數,例如下述程式碼:

  def log(text):

  def decorator(func):

  def wrapper(x):

  print('%s %s():' % (text, func.__name__))

  func(x)

  return wrapper

  return decorator

  @log('執行')

  def my_fun(x):

  print(f"我是 my_fun 函式,我的引數 {x}")

  my_fun(123)

  上述程式碼在編寫裝飾器函式的時候,在裝飾器函式外層又巢狀了一層函式,最終程式碼的執行順序如下所示:

  my_fun = log('執行')(my_fun)

  此時如果我們總結一下,就能得到結論了:使用帶有引數的裝飾器,是在裝飾器外面又包裹了一個函式,使用該函式接收引數,並且返回一個裝飾器函式。

  還有一點要注意的是裝飾器只能接收一個引數,而且必須是函式型別。

  

Python 中級知識之裝飾器,滾雪球學 Python

  7.3 多個裝飾器

  先臨摹一下下述程式碼,再進行學習與研究。

  import time

  def go(func):

  def wrapper(x, y):

  s_time = time.perf_counter()

  func(x, y)

  e_time = time.perf_counter()

  print(f"函式{func.__name__}執行時間是:{e_time-s_time}")

  return wrapper

  def gogo(func):

  def wrapper(x, y):

  print("我是第二個裝飾器")

  return wrapper

  @go

  @gogo

  def func(x, y):

  i = 0

  while i < 1000:

  i += 1

  print(f"x={x},y={y}")

  if __name__ == '__main__':

  func(33, 55)

  程式碼執行之後,輸出結果為:

  我是第二個裝飾器

  函式wrapper執行時間是:0.0034401339999999975

  雖說多個裝飾器使用起來非常簡單,但是問題也出現了,print(f"x={x},y={y}") 這段程式碼執行結果丟失了,這裡就涉及多個裝飾器執行順序問題了。

  先解釋一下裝飾器的裝飾順序。

  import time

  def d1(func):

  def wrapper1():

  print("裝飾器1開始裝飾")

  func()

  print("裝飾器1結束裝飾")

  return wrapper1

  def d2(func):

  def wrapper2():

  print("裝飾器2開始裝飾")

  func()

  print("裝飾器2結束裝飾")

  return wrapper2

  @d1

  @d2

  def func():

  print("被裝飾的函式")

  if __name__ == '__main__':

  func()

  上述程式碼執行的結果為:

  裝飾器1開始裝飾

  裝飾器2開始裝飾

  被裝飾的函式

  裝飾器2結束裝飾

  裝飾器1結束裝飾

  可以看到非常對稱的輸出,同時證明被裝飾的函式在最內層,轉換成函式呼叫的程式碼如下:

  d1(d2(func))

  你在這部分需要注意的是,裝飾器的外函式和內函式之間的語句,是沒有裝飾到目標函式上的,而是在裝載裝飾器時的附加操作。

  在對函式進行裝飾的時候,外函式與內函式之間的程式碼會被執行。

  測試效果如下:

  import time

  def d1(func):

  print("我是 d1 內外函式之間的程式碼")

  def wrapper1():

  print("裝飾器1開始裝飾")

  func()

  print("裝飾器1結束裝飾")

  return wrapper1

  def d2(func):

  print("我是 d2 內外函式之間的程式碼")

  def wrapper2():

  print("裝飾器2開始裝飾")

  func()

  print("裝飾器2結束裝飾")

  return wrapper2

  @d1

  @d2

  def func():

  print("被裝飾的函式")

  執行之後,你就能發現輸出結果如下:

  我是 d2 內外函式之間的程式碼

  我是 d1 內外函式之間的程式碼

  d2 函式早於 d1 函式執行。

  接下來在回顧一下裝飾器的概念:

  被裝飾的函式的名字會被當作引數傳遞給裝飾函式。

  裝飾函式執行它自己內部的程式碼後,會將它的返回值賦值給被裝飾的函式。

  這樣看上文中的程式碼執行過程是這樣的,d1(d2(func)) 執行 d2(func) 之後,原來的 func 這個函式名會指向 wrapper2 函式,執行 d1(wrapper2) 函式之後,wrapper2 這個函式名又會指向 wrapper1。因此最後的 func 被呼叫的時候,相當於程式碼已經切換成如下內容了。

  # 第一步

  def wrapper2():

  print("裝飾器2開始裝飾")

  print("被裝飾的函式")

  print("裝飾器2結束裝飾")

  # 第二步

  print("裝飾器1開始裝飾")

  wrapper2()

  print("裝飾器1結束裝飾")

  # 第三步

  def wrapper1():

  print("裝飾器1開始裝飾")

  print("裝飾器2開始裝飾")

  print("被裝飾的函式")

  print("裝飾器2結束裝飾")

  print("裝飾器1結束裝飾")

  上述第三步執行之後的程式碼,恰好與我們的程式碼輸出一致。

  那現在再回到本小節一開始的案例,為何輸出資料丟失掉了。

  import time

  def go(func):

  def wrapper(x, y):

  s_time = time.perf_counter()

  func(x, y)

  e_time = time.perf_counter()

  print(f"函式{func.__name__}執行時間是:{e_time-s_time}")

  return wrapper

  def gogo(func):

  def wrapper(x, y):

  print("我是第二個裝飾器")

  return wrapper

  @go

  @gogo

  def func(x, y):

  i = 0

  while i < 1000:

  i += 1

  print(f"x={x},y={y}")

  if __name__ == '__main__':

  func(33, 55)

  在執行裝飾器程式碼裝飾之後,呼叫 func(33,55) 已經切換為 go(gogo(func)),執行 gogo(func) 程式碼轉換為下述內容:

  def wrapper(x, y):

  print("我是第二個裝飾器")

  在執行 go(wrapper),程式碼轉換為:

  s_time = time.perf_counter()

  print("我是第二個裝飾器")

  e_time = time.perf_counter()

  print(f"函式{func.__name__}執行時間是:{e_time-s_time}")

  此時,你會發現引數在執行過程被丟掉了。

  7.4 functools.wraps 大連人流醫院

  使用裝飾器可以大幅度提高程式碼的複用性,但是缺點就是原函式的元資訊丟失了,比如函式的 __doc__、__name__:

  # 裝飾器

  def logged(func):

  def logging(*args, **kwargs):

  print(func.__name__)

  print(func.__doc__)

  func(*args, **kwargs)

  return logging

  # 函式

  @logged

  def f(x):

  """函式文件,說明"""

  return x * x

  print(f.__name__) # 輸出 logging

  print(f.__doc__) # 輸出 None

  解決辦法非常簡單,匯入 from functools import wraps ,修改程式碼為下述內容:

  from functools import wraps

  # 裝飾器

  def logged(func):

  @wraps(func)

  def logging(*args, **kwargs):

  print(func.__name__)

  print(func.__doc__)

  func(*args, **kwargs)

  return logging

  # 函式

  @logged

  def f(x):

  """函式文件,說明"""

  return x * x

  print(f.__name__) # 輸出 f

  print(f.__doc__) # 輸出 函式文件,說明

  7.5 基於類的裝飾器

  在實際編碼中 一般 “函式裝飾器” 最為常見,“類裝飾器” 出現的頻率要少很多。

  基於類的裝飾器與基於函式的基本用法一致,先看一段程式碼:

  class H1(object):

  def __init__(self, func):

  self.func = func

  def __call__(self, *args, **kwargs):

  return '

  ' + self.func(*args, **kwargs) + '

  '

  @H1

  def text(name):

  return f'text {name}'

  s = text('class')

  print(s)

  類 H1 有兩個方法:

  __init__:接收一個函式作為引數,就是待被裝飾的函式;

  __call__:讓類物件可以呼叫,類似函式呼叫,觸發點是被裝飾的函式呼叫時觸發。

  最後在附錄一篇寫的不錯的 部落格,可以去學習。

  在這裡類裝飾器的細節就不在展開了,等到後面滾雪球相關專案實操環節再說。

  裝飾器為類和類的裝飾器在細節上是不同的,上文提及的是裝飾器為類,你可以在思考一下如何給類新增裝飾器。

  7.6 內建裝飾器

  常見的內建裝飾器有 @property、@staticmethod、@classmethod。該部分內容在細化物件導向部分進行說明,本文只做簡單的備註。

  7.6.1 @property

  把類內方法當成屬性來使用,必須要有返回值,相當於 getter,如果沒有定義 @func.setter 修飾方法,是隻讀屬性。

  7.6.2 @staticmethod

  靜態方法,不需要表示自身物件的 self 和自身類的 cls 引數,就跟使用函式一樣。

  7.6.3 @classmethod

  類方法,不需要 self 引數,但第一個引數需要是表示自身類的 cls 引數。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69945560/viewspace-2763359/,如需轉載,請註明出處,否則將追究法律責任。

相關文章