python中閉包和裝飾器的理解(關於python中閉包和裝飾器解釋最好的文章)。

轉載:http://python.jobbole.com/81683/python

呵呵!做爲一名教python的老師,我發現學生們基本上一開始很難搞定python的裝飾器,也許由於裝飾器確實很難懂。搞定裝飾器須要你瞭解一些函數式編程的概念,固然還有理解在python中定義和調用函數相關語法的一些特色。編程

我無法讓裝飾器變得簡單,可是經過一步步的剖析,我也許可以讓你在理解裝飾器的時候更自信一點。由於裝飾器很複雜,這篇文章將會很長(本身都說很長,還敢這麼多廢話blablabla前戲就不繼續翻譯直接省略了)閉包

1. 函數app

在python中,函數經過def關鍵字、函數名和可選的參數列表定義。經過return關鍵字返回值。咱們舉例來講明如何定義和調用一個簡單的函數:編程語言

def foo():
     return 1
foo()
1

方法體(固然多行也是同樣的)是必須的,經過縮進來表示,在方法名的後面加上雙括號()就可以調用函數ide

2. 做用域函數式編程

在python中,函數會建立一個新的做用域。python開發者可能會說函數有本身的命名空間,差很少一個意思。這意味着在函數內部碰到一個變量的時候函數會優先在本身的命名空間裏面去尋找。讓咱們寫一個簡單的函數看一下 本地做用域 和 全局做用域有什麼不一樣:函數

1 a_string = "This is a global variable"
2 def foo():
3      print locals()
4 print globals() # doctest: +ELLIPSIS
5 {, 'a_string': 'This is a global variable'}
6 foo() # 2
7 {}

內置的函數globals返回一個包含全部python解釋器知道的變量名稱的字典(爲了乾淨和洗的白白的,我省略了python自行建立的一些變量)。在#2我調用了函數 foo 把函數內部本地做用域裏面的內容打印出來。咱們可以看到,函數foo有本身獨立的命名空間,雖然暫時命名空間裏面什麼都尚未。學習

3. 變量解析規則編碼

固然這並非說咱們在函數裏面就不能訪問外面的全局變量。在python的做用域規則裏面,建立變量必定會必定會在當前做用域裏建立一個變量,可是訪問或者修改變量時會先在當前做用域查找變量,沒有找到匹配變量的話會依次向上在閉合的做用域裏面進行查看找。因此若是咱們修改函數foo的實現讓它打印全局的做用域裏的變量也是能夠的:

1 a_string = "This is a global variable"
2 def foo():
3      print a_string # 1
4 foo()
5 This is a global variable

在#1處,python解釋器會嘗試查找變量a_string,固然在函數的本地做用域裏面是找不到的,因此接着會去上層的做用域裏面去查找。
可是另外一方面,假如咱們在函數內部給全局變量賦值,結果卻和咱們想的不同:

1 a_string = "This is a global variable"
2 def foo():
3      a_string = "test" # 1
4      print locals()
5 foo()
6 {'a_string': 'test'}
7 a_string # 2
8 'This is a global variable'

咱們可以看到,全局變量可以被訪問到(若是是可變數據類型(像list,dict這些)甚至可以被更改)可是賦值不行。在函數內部的#1處,咱們實際上新建立了一個局部變量,隱藏全局做用域中的同名變量。咱們能夠經過打印出局部命名空間中的內容得出這個結論。咱們也能看到在#2處打印出來的變量a_string的值並無改變。

4. 變量生存週期

值得注意的一個點是,變量不只是生存在一個個的命名空間內,他們都有本身的生存週期,請看下面這個例子:

1 def foo():
2      x = 1
3 foo()
4 print x # 1
5 Traceback (most recent call last):
6 
7 NameError: name 'x' is not defined

#1處發生的錯誤不只僅是由於做用域規則致使的(儘管這是拋出了NameError的錯誤的緣由)它還和python以及其它不少編程語言中函數調用實現的機制有關。在這個地方這個執行時間點並無什麼有效的語法讓咱們可以獲取變量x的值,由於它這個時候壓根不存在!函數foo的命名空間隨着函數調用開始而開始,結束而銷燬。

5. 函數參數

python容許咱們向函數傳遞參數,參數會變成本地變量存在於函數內部

1 def foo(x):
2      print locals()
3 foo(1)
4 {'x': 1}

在Python裏有不少的方式來定義和傳遞參數,完整版能夠查看 python官方文檔。咱們這裏簡略的說明一下:函數的參數能夠是必須的位置參數或者是可選的命名,默認參數。

 1 def foo(x, y=0): # 1
 2      return x - y
 3 foo(3, 1) # 2
 4 2
 5 foo(3) # 3
 6 3
 7 foo() # 4
 8 Traceback (most recent call last):
 9 
