乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      Python 中的函數(shù)裝飾器和閉包

       新進小設(shè)計 2021-05-05

      函數(shù)裝飾器可以被用于增強方法的某些行為,如果想自己實現(xiàn)裝飾器,則必須了解閉包的概念。

      裝飾器的基本概念

      裝飾器是一個可調(diào)用對象,它的參數(shù)是另一個函數(shù),稱為被裝飾函數(shù)。裝飾器可以修改這個函數(shù)再將其返回,也可以將其替換為另一個函數(shù)或者可調(diào)用對象。

      例如:有個名為 decorate 的裝飾器:

      @decorate
      def target():
          print('running target()')

      上述代碼的寫法和以下寫法的效果是一樣的:

      def target():
          print('running target()')
          
      target = decorate(target)

      但是,它們返回的 target 不一定是原來的那個 target 函數(shù),例如下面這個例子:

      >>> def deco(func):
      ...     def inner():
      ...         print('running inner()')
      ...     return inner
      ...
      >>> @deco
      ... def target():
      ...     print('running target()')
      ...
      >>> target()
      running inner()
      >>> target
      <function deco.<locals>.inner at 0x0000013D88563040>

      可以看到,調(diào)用 target 函數(shù)執(zhí)行的是 inner 函數(shù),這里的 target 實際上是 inner 的引用。

      何時執(zhí)行裝飾器

      裝飾器的另一個關(guān)鍵特性是,它們在被裝飾函數(shù)定義時立即執(zhí)行,這通常是發(fā)生在導(dǎo)入模塊的時候。

      例如下面的這個模塊:registration.py

      # 存儲被裝飾器 @register 裝飾的函數(shù)
      registry = []
      
      
      # 裝飾器
      def register(func):
          print(f"注冊函數(shù) -> {func}")
          # 記錄被裝飾的函數(shù)
          registry.append(func)
          return func
      
      
      @register
      def f1():
          print("執(zhí)行 f1()")
      
      
      @register
      def f2():
          print("執(zhí)行 f2()")
      
      
      def f3():
          print("執(zhí)行 f3()")
      
      
      if __name__ == "__main__":
          print("執(zhí)行主函數(shù)")
          print("registry -> ", registry)
          f1()
          f2()
          f3()

      現(xiàn)在我們在命令行執(zhí)行這個腳本:

      $ python registration.py
      注冊函數(shù) -> <function f1 at 0x000001F6FC8320D0>
      注冊函數(shù) -> <function f2 at 0x000001F6FC832160>
      執(zhí)行主函數(shù)
      registry ->  [<function f1 at 0x000001F6FC8320D0>, <function f2 at 0x000001F6FC832160>]
      執(zhí)行 f1()
      執(zhí)行 f2()
      執(zhí)行 f3()

      這里我們可以看到,在主函數(shù)執(zhí)行之前,register 已經(jīng)執(zhí)行了兩次。加載模塊后,registry 中已經(jīng)有兩個被裝飾函數(shù)的引用:f1f2。不過這兩個函數(shù)以及 f3 都是在腳本中明確調(diào)用后才開始執(zhí)行的。

      如果只是單純的導(dǎo)入 registration.py 模塊而不運行:

      >>> import registration
      注冊函數(shù) -> <function f1 at 0x0000022670012280>
      注冊函數(shù) -> <function f2 at 0x0000022670012310>

      查看 registry 中的值:

      >>> registration.registry
      [<function f1 at 0x0000022670012280>, <function f2 at 0x0000022670012310>]

      這個例子主要說明:裝飾器在導(dǎo)入模塊時立即執(zhí)行,而被裝飾的函數(shù)只有在明確調(diào)用時才運行。這也突出了 Python 中導(dǎo)入時和運行時這個兩個概念的區(qū)別。

      在裝飾器的實際使用中,有兩點和示例是不同的:

      • 示例中裝飾器和被裝飾函數(shù)在同一個模塊中。實際使用中,裝飾器通常在一個單獨的模塊中定義,然后再應(yīng)用到其它模塊的函數(shù)上。

      • 示例中 register 裝飾器返回的函數(shù)和傳入的參數(shù)相同。實際使用中,裝飾器會在內(nèi)部定義一個新函數(shù),然后將其返回。

      裝飾器內(nèi)部定義并返回新函數(shù)的做法需要靠閉包才能正常運作。為了理解閉包,則必須先了解 Python 中的變量作用域。

      變量作用域的規(guī)則

      我們來看下面這個例子,一個函數(shù)讀取一個局部變量 a,一個全局變量 b

      >>> def f1(a):
      ...     print(a)
      ...     print(b)
      ...
      >>> f1(3)
      3
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 3, in f1
      NameError: name 'b' is not defined

      出現(xiàn)錯誤并不奇怪。如果我們先給 b 賦值,再調(diào)用 f1,那就不會出錯了:

      >>> b = 1
      >>> f1(3)
      3
      1

      現(xiàn)在,我們來看一個不尋常的例子:

      >>> b = 1
      >>> def f2(a):
      ...     print(a)
      ...     print(b)
      ...     b = 2
      ...
      >>> f2(3)
      3
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 3, in f2
      UnboundLocalError: local variable 'b' referenced before assignment

      這里,f2 函數(shù)的前兩行和 f1 相同,然后再給 b 賦值??墒?,在賦值之前,第二個 print 失敗了。這是因為Python 在編譯函數(shù)的定義體時,發(fā)現(xiàn)在函數(shù)中有給 b 賦值的語句,因此判斷它是局部變量。而在上述示例中,當(dāng)我們打印局部變量 b 時,它并沒有被綁定值,故而報錯。

      Python 不要求聲明變量,但是會把在函數(shù)定義體中賦值的變量當(dāng)成局部變量。

      如果想把上述示例中的 b 看成全局變量,則需要使用 global 聲明:

      >>> b = 1
      >>> def f3(a):
      ...     global b
      ...     print(a)
      ...     print(b)
      ...     b = 2
      ...
      >>> f3(3)
      3
      1
      >>> b
      2
      >>> f3(3)
      3
      2

      閉包

      閉包是指延伸了作用域的函數(shù),其中包含了函數(shù)定義體中的引用,以及不在定義體中定義的非全局變量

      我們通過以下示例來理解這句話。

      假設(shè)我們有這種需求,計算某個商品在整個歷史中的平均收盤價格(商品每天的價格會變化)。例如:

      >>> avg(10)
      10.0
      >>> avg(11)
      10.5
      >>> avg(12)
      11.0

      那么如何獲取 avg 函數(shù)?歷史收盤價格又是如何保存的?

      我們可以用一個類來實現(xiàn):

      class Averager:
          def __init__(self):
              self.serial = []
      
          def __call__(self, price):
              self.serial.append(price)
              return sum(self.serial) / len(self.serial)

      Averager 的實例是一個可調(diào)用對象。

      >>> avg = Averager()
      >>> avg(10)
      10.0
      >>> avg(11)
      10.5
      >>> avg(12)
      11.0

      也可以使用一個函數(shù)來實現(xiàn):

      >>> def make_averager():
      ...     serial = []
      ...     def averager(price):
      ...         serial.append(price)
      ...         return sum(serial) / len(serial)
      ...     return averager
      ...
      >>> avg = make_averager()
      >>> avg(10)
      10.0
      >>> avg(11)
      10.5
      >>> avg(12)
      11.0

      第一種寫法很明顯的可以看到,所有歷史收盤價均保存在實例變量 self.serial 中。

      第二種寫法我們要好好的分析一下:serialmake_averager 的局部變量,但是當(dāng)我們調(diào)用 avg(10) 時,make_averager 函數(shù)已經(jīng)返回了,它的作用域不是應(yīng)該消失了嗎?

      實際上,在 averager 函數(shù)中,serial自由變量(未在本地作用域中綁定的變量)。如下圖所示:

      averager 的閉包延伸到它的作用域之外,包含了自由變量 serial

      我們可以在 averager 返回對象的 __code__ 屬性中查看它的局部變量和自由變量的名字。

      >>> avg.__code__.co_varnames
      ('price',)
      >>> avg.__code__.co_freevars
      ('serial',)

      自由變量 serial 綁定的值存放在 avg 對象的 __closure__ 屬性中,它是一個元組,里面的元素是 cell 對象,它的 cell_contents 屬性保存實際的值:

      >>> avg.__closure__
      (<cell at 0x000002266FF99430: list object at 0x00000226702841C0>,)
      >>> avg.__closure__[0].cell_contents
      [10, 11, 12]

      綜上所述,閉包是一種函數(shù),它會保留定義函數(shù)時存在的自由變量的綁定值,這樣在我們調(diào)用這個函數(shù)時,即使作用域不在了,仍然可以使用這些綁定的值。

      注意:

      只有嵌套在其它函數(shù)中的函數(shù)才可能需要處理不在全局作用域中的外部變量。

      nonlocal 聲明

      前面的 make_averager 方法的效率并不高,我們可以只保存當(dāng)前的總值和元素個數(shù),再使用它們計算平均值。下面是我們更改后的函數(shù)體:

      >>> def make_averager():
      ...     count = total = 0
      ...     def averager(price):
      ...         count += 1
      ...         total += price
      ...         return total / count
      ...     return averager

      但是這個寫法實際上是有問題的,我們先運行再分析:

      >>> avg = make_averager()
      >>> avg(10)
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 4, in averager
      UnboundLocalError: local variable 'count' referenced before assignment

      這里 count 被當(dāng)成 averager 的局部變量,而不是我們期望的自由變量。這是因為 count += 1 相當(dāng)于 count = count + 1。因此,我們在 averager 函數(shù)體中實際包含了給 count 賦值的操作,這就把 count 變成局部變量。total 也有這個問題。

      為了解決這個問題,Python3 引入了 nonlocal 關(guān)鍵字,用于聲明自由變量。使用 nonlocal 修改上述的例子:

      >>> def make_averager():
      ...     count = total = 0
      ...     def averager(price):
      ...         nonlocal count, total
      ...         count += 1
      ...         total += price
      ...         return total / count
      ...     return averager
      ...
      >>> avg = make_averager()
      >>> avg(10)
      10.0
      >>> avg(11)
      10.5
      >>> avg(12)
      11.0

      疊放裝飾器

      如果我們把 @d1@d2 兩個裝飾器應(yīng)用到同一個函數(shù) f() 上,實際相當(dāng)于 f = d1(d2(f))。

      也就是說,下屬代碼:

      @d1
      @d2
      def f():
      pass

      等同于:

      def f():
      pass
      
      
      f = d1(d2(f))

      參數(shù)化裝飾器

      Python 會把被裝飾的參數(shù)作為第一個參數(shù)傳遞給裝飾器函數(shù),那么如何讓裝飾器接受其它的參數(shù)呢?這里我們需要定義一個裝飾器工廠函數(shù),返回真正的裝飾器函數(shù)。

      以本文開頭的 register 裝飾器為例,我們?yōu)樗砑右粋€ active 參數(shù),如果置為 False,那就不注冊這個函數(shù)。

      registry = []
      
      
      def register(active=True):
          def decorate(func):
              if active:
                  print(f"注冊函數(shù) -> {func}")
                  # 記錄被裝飾的函數(shù)
                  registry.append(func)
              return func
      
          return decorate
      
      
      @register()
      def f1():
          print("執(zhí)行 f1")
      
      
      @register(active=False)
      def f2():
          print("執(zhí)行 f2")

      現(xiàn)在我們導(dǎo)入這個模塊:

      >>> import registration
      注冊函數(shù) -> <function f1 at 0x0000016D80402280>

      可以看到只注冊了 f1 函數(shù)。

      實現(xiàn)一個簡單的裝飾器

      這里我們使用嵌套函數(shù)實現(xiàn)一個簡單的裝飾器:計算被裝飾函數(shù)執(zhí)行的耗時,并將函數(shù)名、參數(shù)和執(zhí)行的結(jié)果打印出來。

      import time
      
      
      def clock(func):
          def clocked(*args):
              start_time = time.perf_counter()
              result = func(*args)
              cost = time.perf_counter() - start_time
              print(
                  "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
              )
              return result
      
          return clocked

      下面我們來試試這個裝飾器:

      >>> @clock
      ... def factorial(n):
      ...     # 計算 n 的階乘
      ...     return 1 if n < 2 else n * factorial(n - 1)
      >>> 
      >>> factorial(6)
      [0.00] factorial(['1']) -> 1
      [0.00] factorial(['2']) -> 2
      [0.00] factorial(['3']) -> 6
      [0.00] factorial(['4']) -> 24
      [0.00] factorial(['5']) -> 120
      [0.00] factorial(['6']) -> 720
      720

      具體來分析一下,這里 factorial 作為 func 參數(shù)傳遞給 clock 函數(shù),然后 clock 函數(shù)返回 clocked 函數(shù),Python 解釋器會把 clocked 賦值給 factorial。所以,如果我們查看 factorial__name__ 屬性,會發(fā)現(xiàn)它的值是 clocked 而不是 factorial。

      >>> factorial.__name__
      'clocked'

      所以,factorial 保存的是 clocked 的引用,每次調(diào)用 factorial 實際上都是在調(diào)用 clocked 函數(shù)。

      我們也可以使用 functools.wraps 裝飾器把 func 的一些屬性復(fù)制到 clocked 函數(shù)上,例如:__name____doc__

      def clock(func):
          @functools.wraps(func)
          def clocked(*args):
              start_time = time.perf_counter()
              result = func(*args)
              cost = time.perf_counter() - start_time
              print(
                  "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
              )
              return result
      
          return clocked
      >>> 
      >>> @clock
      ... def factorial(n):
      ...     return 1 if n < 2 else n * factorial(n - 1)
      >>> 
      >>> factorial.__name__
      'factorial'

      標(biāo)準(zhǔn)庫中的裝飾器

      使用 functools.lru_cache 做備忘

      functools.lru_cache 會把耗時的函數(shù)的結(jié)果保存起來,避免傳入相同的參數(shù)時的重復(fù)計算。lru 的意思是 Least Recently Used,表示緩存不會無限增長,一段時間不用的緩存條目會被丟棄。

      lru_cache 非常適合計算第 n 個斐波那契數(shù)這樣的慢速遞歸函數(shù)。

      我們來看看不使用 lru_cache 時的情況:

      >>> @clock
      ... def fibonacci(n):
      ...     return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
      ...
      >>> fibonacci(6)
      [0.00000040] fibonacci(['0']) -> 0
      [0.00000060] fibonacci(['1']) -> 1
      [0.00030500] fibonacci(['2']) -> 1
      [0.00000030] fibonacci(['1']) -> 1
      [0.00000040] fibonacci(['0']) -> 0
      [0.00000060] fibonacci(['1']) -> 1
      [0.00042110] fibonacci(['2']) -> 1
      [0.00074440] fibonacci(['3']) -> 2
      [0.00128530] fibonacci(['4']) -> 3
      [0.00000020] fibonacci(['1']) -> 1
      [0.00000030] fibonacci(['0']) -> 0
      [0.00000050] fibonacci(['1']) -> 1
      [0.00035500] fibonacci(['2']) -> 1
      [0.00055270] fibonacci(['3']) -> 2
      [0.00000030] fibonacci(['0']) -> 0
      [0.00000060] fibonacci(['1']) -> 1
      [0.00041220] fibonacci(['2']) -> 1
      [0.00000040] fibonacci(['1']) -> 1
      [0.00000040] fibonacci(['0']) -> 0
      [0.00000050] fibonacci(['1']) -> 1
      [0.00032410] fibonacci(['2']) -> 1
      [0.00061420] fibonacci(['3']) -> 2
      [0.00122760] fibonacci(['4']) -> 3
      [0.00206850] fibonacci(['5']) -> 5
      [0.00352630] fibonacci(['6']) -> 8
      8

      這種方式有很多重復(fù)的計算,例如 fibonacci(['1']) 執(zhí)行了 8 次,fibonacci(['2']) 執(zhí)行了 5 次等等。

      現(xiàn)在我們使用 functools.lru_cache 優(yōu)化一下:

      >>> @functools.lru_cache
      ... @clock
      ... def fibonacci(n):
      ...     return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
      ...
      >>> fibonacci(6)
      [0.00000060] fibonacci(['0']) -> 0
      [0.00000070] fibonacci(['1']) -> 1
      [0.00106320] fibonacci(['2']) -> 1
      [0.00000080] fibonacci(['3']) -> 2
      [0.00132790] fibonacci(['4']) -> 3
      [0.00000060] fibonacci(['5']) -> 5
      [0.00159670] fibonacci(['6']) -> 8
      8

      可以看到節(jié)省了一半的執(zhí)行時間,并且 n 的每個值只調(diào)用了一次函數(shù)。

      在執(zhí)行 fibonacci(30) 時,如果使用未優(yōu)化的版本需要 141 秒,使用優(yōu)化后的版本只需要 0.002 秒。

      除了優(yōu)化遞歸算法之外,lru_cache 在從 WEB 獲取信息的應(yīng)用中也能發(fā)揮巨大作用。

      lru_cache 還有兩個可選參數(shù):

      def lru_cache(maxsize=128, typed=False):
      • maxsize:最多可存儲的調(diào)用結(jié)果的個數(shù)。緩存滿了之后,舊的結(jié)果被丟棄。為了獲取最佳的性能,maxsize 應(yīng)該設(shè)置為 2 的冪。

      • typed:如果置為 True,會把不同參數(shù)類型得到的結(jié)果分開保存。例如:f(3.0)f(3) 會被當(dāng)成不同的調(diào)用。

      單分派泛函數(shù)

      假設(shè)我們現(xiàn)在開發(fā)一個調(diào)試 WEB 應(yīng)用的工具:生成 HTML,顯示不同類型的 Python 對象。

      我們可以這樣編寫一個函數(shù):

      import html
      
      
      def htmlize(obj):
          content = html.escape(repr(obj))
          return f"<pre>{content}</pre>"

      現(xiàn)在我們需要做一些拓展,讓它使用特別的方式顯示某些特定類型:

      • str:把字符串內(nèi)部的 \n 替換為 <br>\n,并且使用 <p> 替換 <pre>

      • int:以十進制和十六進制顯示數(shù)字;

      • list:顯示一個 HTML 列表,根據(jù)各個元素的類型格式化;

      最常用的方式就是寫 if...elif..else 判斷:

      import numbers
      from collections.abc import MutableSequence
      
      
      def htmlize(obj):
          if isinstance(obj, str):
              content = obj.replace("\n", "<br>\n")
              return f"<p>{content}</p>"
          elif isinstance(obj, numbers.Integral):
              content = f"{obj} ({hex(obj)})"
              return f"<pre>{content}</pre>"
          elif isinstance(obj, MutableSequence):
              content = "</li>\n<li>".join(htmlize(item) for item in obj)
              return "<ul>\n<li>" + content + "</li>\n</ul>"
          else:
              content = f"<pre>{obj}</pre>"
              return content

      如果想添加新的類型判斷,只會將函數(shù)越寫越長,并且各個類型之間耦合度較高,不利于維護。

      Python 3.4 新增的 functools.singledispatch 裝飾器可以將整個方案拆分成多個模塊。

      import numbers
      from collections.abc import MutableSequence
      from functools import singledispatch
      
      
      @singledispatch
      def htmlize(obj):
          content = f"<pre>{obj}</pre>"
          return content
      
      
      @htmlize.register(str)
      def _(text):
          content = text.replace("\n", "<br>\n")
          return f"<p>{content}</p>"
      
      
      @htmlize.register(numbers.Integral)
      def _(num):
          content = f"{num} ({hex(num)})"
          return f"<pre>{content}</pre>"
      
      
      @htmlize.register(MutableSequence)
      def _(seq):
          content = "</li>\n<li>".join(htmlize(item) for item in seq)
          return "<ul>\n<li>" + content + "</li>\n</ul>"

      這里我們?yōu)槊恳粋€需要特殊處理的類型都定義另一個專門的函數(shù)。

      functools.singledispatch 的更詳細的文檔參考:https://www./dev/peps/pep-0443/。

        本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
        轉(zhuǎn)藏 分享 獻花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多