好湿?好紧?好多水好爽自慰,久久久噜久噜久久综合,成人做爰A片免费看黄冈,机机对机机30分钟无遮挡

主頁 > 知識庫 > Python 中的函數裝飾器和閉包詳解

Python 中的函數裝飾器和閉包詳解

熱門標簽:鎮江人工外呼系統供應商 申請辦個400電話號碼 400電話辦理費用收費 千呼ai電話機器人免費 騰訊地圖標注有什么版本 外呼系統前面有錄音播放嗎 高德地圖標注字母 柳州正規電銷機器人收費 深圳網絡外呼系統代理商

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

裝飾器的基本概念

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

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

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

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

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

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

>>> 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>

可以看到,調用 target 函數執行的是 inner 函數,這里的 target 實際上是 inner 的引用。

何時執行裝飾器

裝飾器的另一個關鍵特性是,它們在被裝飾函數定義時立即執行,這通常是發生在導入模塊的時候。

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

# 存儲被裝飾器 @register 裝飾的函數
registry = []

# 裝飾器
def register(func):
 print(f"注冊函數 -> {func}")
 # 記錄被裝飾的函數
 registry.append(func)
 return func

@register
def f1():
 print("執行 f1()")

@register
def f2():
 print("執行 f2()")

def f3():
 print("執行 f3()")

if __name__ == "__main__":
 print("執行主函數")
 print("registry -> ", registry)
 f1()
 f2()
 f3()

現在我們在命令行執行這個腳本:

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

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

如果只是單純的導入 registration.py 模塊而不運行:

>>> import registration
注冊函數 -> function f1 at 0x0000022670012280>
注冊函數 -> function f2 at 0x0000022670012310>

查看 registry 中的值:

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

這個例子主要說明:裝飾器在導入模塊時立即執行,而被裝飾的函數只有在明確調用時才運行。這也突出了 Python 中導入時和運行時這個兩個概念的區別。

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

  • 示例中裝飾器和被裝飾函數在同一個模塊中。實際使用中,裝飾器通常在一個單獨的模塊中定義,然后再應用到其它模塊的函數上。
  • 示例中 register 裝飾器返回的函數和傳入的參數相同。實際使用中,裝飾器會在內部定義一個新函數,然后將其返回。

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

變量作用域的規則

我們來看下面這個例子,一個函數讀取一個局部變量 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

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

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

現在,我們來看一個不尋常的例子:

>>> 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 函數的前兩行和 f1 相同,然后再給 b 賦值。可是,在賦值之前,第二個 print 失敗了。這是因為Python 在編譯函數的定義體時,發現在函數中有給 b 賦值的語句,因此判斷它是局部變量。而在上述示例中,當我們打印局部變量 b 時,它并沒有被綁定值,故而報錯。

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

如果想把上述示例中的 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

閉包

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

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

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

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

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

我們可以用一個類來實現:

class Averager:
 def __init__(self):
  self.serial = []

 def __call__(self, price):
  self.serial.append(price)
  return sum(self.serial) / len(self.serial)

Averager 的實例是一個可調用對象。

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

也可以使用一個函數來實現:

>>> 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 的局部變量,但是當我們調用 avg(10) 時,make_averager 函數已經返回了,它的作用域不是應該消失了嗎?

實際上,在 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]

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

注意:

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

nonlocal 聲明

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

>>> 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 被當成 averager 的局部變量,而不是我們期望的自由變量。這是因為 count += 1 相當于 count = count + 1。因此,我們在 averager 函數體中實際包含了給 count 賦值的操作,這就把 count 變成局部變量。total 也有這個問題。

為了解決這個問題,Python3 引入了 nonlocal 關鍵字,用于聲明自由變量。使用 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 兩個裝飾器應用到同一個函數 f() 上,實際相當于 f = d1(d2(f))

也就是說,下屬代碼:

@d1
@d2
def f():
	pass

等同于:

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

參數化裝飾器

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

以本文開頭的 register 裝飾器為例,我們為它添加一個 active 參數,如果置為 False,那就不注冊這個函數。

registry = []

def register(active=True):
 def decorate(func):
  if active:
   print(f"注冊函數 -> {func}")
   # 記錄被裝飾的函數
   registry.append(func)
  return func

 return decorate

@register()
def f1():
 print("執行 f1")

@register(active=False)
def f2():
 print("執行 f2")

現在我們導入這個模塊:

>>> import registration
注冊函數 -> function f1 at 0x0000016D80402280>

可以看到只注冊了 f1 函數。

實現一個簡單的裝飾器