10 TypeError: foo() takes at least 1 argument (0 given)
11 foo(y=1, x=3) # 5
12 2

在#1處咱們定義了函數foo,它有一個位置參數x和一個命名參數y。在#2處咱們可以經過常規的方式來調用函數,儘管有一個命名參數,但參數依然能夠經過位置傳遞給函數。在調用函數的時候,對於命名參數y咱們也能夠徹底無論就像#3處所示的同樣。若是命名參數沒有接收到任何值的話,python會自動使用聲明的默認值也就是0。須要注意的是咱們不能省略第一個位置參數x, 不然的話就會像#5處所示發生錯誤。

目前還算簡潔清晰吧, 可是接下來可能會有點使人困惑。python支持函數調用時的命名參數(我的以爲應該是命名實參)。看看#5處的函數調用,咱們傳遞的是兩個命名實參,這個時候由於有名稱標識,參數傳遞的順序也就不用在乎了。

固然相反的狀況也是正確的:函數的第二個形參是y,可是咱們經過位置的方式傳遞值給它。在#2處的函數調用foo(3,1),咱們把3傳遞給了第一個參數,把1傳遞給了第二個參數,儘管第二個參數是一個命名參數。

桑不起,感受用了好大一段才說清楚這麼一個簡單的概念:函數的參數能夠有名稱和位置。這意味着在函數的定義和調用的時候會稍稍在理解上有點兒不一樣。咱們能夠給只定義了位置參數的函數傳遞命名參數(實參),反之亦然!若是以爲不夠能夠查看官方文檔

6. 嵌套函數

Python容許建立嵌套函數。這意味着咱們能夠在函數裏面定義函數並且現有的做用域和變量生存週期依舊適用。

1 def outer():
2      x = 1
3      def inner():
4          print x # 1
5      inner() # 2
6 
7 outer()
8 1

這個例子有一點兒複雜,可是看起來也還行。想想在#1發生了什麼:python解釋器需找一個叫x的本地變量,查找失敗以後會繼續在上層的做用域裏面尋找,這個上層的做用域定義在另一個函數裏面。對函數outer來講,變量x是一個本地變量,可是如先前提到的同樣,函數inner能夠訪問封閉的做用域(至少能夠讀和修改)。在#2處,咱們調用函數inner,很是重要的一點是,inner也僅僅是一個遵循python變量解析規則的變量名,python解釋器會優先在outer的做用域裏面對變量名inner查找匹配的變量.

7. 函數是python世界裏的一級類對象

顯而易見,在python裏函數和其餘東西同樣都是對象。(此處應該大聲歌唱)啊!包含變量的函數,你也並非那麼特殊!

1 issubclass(int, object) # all objects in Python inherit from a common baseclass
2 True
3 def foo():
4      pass
5 foo.__class__ # 1
6 <type 'function'>
7 issubclass(foo.__class__, object)
8 True

你也許從沒有想過,你定義的函數竟然會有屬性。沒辦法,函數在python裏面就是對象,和其餘的東西同樣,也許這樣描述會太學院派太官方了點:在python裏,函數只是一些普通的值而已和其餘的值一毛同樣。這就是說你尅一把函數想參數同樣傳遞給其餘的函數或者說從函數了裏面返回函數!若是你歷來沒有這麼想過,那看看下面這個例子:

 1 def add(x, y):
 2      return x + y
 3 def sub(x, y):
 4      return x - y
 5 def apply(func, x, y): # 1
 6      return func(x, y) # 2
 7 apply(add, 2, 1) # 3
 8 3
 9 apply(sub, 2, 1)
10 1

這個例子對你來講應該不會很奇怪。add和sub是很是普通的兩個python函數,接受兩個值,返回一個計算後的結果值。在#1處大家能看到準備接收一個函數的變量只是一個普通的變量而已,和其餘變量同樣。在#2處咱們調用傳進來的函數:「()表明着調用的操做而且調用變量包含的值。在#3處,大家也能看到傳遞函數並無什麼特殊的語法。」 函數的名稱只是很其餘變量同樣的表標識符而已。

大家也許看到過這樣的行爲:「python把頻繁要用的操做變成函數做爲參數進行使用,像經過傳遞一個函數給內置排序函數的key參數從而來自定義排序規則。那把函數當作返回值回事這樣的狀況呢:

 1 def outer():
 2      def inner():
 3          print "Inside inner"
 4      return inner # 1
 5 
 6 foo = outer() #2
 7 foo # doctest:+ELLIPSIS
 8 <function inner at 0x>
 9 foo()
