![python中的函數增強神器functools模塊](http://p2.ttnews.xyz/loading.gif)
functools是一個函數增強器,主要為高階函數使用,作用於或者返回其他函數的函數,通常任何可調用的對象都可視為“函數”。主要包括以下幾個函數:
![python中的函數增強神器functools模塊](http://p2.ttnews.xyz/loading.gif)
cached_property
將類的方法轉換為屬性,該屬性的值將被計算一次,然後在實例生命週期中作為常規屬性進行緩存。 與property()類似,但增加了緩存,對於計算複雜的屬性很有用。cached_property在Python3.8之前的很多第三方庫當中都有自己的實現,比如werkzeug.utils.cached_property、django.utils.functional.cached_property
舉例如下:
<code># 在沒有cached_property之前定義類屬性 class DataSet: def __init__(self): self._data = None @property def data(self): print('開始計算數據') if not self._data: # 計算data數據 self._data = 10 * 10 print('計算data數據') return self._data obj = DataSet() print(obj.data) # 輸出 開始計算數據 計算data數據 100 print(obj.data) # 輸出 開始計算數據 100/<code>
使用變量記錄屬性數據,並在屬性計算是進行判斷,防止計算多次
<code>from functools import cached_property class DataSet: @cached_property def data(self): print('開始計算數據') return 10 * 10 obj = DataSet() print(obj.data) # 輸出: 開始計算數據 100 print(obj.data) # 輸出: 100/<code>
可以看到,data屬性函數只被計算了一次,而且無需額外定義變量計算。cached_property同時具有線程安全,在多線程中不會存在多次計算的問題。另外不支持python中的異步編程:asyncio。注意這個特性是在Python3.8中新增的。
cmp_to_key
將舊式比較功能轉換為鍵功能。 與接受關鍵功能的工具(例如sorted(),min(),max(),heapq.nlargest(),heapq.nsmallest(),itertools.groupby())一起使用。 該函數主要用作從Python 2轉換而來的程序的轉換工具,該程序支持使用比較函數。
比較函數是任何可調用的函數,它們接受兩個參數進行比較,小於返回一個負數,等於返回零,大於返回一個正數。 鍵函數是一個可調用的函數,它接受一個參數並返回另一個值用作排序鍵。
<code>from functools import cmp_to_key l = [ { 'name': 'Tom', 'age': 12 }, { 'name': 'Join', 'age': 52 }, { 'name': 'Jeke', 'age': 23 } ] def compare_func(a, b): if a.get('age') > b.get('age'): return 1 #必須返回正數,不能是True else: return -1 #必須返回負數,不能是False print(sorted(l, key=cmp_to_key(compare_func))) # 輸出: [{'name': 'Tom', 'age': 12}, {'name': 'Jeke', 'age': 23}, {'name': 'Join', 'age': 52}]/<code>
在python2中sorted的函數原型是:sorted(iterable, cmp=None, key=None, reverse=False),參數中包含一個cmp參數,來提供讓我們傳入一個自定義函數的參數,但是python3 中的sorted函數原型是:sorted(iterable, /, *, key=None, reverse=False),這裡出現了/,*兩個符號,上一篇我們介紹過,主要是後面沒有了cmp參數,自定義函數排序就很不方便。這時候functools.cmp_to_key就為我們提供了這樣一個自定義函數排序方式,將函數轉換為鍵功能-key
lru_cache
緩存裝飾器,根據參數緩存每次函數調用結果,對於相同參數的,無需重新函數計算,直接返回之前緩存的返回值
<code>def a(x): print(x) return x+1 print(a()) # 輸出: 3 4 print(a()) # 輸出: 3 4/<code>
不使用緩存記錄,每次都重新執行函數計算
<code>from functools import lru_cache @lru_cache() def a(x): print(x) return x+1 print(a(3)) # 輸出 3 4 print(a(3)) # 輸出 4 print(a(4)) # 輸出 4 5/<code>
使用緩存記錄後,第一次a(3)調用,計算了數據後會進行緩存,第二次a(3)調用,因為參數相同,所以直接返回緩存的數據,第三次a(4)調用,因為參數不同,需要重新計算
partial
偏函數,可以擴展函數功能,但是不等於裝飾器,通常應用的場景是當我們要頻繁調用某個函數時,其中某些參數是已知的固定值,通常我們可以調用這個函數多次,但這樣看上去似乎代碼有些冗餘,而偏函數的出現就是為了很少的解決這一個問題。
舉一個簡單的例子:
<code>def add(a, b, c, x=1, y=2, z=3): return sum([a, b, c, x, y, z]) print(add(1, 2, 3, x=1, y=2, z=3)) #輸出 12/<code>
如果我們頻繁調用此函數,並且固定傳入某些參數,比如b=20, x=100
<code>from functools import partial def add(a, b, c, x=1, y=2, z=3): print(a, b, c, x, y, z) return sum([a, b, c, x, y, z]) add_100 = partial(add, 20, x=100) print(add_100(1, 2, y=2, z=3)) # 輸出 20 1 2 100 2 3 128/<code>
在進行函數重新定義時,如果需要固定非關鍵字參數,那麼默認定義的是第一個非關鍵字參數;如果需要固定關鍵字參數,直接指定關鍵字即可。
實際上偏函數的使用更多是在回調函數時使用,舉例如下:
<code>register_func = [] def call_back(n): print('call_back: ', n) def call_back1(n, m): print('call_back1: ', n, m) # 註冊回調函數 register_func.append((call_back, 10)) register_func.append((call_back1, 100, 200)) # 執行回調函數 for item in register_func: func = item[0] args = item[1:] func(*args) # 輸出 call_back: 10 call_back1: 100 200/<code>
上面我們在註冊回調函數的時候,需要記錄函數名和各個參數,非常不方便,如果使用偏函數進行修飾
<code>from functools import partial register_func = [] def call_back(n): print('call_back: ', n) def call_back1(n, m): print('call_back1: ', n, m) call_back_partial = partial(call_back, 10) call_back_partial1 = partial(call_back1, 100, 200) # 註冊回調函數 register_func.append(call_back_partial) register_func.append(call_back_partial1) # 執行回調函數 for func in register_func: func() # 輸出 call_back: 10 call_back1: 100 200/<code>
對比上面的方式,偏函數定義的優勢在哪裡呢?
- 註冊回調函數時,我們是知道函數參數的,所以在此使用偏函數很簡單、很方便
- 使用偏函數後,註冊回調函數和調用回調函數那裡都使用完全固定的寫法,無論傳入的是固定參數、非固定參數或者關鍵字參數
- 相對於上面一點,只需要在註冊的時候使用偏函數重新生成一個回調函數
這在回調函數的使用中是非常頻繁、方便,而且爽就一個字
reduce
函數原型如下:
<code>def reduce(function, iterable, initializer=None): it = iter(iterable) if initializer is None: value = next(it) else: value = initializer for element in it: value = function(value, element) return value/<code>
可以看到實際執行是將迭代器iterable中每一個元素傳入function函數進行累計計算,並將最終值返回。一個簡單的使用示例:
<code>a=[1,3,5] b=reduce(lambda x,y:x+y,a) print(b) # 輸出 9/<code>
將a列表傳入匿名函數進行累加計算
singledispatch
python函數重載,直接舉例來說明
<code>def connect(address): if isinstance(address, str): ip, port = address.split(':') elif isinstance(address, tuple): ip, port = address else: print('地址格式不正確') # 傳入字符串 connect('123.45.32.18:8080') # 傳入元祖 connect(('123.45.32.18', 8080))/<code>
簡單來說就是address可能是字符串,也可能是元組,那麼我們就需要在函數內進行單獨處理,如果這種類型很多呢?那就需要if...elif...elif...elif..esle...,寫起來非常不美觀,而且函數的可讀性也會變差。
學過C++和Java的同學都知道函數重載,同樣的函數名,同樣的參數個數,不同的參數類型,實現多個函數,程序運行時將根據不同的參數類型自動調用對應的函數。python也提供了這樣的重載方式
<code>from functools import singledispatch @singledispatch def connect(address): print(f'傳入參數類型為:{type(address)}, 不是有效的類型') @connect.register def connect_str(address: str): ip, port = address.split(':') print(f'參數為字符串,IP是{ip}, 端口是{port}') @connect.register def connect_tuple(address: tuple): ip, port = address print(f'參數為元組,IP是{ip}, 端口是{port}') connect('123.45.32.18:8080') # 輸出 參數為字符串,IP是123.45.32.18, 端口是8080 connect(('123.45.32.18', '8080')) # 輸出 參數為元組,IP是123.45.32.18, 端口是8080/<code>
先使用singledispatch裝飾器修飾connect函數,然後使用connect.register裝飾器註冊不同參數類型的函數(函數名可以隨意,甚至不寫,使用_代替),在調用的時候就會默認按照參數類型調用對應的函數執行。
total_ordering
定義一個類,類中定義了一個或者多個比較排序方法,這個類裝飾器將會補充其餘的比較方法,減少了自己定義所有比較方法時的工作量;
被修飾的類必須至少定義 __lt__(), __le__(),__gt__(),__ge__()中的一個,同時,被修飾的類還應該提供 __eq__()方法。簡單來說就是隻需要重載部分運算符,裝飾器就會自動幫我們實現其他的方法。
<code>class Person: # 定義相等的比較函數 def __eq__(self, other): return ((self.lastname.lower(), self.firstname.lower()) == (other.lastname.lower(), other.firstname.lower())) # 定義小於的比較函數 def __lt__(self, other): return ((self.lastname.lower(), self.firstname.lower()) < (other.lastname.lower(), other.firstname.lower())) p1 = Person() p2 = Person() p1.lastname = "123" p1.firstname = "000" p2.lastname = "1231" p2.firstname = "000" print(p1 < p2) print(p1 <= p2) print(p1 == p2) print(p1 > p2) print(p1 >= p2) # 輸出 True Traceback (most recent call last): File "/Volumes/Code/Python工程代碼/Python基礎知識/特殊特性學習/test.py", line 31, in print(p1 <= p2) TypeError: '<=' not supported between instances of 'Person' and 'Person'/<code>
報錯在p1 <= p2這一行,提醒我們在Person對象之間不支持
<=符號,使用total_ordering裝飾器修飾以後。<code>from functools import total_ordering @total_ordering class Person: # 定義相等的比較函數 def __eq__(self, other): return ((self.lastname.lower(), self.firstname.lower()) == (other.lastname.lower(), other.firstname.lower())) # 定義小於的比較函數 def __lt__(self, other): return ((self.lastname.lower(), self.firstname.lower()) < (other.lastname.lower(), other.firstname.lower())) p1 = Person() p2 = Person() p1.lastname = "123" p1.firstname = "000" p2.lastname = "1231" p2.firstname = "000" print(p1 < p2) print(p1 <= p2) print(p1 == p2) print(p1 > p2) print(p1 >= p2) # 輸出 True True False False False/<code>
只在類上面增加了total_ordering裝飾器,就可以完美支持所有的比較運算符了
wraps
python中的裝飾器是“接受函數為參數,以函數為返回值”。但是裝飾器函數也會有一些負面影響。我們來看一下例子:
<code># 普通函數 def add(x, y): return x + y print(add.__name__) # 輸出 add # 裝飾器函數 def decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @decorator def add(x, y): return x + y print(add.__name__) # 輸出 wrapper/<code>
可以看到函數名發生了變化,變為裝飾器函數中的wrapper,除了__name__屬性外還有其他屬性,定義在WRAPPER_ASSIGNMENTS和WRAPPER_UPDATES變量中,包括__module__、__name__、 __qualname__、__doc__、__annotations__、__dict__。在很多情況下,我們需要對函數進行針對性處理,必須獲取函數的模塊屬性進行處理,這個時候,就必須消除這種負面影響。functools.wraps就為我們解決了這個問題。
<code>from functools import wraps def decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @decorator def add(x, y): return x + y print(add.__name__) # 輸出 add/<code>
即使使用了裝飾器修飾,我們仍然能獲取到原函數的屬性
update_wrapper
update_wrapper 的作用與 wraps 類似,不過功能更加強大,換句話說,wraps 其實是 update_wrapper 的特殊化,實際上 wraps(wrapped) 的函數源碼為:
<code>def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)/<code>
使用方式:
<code>from functools import update_wrapper def decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return update_wrapper(wrapper, func) @decorator def add(x, y): return x + y print(add.__name__) # 輸出 add/<code>
注意:wraps和update_wrapper是專為裝飾器函數所設計,而且強烈建議在定義裝飾器時進行修飾
(此處已添加圈子卡片,請到今日頭條客戶端查看)