這里我們使用嵌套函數實現一個簡單的裝飾器:計算被裝飾函數執行的耗時,并將函數名、參數和執行的結果打印出來。

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 參數傳遞給 clock 函數,然后 clock 函數返回 clocked 函數,Python 解釋器會把 clocked 賦值給 factorial。所以,如果我們查看 factorial__name__ 屬性,會發現它的值是 clocked 而不是 factorial

>>> factorial.__name__
'clocked'

所以,factorial 保存的是 clocked 的引用,每次調用 factorial 實際上都是在調用 clocked 函數。

我們也可以使用 functools.wraps 裝飾器把 func 的一些屬性復制到 clocked 函數上,例如:__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'

標準庫中的裝飾器

使用 functools.lru_cache 做備忘

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

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

我們來看看不使用 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

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

現在我們使用 functools.lru_cache 優化一下:

>>> @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

可以看到節省了一般的執行時間,并且 n 的每個值只調用了一次函數。

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

除了優化遞歸算法之外,lru_cache 在從 WEB 獲取信息的應用中也能發揮巨大作用。

lru_cache 還有兩個可選參數:

def lru_cache(maxsize=128, typed=False):
  • maxsize:最多可存儲的調用結果的個數。緩存滿了之后,舊的結果被丟棄。為了獲取最佳的性能,maxsize 應該設置為 2 的冪。
  • typed:如果置為 True,會把不同參數類型得到的結果分開保存。例如:f(3.0)f(3) 會被當成不同的調用。

單分派泛函數

假設我們現在開發一個調試 WEB 應用的工具:生成 HTML,顯示不同類型的 Python 對象。

我們可以這樣編寫一個函數:

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

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

  • str:把字符串內部的 \n 替換為 br>\n,并且使用 p> 替換 pre>
  • int:以十進制和十六進制顯示數字;
  • list:顯示一個 HTML 列表,根據各個元素的類型格式化;

最常用的方式就是寫 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>\nli>".join(htmlize(item) for item in obj)
  return "ul>\nli>" + content + "/li>\n/ul>"
 else:
  content = f"pre>{obj}/pre>"
  return content

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

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>\nli>".join(htmlize(item) for item in seq)
 return "ul>\nli>" + content + "/li>\n/ul>"

這里我們為每一個需要特殊處理的類型都定義另一個專門的函數。

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

到此這篇關于Python 中的函數裝飾器和閉包詳解的文章就介紹到這了,更多相關Python函數裝飾器和閉包內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:
  • python閉包和裝飾器你了解嗎
  • Python 中閉包與裝飾器案例詳解
  • Python必備基礎之閉包和裝飾器知識總結
  • python高級語法之閉包和裝飾器詳解
  • python閉包的實例詳解

標簽:合肥 平頂山 郴州 哈爾濱 烏蘭察布 海南 烏蘭察布 大慶

巨人網絡通訊聲明:本文標題《Python 中的函數裝飾器和閉包詳解》,本文關鍵詞  Python,中的,函數,裝飾,器,;如發現本文內容存在版權問題,煩請提供相關信息告之我們,我們將及時溝通與處理。本站內容系統采集于網絡,涉及言論、版權與本站無關。
  • 相關文章
  • 下面列出與本文章《Python 中的函數裝飾器和閉包詳解》相關的同類信息!
  • 本頁收集關于Python 中的函數裝飾器和閉包詳解的相關信息資訊供網民參考!
  • 推薦文章
    主站蜘蛛池模板: 美女胸罩秘?露出奶头图片| 欧美女人天堂| 久久天天躁夜夜躁狠狠躁2015| 91免费精品国偷自产在线在线| 九色l91poryl国产| 8050午夜二级一片 更新时间| 欧美freesex黑人又粗又| 成人福利视频网| ??成人福利午夜A片| 国产精品人妻系列无码大地资源| 18以下岁毛片在免费播放| gogogo免费高清人体摄影| 精品日产免费二区| 拍拍拍免费网站| 公啊?好痛?嗯?轻一点黄| 伊人久久久久久久久久| 韩国理论在线观看2024| 日韩AV精品国产AV在线观看 | 男人捅女人app| 天欲在线观看| 公牛与女人又大又爽| 国产午夜性春猛交XXXX公交车 | 成人黄色激情视频| 天天射天天干| 一级片在线免费看| 中文字幕一区二区三区四区五区六| 91国偷自产一区二区三区蜜芽| 男人用嘴添女的下身视频| 欧美亚洲人成网站在线观看刚交| 九九久久自然熟的香蕉图片| 少妇厨房激情婬乱1一15视频| 理发店强好久| 青青草白白色| 九七影视| 热久久| 中文字幕精品久久一二三区红杏| 甜蜜惩罚在线观看| 国产一区二区三区免费播放| 国产乔依琳视频在线播放| 噜噜噜噜精品视频在线观看| 荡女淫春2在线观看|