10 Inside inner

這個例子看起來也許會更加的奇怪。在#1處我把剛好是函數標識符的變量inner做爲返回值返回出來。這並無什麼特殊的語法:」把函數inner返回出來,不然它根本不可能會被調用到。「還記得變量的生存週期嗎?每次函數outer被調用的時候,函數inner都會被從新定義,若是它不被當作變量返回的話,每次執行事後它將不復存在。

在#2處咱們捕獲住返回值 – 函數inner,將它存在一個新的變量foo裏。咱們可以看到,當對變量foo進行求值,它確實包含函數inner,並且咱們可以對他進行調用。初次看起來可能會以爲有點奇怪,可是理解起來並不困難是吧。堅持住,由於奇怪的轉折立刻就要來了(嘿嘿嘿嘿,我笑的並不猥瑣!)

8. 閉包

咱們先不急着定義什麼是閉包,先來看看一段代碼,僅僅是把上一個例子簡單的調整了一下:

1 def outer():
2      x = 1
3      def inner():
4          print x # 1
5      return inner
6 foo = outer()
7 foo.func_closure # doctest: +ELLIPSIS
8 (<cell at 0x: int object at 0x>,)

在上一個例子中咱們瞭解到,inner做爲一個函數被outer返回,保存在一個變量foo,而且咱們可以對它進行調用foo()。不過它會正常的運行嗎?咱們先來看看做用域規則。

全部的東西都在python的做用域規則下進行工做:「x是函數outer裏的一個局部變量。當函數inner在#1處打印x的時候,python解釋器會在inner內部查找相應的變量,固然會找不到,因此接着會到封閉做用域裏面查找,而且會找到匹配。

可是從變量的生存週期來看,該怎麼理解呢?咱們的變量x是函數outer的一個本地變量,這意味着只有當函數outer正在運行的時候纔會存在。根據咱們已知的python運行模式,咱們無法在函數outer返回以後繼續調用函數inner,在函數inner被調用的時候,變量x早已不復存在,可能會發生一個運行時錯誤。

萬萬沒想到,返回的函數inner竟然可以正常工做。Python支持一個叫作函數閉包的特性,用人話來說就是,嵌套定義在非全局做用域裏面的函數可以記住它在被定義的時候它所處的封閉命名空間。這可以經過查看函數的func_closure屬性得出結論,這個屬性裏面包含封閉做用域裏面的值(只會包含被捕捉到的值,好比x,若是在outer裏面還定義了其餘的值,封閉做用域裏面是不會有的)

記住,每次函數outer被調用的時候,函數inner都會被從新定義。如今變量x的值不會變化,因此每次返回的函數inner會是一樣的邏輯,假如咱們稍微改動一下呢?

 1 def outer(x):
 2      def inner():
 3          print x # 1
 4      return inner
 5 print1 = outer(1)
 6 print2 = outer(2)
 7 print1()
 8 1
 9 print2()
10 2

從這個例子中你可以看到閉包 – 被函數記住的封閉做用域 – 可以被用來建立自定義的函數,本質上來講是一個硬編碼的參數。事實上咱們並非傳遞參數1或者2給函數inner,咱們其實是建立了可以打印各類數字的各類自定義版本。

閉包單獨拿出來就是一個很是強大的功能, 在某些方面,你也許會把它當作一個相似於面嚮對象的技術:outer像是給inner服務的構造器,x像一個私有變量。使用閉包的方式也有不少:你若是熟悉python內置排序方法的參數key,你說不定已經寫過一個lambda方法在排序一個列表的列表的時候基於第二個元素而不是第一個。如今你說不定也能夠寫一個itemgetter方法,接收一個索引值來返回一個完美的函數,傳遞給排序函數的參數key。

不過,咱們如今不會用閉包作這麼low的事(⊙o⊙)…!相反,讓咱們再爽一次,寫一個高大上的裝飾器!

 

9. 裝飾器

裝飾器其實就是一個閉包,把一個函數當作參數而後返回一個替代版函數。咱們一步步從簡到繁來瞅瞅:

 1 def outer(some_func):
 2      def inner():
 3          print "before some_func"
 4          ret = some_func() # 1
 5          return ret + 1
 6      return inner
 7 def foo():
 8      return 1
 9 decorated = outer(foo) # 2
10 decorated()
11 before some_func
12 2

仔細看看上面這個裝飾器的例子。們定義了一個函數outer,它只有一個some_func的參數,在他裏面咱們定義了一個嵌套的函數inner。inner會打印一串字符串,而後調用some_func,在#1處獲得它的返回值。在outer每次調用的時候some_func的值可能會不同,可是無論some_func的之如何,咱們都會調用它。最後,inner返回some_func() + 1的值 – 咱們經過調用在#2處存儲在變量decorated裏面的函數可以看到被打印出來的字符串以及返回值2,而不是指望中調用函數foo獲得的返回值1。

咱們能夠認爲變量decorated是函數foo的一個裝飾版本,一個增強版本。事實上若是打算寫一個有用的裝飾器的話,咱們可能會想願意用裝飾版本徹底取代原先的函數foo,這樣咱們老是會獲得咱們的」增強版「foo。想要達到這個效果,徹底不須要學習新的語法,簡單地賦值給變量foo就好了:

1 foo = outer(foo)
2 foo # doctest: +ELLIPSIS
3 <function inner at 0x>

如今,任何怎麼調用都不會牽扯到原先的函數foo,都會獲得新的裝飾版本的foo,如今咱們仍是來寫一個有用的裝飾器。

想象咱們有一個庫,這個庫可以提供相似座標的對象,也許它們僅僅是一些x和y的座標對。不過惋惜的是這些座標對象不支持數學運算符,並且咱們也不能對源代碼進行修改,所以也就不能直接加入運算符的支持。咱們將會作一系列的數學運算,因此咱們想要可以對兩個座標對象進行合適加減運算的函數,這些方法很容易就能寫出:

 1 class Coordinate(object):
 2      def __init__(self, x, y):
 3          self.x = x
 4          self.y = y
 5      def __repr__(self):
 6          return "Coord: " + str(self.__dict__)
 7 def add(a, b):
 8      return Coordinate(a.x + b.x, a.y + b.y)
 9 def sub(a, b):
10      return Coordinate(a.x - b.x, a.y - b.y)
11 one = Coordinate(100, 200)
12 two = Coordinate(300, 200)
13 add(one, two)
14 Coord: {'y': 400, 'x': 400}

若是不巧咱們的加減函數同時也須要一些邊界檢查的行爲那該怎麼辦呢?搞很差你只可以對正的座標對象進行加減操做,任何返回的值也都應該是正的座標。因此如今的指望是這樣:

1 one = Coordinate(100, 200)
2 two = Coordinate(300, 200)
3 three = Coordinate(-100, -100)
4 sub(one, two)
5 Coord: {'y': 0, 'x': -200}
6 add(one, three)
7 Coord: {'y': 100, 'x': 0}

咱們指望在不更改座標對象one, two, three的前提下one減去two的值是{x: 0, y: 0},one加上three的值是{x: 100, y: 200}。與其給每一個方法都加上參數和返回值邊界檢查的邏輯,咱們來寫一個邊界檢查的裝飾器!

 1 def wrapper(func):
 2      def checker(a, b): # 1
 3          if a.x < 0 or a.y < 0:
 4              a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
 5          if b.x < 0 or b.y < 0:
 6              b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
 7          ret = func(a, b)
 8          if ret.x < 0 or ret.y < 0:
 9              ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
10          return ret
11      return checker
12 add = wrapper(add)
13 sub = wrapper(sub)
14 sub(one, two)
15 Coord: {'y': 0, 'x': 0}
16 add(one, three)
17 Coord: {'y': 200, 'x': 100}

這個裝飾器能想先前的裝飾器例子同樣進行工做,返回一個通過修改的函數,可是在這個例子中,它可以對函數的輸入參數和返回值作一些很是有用的檢查和格式化工做,將負值的x和 y替換成0。

顯而易見,經過這樣的方式,咱們的代碼變得更加簡潔:將邊界檢查的邏輯隔離到單獨的方法中,而後經過裝飾器包裝的方式應用到咱們須要進行檢查的地方。另一種方式經過在計算方法的開始處和返回值以前調用邊界檢查的方法也可以達到一樣的目的。可是不可置否的是,使用裝飾器可以讓咱們以最少的代碼量達到座標邊界檢查的目的。事實上,若是咱們是在裝飾本身定義的方法的話,咱們可以讓裝飾器應用的更加有逼格。

10. 使用 @ 標識符將裝飾器應用到函數

Python2.4支持使用標識符@將裝飾器應用在函數上,只須要在函數的定義前加上@和裝飾器的名稱。在上一節的例子裏咱們是將本來的方法用裝飾後的方法代替:

1 add = wrapper(add)

