python中的函數增強神器functools模塊

python中的函數增強神器functools模塊

functools是一個函數增強器,主要為高階函數使用,作用於或者返回其他函數的函數,通常任何可調用的對象都可視為“函數”。主要包括以下幾個函數:

python中的函數增強神器functools模塊

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

緩存裝飾器,根據參數緩存每次函數調用結果,對於相同參數的,無需重新函數計算,直接返回之前緩存的返回值

  • 如果maxsize設置為None,則禁用LRU功能,並且緩存可以無限制增長;當maxsize是2的冪時,LRU功能執行得最好;
  • 如果 typed設置為True, 則不同類型的函數參數將單獨緩存。例如,f(3)和f(3.0)將被視為具有不同結果的不同調用;
  • 緩存是有內存存儲空間限制的;
  • <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>

    注意:wrapsupdate_wrapper是專為裝飾器函數所設計,而且強烈建議在定義裝飾器時進行修飾

    (此處已添加圈子卡片,請到今日頭條客戶端查看)


    分享到:


    相關文章: