python奇遇記:深刻理解裝飾器

什麼是裝飾器

裝飾器是什麼,簡單來講,裝飾器能夠改變一個函數的行爲,好比本來有一個函數用來計算菲波那切數列,咱們給這個函數加個計算執行時間的裝飾器,這樣原來的函數不只可以計算菲波那切數列,並且還能夠輸出計算花費了多少時間。python

在Python中,有幾個很常見的內置裝飾器:好比@staticmethod, 它能夠將一個類的方法聲明爲靜態的。@property, 爲類中的變量設置get和set方法,保證了封裝性。web

若是你使用過python的web框架(好比flask)開發過網站,你應該常常會見到裝飾器,像下面這樣:flask

@app.route("/")
def hello():
    return "Hello World!"

這段代碼把路由綁定到hello函數上,這樣你輸入網址以後就能夠看到Hello World閉包

先來看個很簡單的例子:app

# 定義了一個裝飾器
def deco(func):
  def hah():
      print('hahha')
  return hah

上面咱們定義了一個裝飾器,打印hahah,接下來使用:框架

# 使用這個裝飾器
@deco
def lal():
    pritn('lalalala')

lal()

執行lal()會輸出hahha。 可見deco裝飾器改變了lal函數的功能。上面的代碼中,咱們其實是把lal函數放入了deco函數,像這樣:函數

lal = deco(lal)

只不過,直接使用@標誌把裝飾器放在某個函數上更方便一點而已。網站

裝飾器其實就是一個函數嵌套另外一個函數(這裏涉及到一個概念叫作閉包,下面會講到)。在裝飾器的定義中,須要把內部的函數返回(像hah),內部函數用來真正的改變被裝飾函數的功能。code

不過,上面定義的裝飾器好像沒什麼用,咱們來真正的寫一個裝飾器,像文章開頭說的那樣,定義一個裝飾器計算函數執行的時間。作用域

實現一個簡單的裝飾器

import time
# 這個裝飾器接收一個函數做爲參數
def clock(func):
  # clocked用來改變被裝飾函數功能
  # 接收任意可變參數
    def clocked(*args):
      #先計算時間
        t0 = time.perf_counter()
        # 而後運行被裝飾的函數
        result = func(*args)
        # 計算運行先後的時間差
        elapsed = time.perf_counter()-t0
        # 函數的名字
        name = func.__name__
        # 被裝飾函數的全部變量
        arg_str = ','.join(repr(arg) for arg in args)
        # 輸出
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        # 返回被裝飾函數執行結果
        # 可見裝飾器是在原來的函數上增長了某些功能
        # 而不是徹底改變被裝飾函數
        return result
     # 把clocked函數返回
    return clocked

來使用一下上面定義的裝飾器:

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

執行結果:

[0.00000030s] factorial(1) -> 1
[0.00004588s] factorial(2) -> 2
[0.00007184s] factorial(3) -> 6
[0.00060794s] factorial(4) -> 24
[0.00064205s] factorial(5) -> 120
[0.00066801s] factorial(6) -> 720
720

能夠看到,在輸出計算結果的同時,輸出了每一步的執行時間。

裝飾器除了改變函數功能以外還有一個特性是,函數裝飾器在導入模塊時當即執行,而被裝飾的函數只在明確調用時運行。這點須要注意。

固然了,裝飾器之上還能夠放一個裝飾器,不過是多了一層嵌套而已。

python中還有一個內置的模塊functools,這裏面定義了一些經常使用的裝飾器函數,幫助你更好地定義本身的裝飾器。這裏就不講了。

閉包

說到閉包,在上面的代碼中咱們已經見識到了,函數中嵌套函數就是閉包。嚴格來講,閉包是指延伸了做用域的函數,怎麼理解?不如來看個例子:

咱們定義一個函數不斷計算平均值,它會記住上一次計算的值進行累計。

# 先看一些效果
avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))

輸出以下:

10.0
10.5
11.0

第一次輸出10,第二次輸出10加11的平均值,第三次輸出10加11加12的平均值。

怎麼實現的?

def make_averager():
  # 局部變量series
  # 用來保存每次輸入的值
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

上面的函數中,series是局部變量。當咱們調用avg(10)的時候,函數已經返回了,按理說它的本地做用域已經不存在了,可是咱們仍是能夠繼續使用。這是由於series實際上是自由變量,它不受本地做用域的限制。須要注意的是,對於不可變類型,須要顯示用關鍵字nonlocal 聲明自由變量,若是不聲明的話,會隱式的建立局部變量,這樣自由變量就會失效。而可變類型則不須要。好比,咱們來更改一下上面的代碼:

# 改一下求平均值的函數
# 用另外一種方法
def make_averager():
   count = 0
   total = 0
   def averager(new_value):
      # count、total是不可變類型
      # 須要聲明爲自由變量
     nonlocal count, total
     count += 1
     total += new_value
     return total / count
   return averager

除了上面說的裝飾器的用法以外,咱們還能夠爲裝飾器添加參數,像app.route('/') 這樣,限於篇幅,下一篇文章再介紹。

相關文章
相關標籤/搜索