這種方式可以在任什麼時候候對任意方法進行包裝。可是若是咱們自定義一個方法,咱們可使用@進行裝飾:

1 @wrapper
2  def add(a, b):
3      return Coordinate(a.x + b.x, a.y + b.y)

須要明白的是,這樣的作法和先前簡單的用包裝方法替代原有方法是一毛同樣的, python只是加了一些語法糖讓裝飾的行爲更加的直接明確和優雅一點。

11. *args and **kwargs

咱們已經完成了一個有用的裝飾器,可是因爲硬編碼的緣由它只能應用在一類具體的方法上,這類方法接收兩個參數,傳遞給閉包捕獲的函數。若是咱們想實現一個可以應用在任何方法上的裝飾器要怎麼作呢?再好比,若是咱們要實現一個能應用在任何方法上的相似於計數器的裝飾器,不須要改變原有方法的任何邏輯。這意味着裝飾器可以接受擁有任何簽名的函數做爲本身的被裝飾方法,同時可以用傳遞給它的參數對被裝飾的方法進行調用。

很是巧合的是Python正好有支持這個特性的語法。能夠閱讀 Python Tutorial 獲取更多的細節。當定義函數的時候使用了*,意味着那些經過位置傳遞的參數將會被放在帶有*前綴的變量中, 因此:

 1 def one(*args):
 2      print args # 1
 3 one()
 4 ()
 5 one(1, 2, 3)
 6 (1, 2, 3)
 7 def two(x, y, *args): # 2
 8      print x, y, args
 9 two('a', 'b', 'c')
10 a b ('c',)

第一個函數one只是簡單地講任何傳遞過來的位置參數所有打印出來而已,大家可以看到,在代碼#1處咱們只是引用了函數內的變量args, *args僅僅只是用在函數定義的時候用來表示位置參數應該存儲在變量args裏面。Python容許咱們制定一些參數而且經過args捕獲其餘全部剩餘的未被捕捉的位置參數,就像#2處所示的那樣。
*操做符在函數被調用的時候也能使用。意義基本是同樣的。當調用一個函數的時候,一個用*標誌的變量意思是變量裏面的內容須要被提取出來而後當作位置參數被使用。一樣的,來看個例子:

1 def add(x, y):
2      return x + y
3 lst = [1,2]
4 add(lst[0], lst[1]) # 1
5 3
6 add(*lst) # 2
7 3

#1處的代碼和#2處的代碼所作的事情實際上是同樣的,在#2處,python爲咱們所作的事其實也能夠手動完成。這也不是什麼壞事,*args要麼是表示調用方法大的時候額外的參數能夠從一個可迭代列表中取得,要麼就是定義方法的時候標誌這個方法可以接受任意的位置參數。
接下來提到的**會稍多更復雜一點,**表明着鍵值對的餐宿字典,和*所表明的意義相差無幾,也很簡單對不對:

1 def foo(**kwargs):
2      print kwargs
3 foo()
4 {}
5 foo(x=1, y=2)
6 {'y': 2, 'x': 1}

當咱們定義一個函數的時候,咱們可以用**kwargs來代表,全部未被捕獲的關鍵字參數都應該存儲在kwargs的字典中。如前所訴,argshe kwargs並非python語法的一部分,但在定義函數的時候,使用這樣的變量名算是一個不成文的約定。和*同樣,咱們一樣能夠在定義或者調用函數的時候使用**。

1 dct = {'x': 1, 'y': 2}
2 def bar(x, y):
3      return x + y
4 bar(**dct)
5 3

12. 更通用的裝飾器

有了這招新的技能,咱們隨隨便便就能夠寫一個可以記錄下傳遞給函數參數的裝飾器了。先來個簡單地把日誌輸出到界面的例子:

1 def logger(func):
2      def inner(*args, **kwargs): #1
3          print "Arguments were: %s, %s" % (args, kwargs)
4          return func(*args, **kwargs) #2
5      return inner

請注意咱們的函數inner,它可以接受任意數量和類型的參數並把它們傳遞給被包裝的方法,這讓咱們可以用這個裝飾器來裝飾任何方法。

 1 @logger
 2  def foo1(x, y=1):
 3      return x * y
 4 @logger
 5  def foo2():
 6      return 2
 7 foo1(5, 4)
 8 Arguments were: (5, 4), {}
 9 20
10 foo1(1)
11 Arguments were: (1,), {}
12 1
13 foo2()
14 Arguments were: (), {}
15 2

隨便調用咱們定義的哪一個方法,相應的日誌也會打印到輸出窗口,和咱們預期的同樣。

相關文章
相關標籤/搜索