<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          5分鐘掌握 Python 中的裝飾器

          共 7088字,需瀏覽 15分鐘

           ·

          2021-02-26 12:02

          作者 | 鞏慶奎
          來源 | Python中文社區(qū)
          python中的裝飾器用于修飾函數(shù),以增強(qiáng)函數(shù)的行為:記錄函數(shù)執(zhí)行時(shí)間,建立和撤銷環(huán)境,記錄日志等。裝飾器可以在不修改函數(shù)內(nèi)部代碼的前提下實(shí)現(xiàn)以上增強(qiáng)行為。如下代碼建立一個(gè)計(jì)時(shí)裝飾器,隨后描述其工作原理。
          import time 


          def timethis(func):
              def inner(*args,**kwargs):
                  print('start timer:')
                  start = time.time()
                  result = func(*args,**kwargs) 
                  end = time.time()
                  print('end timer:%fs.'%(end - start))
                  return result 
              return inner

          @timethis
          def sleeps(seconds):
              print('  sleeps begin:')
              time.sleep(seconds)
              print('    sleep %d seconds.\n  sleeps over.'%seconds)
              return seconds

          print(sleeps(3))
          執(zhí)行以上代碼,輸出:
          start timer:
            sleeps begin:
              sleep 3 seconds.
            sleeps over.
          end timer:3.002512s.
          3
          可見,timethis裝飾器實(shí)現(xiàn)了為sleeps函數(shù)計(jì)時(shí)的功能。其關(guān)鍵在于@標(biāo)識(shí)符的使用。

          一、理解@標(biāo)識(shí)符

          @標(biāo)識(shí)符是Pyton的語法糖,定義被裝飾函數(shù)時(shí)使用@timethis修飾和用語句sleeps = timethis(sleeps)是等價(jià)的。
          @timethis
          def sleeps(seconds):
              print('  sleeps begin:')
              time.sleep(seconds)
              print('    sleep %d seconds.\n  sleeps over.'%seconds)
              return seconds
          相當(dāng)于
          def sleeps(seconds):
              print('  sleeps begin:')
              time.sleep(seconds)
              print('    sleep %d seconds.\n  sleeps over.'%seconds)
              return seconds
              
          sleeps = timethis(sleeps)
          @語法只是裝飾器調(diào)用的便捷方式:將被裝飾函數(shù)sleeps作為參數(shù)傳給裝飾器函數(shù),再將裝飾器返回值重新綁定到原sleeps變量上。理解了裝飾器的使用方法,我們一步步來理解其定義過程。

          二、裝飾器是一個(gè)函數(shù)

          • 根據(jù)上文timethis裝飾器的定義,它毫無疑問是一個(gè)函數(shù)。名稱是timethis,參數(shù)是func,返回值是inner。
          • 根據(jù) sleeps = timethis(sleeps),可知參數(shù)func是被裝飾的函數(shù)sleeps。
          • 根據(jù)return inner,可知返回值inner是嵌套定義在裝飾器中的一個(gè)函數(shù)。
          綜上,裝飾器本身是一個(gè)函數(shù),參數(shù)也是函數(shù),返回值還是函數(shù)。之所以函數(shù)可以作為裝飾器的參數(shù)和返回值,是因?yàn)楹瘮?shù)在Python中是一等對(duì)象。

          三、函數(shù)是一等對(duì)象

          編程語言中的一等對(duì)象定義為:運(yùn)行時(shí)創(chuàng)建,可賦值給變量或數(shù)據(jù)結(jié)構(gòu),可作為參數(shù)傳遞,可作為返回值返回。
          Python中整數(shù)、字符串、字典類型是一等對(duì)象,具備以上四點(diǎn)特性,理解起來沒有任何困難。但函數(shù)作為一等對(duì)象,需要我們舉例說明。

          3.1運(yùn)行時(shí)創(chuàng)建

          在Python控制臺(tái)中定義一個(gè)函數(shù)reverse,實(shí)現(xiàn)對(duì)word這個(gè)序列類型的反轉(zhuǎn)。
          >>> def reverse(word):
          ...     return word[::-1]
          ...
          >>> reverse
          <function reverse at 0x027A4C40>
          >>> reverse('hello world!')
          '!dlrow olleh'
          因其是在控制臺(tái)會(huì)話中定義的,符合第一條運(yùn)行時(shí)創(chuàng)建的要求。

          3.2可賦值給變量或數(shù)據(jù)結(jié)構(gòu)

          可以將reverse函數(shù)賦值給另外的變量,再調(diào)用。如
          >>> backward=reverse
          >>> backward('hello world!')
          '!dlrow olleh'
          輸出結(jié)果同上。所以函數(shù)符合第二條可賦值給變量的要求。

          3.3函數(shù)作為參數(shù)傳遞

          當(dāng)使用高階函數(shù),如sorted時(shí),高階函數(shù)的key關(guān)鍵字接受一個(gè)單參數(shù)函數(shù),對(duì)每個(gè)元素進(jìn)行迭代,依照這個(gè)key函數(shù)作為排序依據(jù)。
          cars = ['Honda','toyota','hyundai','byd','ford','suzuki','peuguot','nissan','citroen','kia','vw','gm','audi','bmw','beniz']
          print(sorted(cars,key=reverse))
          輸出
          ['Honda''kia''toyota''ford''byd''hyundai''audi''suzuki''gm''nissan''citroen''peuguot''bmw''vw''beniz']
          此時(shí)所有的car是依照結(jié)尾字符的先后排序的。reverse作為參數(shù)傳入高階函數(shù)。符合第三條函數(shù)可作為參數(shù)傳遞。

          3.4函數(shù)作為返回值返回

          為驗(yàn)證第四點(diǎn),我們將reverse函數(shù)包裝起來,讓他在一個(gè)函數(shù)中返回。
          def cmpLib():
              def reverse(word):
                  return word[::-1]
              return reverse
          我們?nèi)杂蒙侠信判蚝瘮?shù),key參數(shù)必須為一個(gè)單參函數(shù)。而函數(shù)backward的執(zhí)行結(jié)果是一個(gè)函數(shù),所以我們把它的調(diào)用結(jié)果作為key值。
          print(sorted(cars,key=cmpLib())
          結(jié)果
          ['Honda''kia''toyota''ford''byd''hyundai''audi''suzuki''gm''nissan''citroen''peuguot''bmw''vw''beniz']
          可見,結(jié)果正確。所以第四條函數(shù)可作為結(jié)果返回也成立。
          綜上,函數(shù)是一等對(duì)象。除了可調(diào)用性之外,函數(shù)和其他如字典、字符串、列表對(duì)象并沒有本質(zhì)區(qū)別。
          理解裝飾器我們需要的是函數(shù)一等性定義的后三點(diǎn):函數(shù)可賦值,可作參數(shù),可作返回結(jié)果。
          我們?cè)賮矸治雠c@timethis等價(jià)的sleeps = timethis(sleeps)語句:右側(cè)函數(shù)先調(diào)用。timethis是裝飾器函數(shù),被裝飾函數(shù)sleeps作為參數(shù)傳入裝飾器中;返回結(jié)果是裝飾器中定義的inner函數(shù);右側(cè)計(jì)算結(jié)果重新賦值給變量sleeps。完美符合以上三點(diǎn)。也就是說sleeps函數(shù)實(shí)際上已經(jīng)指向inner函數(shù)了。
          理解了函數(shù)一等性,就理解了函數(shù)可以作為參數(shù)傳遞和作為結(jié)果返回。那么新定義的內(nèi)部函數(shù)inner為什么采用def inner(*args,**kwargs):的參數(shù)命名形式呢?

          四、可接受任意數(shù)量參數(shù)的函數(shù)

          當(dāng)我們定義不特定數(shù)量參數(shù)的函數(shù)時(shí),可使用*開頭的參數(shù)作可接受任意數(shù)量位置參數(shù)的參數(shù),此時(shí)該參數(shù)作為一個(gè)元組使用。
          同理,可以使用**開頭的關(guān)鍵字參數(shù)接受任意數(shù)量的關(guān)鍵詞參數(shù),此時(shí)該參數(shù)作為一個(gè)字典使用。
          如果同時(shí)接受任意數(shù)量的位置參數(shù)和關(guān)鍵字參數(shù),那么只要聯(lián)合使用 * 和 ** 就可以。而 def inner(*args,**kwargs): 是約定俗成的固定寫法。來看個(gè)例子就可以理解這種寫法了。
          def star(*args,**kwargs):
              print(args,kwargs)

          star(1,2,3)
          star(4,5,name='zhang')
          star(7,name='lisi',gender='m')

          輸出結(jié)果:

          (1, 2, 3) {}
          (4, 5) {'name''zhang'}
          (7,) {'name''lisi''gender''m'}
          args搜集所有位置參數(shù),kwargs搜集所有關(guān)鍵字參數(shù)。這個(gè)技術(shù)應(yīng)用在inner函數(shù)上,恰如其分:當(dāng)我們調(diào)用@語法時(shí),只有被裝飾函數(shù)sleeps作為func參數(shù)傳入timethis裝飾器中,sleeps的參數(shù)并沒有傳入裝飾器函數(shù)中。裝飾器不知道sleeps函數(shù)的參數(shù)數(shù)量和具體值,若在其中func調(diào)用參數(shù),則相當(dāng)于調(diào)用不特定名稱和數(shù)量的參數(shù)。
          接受任意參數(shù)的inner函數(shù),進(jìn)一步將參數(shù)傳給在其中執(zhí)行的func函數(shù)。func函數(shù)是被裝飾的原函數(shù)sleeps,傳給inner函數(shù)的*args,**kwargs參數(shù),直接傳遞給了被裝飾函數(shù)func。這樣就實(shí)現(xiàn)了func(*args,**kwargs)相當(dāng)于sleeps(3)的效果。
          在完成調(diào)用原函數(shù)的基礎(chǔ)上,如何添加計(jì)時(shí)功能的呢?

          五、增強(qiáng)被裝飾函數(shù)的行為

          以下語句實(shí)現(xiàn)了統(tǒng)計(jì)函數(shù)執(zhí)行時(shí)間的功能 ,當(dāng)然也可以實(shí)現(xiàn)比如日志記錄,建立撤銷環(huán)境之類的功能,大同小異。
          print('start timer:')
          start = time.time()
          result = func(*args,**kwargs) 
          end = time.time()
          print('end timer:%fs.'%(end - start))
          很簡(jiǎn)單,就是在調(diào)用原函數(shù)的語句result = func(*args,**kwargs)前后,包裹上相應(yīng)的計(jì)時(shí)功能。
          此處func參數(shù)得以在inner內(nèi)部訪問到,還牽涉到一個(gè)不太好理解的話題——閉包,而理解閉包需要先弄清python中變量的作用域規(guī)則。

          六、變量作用域

          Python變量分全局變量,局部變量。另外函數(shù)的參數(shù)是函數(shù)的局部變量。編寫如下代碼:
          b=3
          def func(a):
              print(a)
              print(b)
              b=2

          func(2)

          讓我們猜猜運(yùn)行結(jié)果,應(yīng)該是1,3對(duì)吧,但執(zhí)行卻提示出錯(cuò):

          File "dec.py", line 47, in func
              print(b)
          UnboundLocalError: local variable 'b' referenced before assignment
          提示先用但未賦值。但b是全局變量,一般理解不論是print(b)對(duì)全局變量的讀取,還是b=2對(duì)全局變量的賦值,都不會(huì)出現(xiàn)這個(gè)問題。
          問題出在b=2語句上,Python對(duì)在函數(shù)定義體中 賦值的變量都認(rèn)為是局部變量。從而導(dǎo)致局部變量b未賦值先使用的問題。
          為解決這個(gè)問題,若在函數(shù)中重新賦值了全局變量,需要在函數(shù)中使用global聲明其為全局變量。即函數(shù)若讀取全局變量,可以直接使用。但若在函數(shù)體中重新賦值全局變量,那就需要global聲明變量是全局變量。
          新問題出現(xiàn)了:在裝飾器timethis中,func是其參數(shù),也就是局部變量,這是無疑的。那么在inner函數(shù)中是怎么訪問func的呢?這就牽涉到閉包問題了。

          七、閉包

          閉包指延伸作用域的函數(shù),函訪可訪問定義體之外定義的非全局變量。在例子中,timethis的參數(shù)func就未定義在inner函數(shù)中,而且也不是全局變量。是閉包將其延伸到了inner函數(shù)中,作為自由變量來使用。所以閉包是一種函數(shù),保留了它在定義時(shí)存在的自由變量。本例中,閉包從timethis定義行到return inner這個(gè)范圍,此時(shí)的局部變量func對(duì)于閉包中的inner函數(shù)來說,就是自由變量,可以讀取和使用。但不可在其中對(duì)自由變量賦值。
          類似于全局變量,當(dāng)我們?cè)谇短椎暮瘮?shù)中對(duì)自由變量訪問時(shí),可以自由使用。但是當(dāng)我們重新對(duì)其賦值時(shí),解釋器會(huì)把這個(gè)值視為一個(gè)局部變量。若需賦值全局變量,需引入global聲明全局變量;若需賦值自由變量,需引入nonlocal聲明自由變量。
          因此,func是作為自由變量被閉包函數(shù)inner使用的。那么以上語句之后為什么有兩個(gè)返回語句呢?

          八、返回值和返回函數(shù)

          • 第一個(gè)返回值,返回的是func的執(zhí)行結(jié)果,它屬于inner函數(shù)的返回值,等效于sleeps函數(shù)的返回值,這是sleep函數(shù)的應(yīng)有之意。保持了原函數(shù)sleeps對(duì)外結(jié)構(gòu)的一致性。
          • 第二個(gè)返回值是inner函數(shù)本身,也就是第三部分講述的函數(shù)作為返回結(jié)果的用法。依據(jù)slepps=timethi(sleeps)語法,其返回結(jié)果是inner函數(shù),傳遞給sleeps函數(shù),使sleeps函數(shù)實(shí)際上等同于inner函數(shù)。所以調(diào)用sleeps(3)相當(dāng)于調(diào)用inner(3)。再加上圍繞他的計(jì)時(shí)功能,故而無損增加了計(jì)時(shí)功能。
          綜上,理解裝飾器最重要的是將@ 語句和賦值語句等同起來。同時(shí)需要理解被裝飾函數(shù)作為參數(shù)傳入裝飾器,嵌套函數(shù)對(duì)其進(jìn)行改造,最后作為函數(shù)返回,使被裝飾函數(shù)實(shí)質(zhì)上關(guān)聯(lián)到新函數(shù)上。
          希望同學(xué)能理解并應(yīng)用!

          瀏覽 21
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.污污 | 激情小说综合网 | 爱草逼爱草逼爱草逼爱草逼爱草逼爱草逼爱草逼 | 九一超级碰 | 国产va |