每週一個 Python 模組 | functools

yongxinz發表於2018-11-12

專欄地址:每週一個 Python 模組

functools 是 Python 中很簡單但也很重要的模組,主要是一些 Python 高階函式相關的函式。 該模組的內容並不多,看 官方文件 也就知道了。

說到高階函式,這是函數語言程式設計正規化中很重要的一個概念,簡單地說, 就是一個可以接受函式作為引數或者以函式作為返回值的函式,因為 Python 中函式是一類物件, 因此很容易支援這樣的函式式特性。

functools 模組中函式只有 cmp_to_keypartialreducetotal_orderingupdate_wrapperwrapslru_cache 這幾個:

被髮配邊疆的 reduce

這個 functools.reduce 就是 Python 2 內建庫中的 reduce,它之所以出現在這裡就是因為 Guido 的獨裁,他並不喜歡函數語言程式設計中的“map-reduce”概念,因此打算將 mapreduce 兩個函式移出內建函式庫,最後在社群的強烈反對中將 map 函式保留在了內建庫中, 但是 Python 3 內建的 map 函式返回的是一個迭代器物件,而 Python 2 中會 eagerly 生成一個 list,使用時要多加註意。

該函式的作用是將一個序列歸納為一個輸出,其原型如下:

reduce(function, sequence, startValue)
複製程式碼

使用示例:

>>> def foo(x, y):
...     return x + y
...
>>> l = range(1, 10)
>>> reduce(foo, l)
45
>>> reduce(foo, l, 10)
55
複製程式碼

偏函式 partialpartialmethod

用於建立一個偏函式,它用一些預設引數包裝一個可呼叫物件,返回結果是可呼叫物件,並且可以像原始物件一樣對待,這樣可以簡化函式呼叫。

一個簡單的使用示例:

from functools import partial

def add(x, y):
    return x + y

add_y = partial(add, 3)  # add_y 是一個新的函式
add_y(4) # 7
複製程式碼

一個很實用的例子:

def json_serial_fallback(obj):
    """
    JSON serializer for objects not serializable by default json code
    """
    if isinstance(obj, (datetime.datetime, datetime.date)):
        return str(obj)
    if isinstance(obj, bytes):
        return obj.decode("utf-8")
    raise TypeError ("%s is not JSON serializable" % obj)

json_dumps = partial(json.dumps, default=json_serial_fallback)
複製程式碼

可以在 json_serial_fallback 函式中新增型別判斷來指定如何 json 序列化一個 Python 物件

partialmethod 是 Python 3.4 中新引入的裝飾器,作用基本類似於 partial, 不過僅作用於方法。舉個例子就很容易明白:

class Cell(object):
    def __init__(self):
        self._alive = False
    @property
    def alive(self):
        return self._alive
    def set_state(self, state):
        self._alive = bool(state)
        
    set_alive = partialmethod(set_state, True)
    set_dead = partialmethod(set_state, False)

c = Cell()
c.alive         # False
c.set_alive()
c.alive         # True
複製程式碼

在 Python 2 中使用 partialmethod 可以這樣定義:

# Code from https://gist.github.com/carymrobbins/8940382
from functools import partial

class partialmethod(partial):
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return partial(self.func, instance,
                       *(self.args or ()), **(self.keywords or {}))
複製程式碼

裝飾器相關

說到“接受函式為引數,以函式為返回值”,在 Python 中最常用的當屬裝飾器了。 functools 庫中裝飾器相關的函式是 update_wrapperwraps,還搭配 WRAPPER_ASSIGNMENTSWRAPPER_UPDATES 兩個常量使用,作用就是消除 Python 裝飾器的一些負面作用。

wraps

例:

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@decorator
def add(x, y):
    return x + y

add     # <function __main__.wrapper>
複製程式碼

可以看到被裝飾的函式的名稱,也就是函式的 __name__ 屬性變成了 wrapper, 這就是裝飾器帶來的副作用,實際上add 函式整個變成了 decorator(add),而 wraps 裝飾器能消除這些副作用:

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@decorator
def add(x, y):
    return x + y

add     # <function __main__.add>
複製程式碼

更正的屬性定義在 WRAPPER_ASSIGNMENTS 中:

>>> functools.WRAPPER_ASSIGNMENTS
('__module__', '__name__', '__doc__')
>>> functools.WRAPPER_UPDATES
('__dict__',)
複製程式碼

update_wrapper

update_wrapper 的作用與 wraps 類似,不過功能更加強大,換句話說,wraps 其實是 update_wrapper 的特殊化,實際上 wraps(wrapped) 相當於 partial(update_wrapper, wrapped=wrapped, **kwargs)

因此,上面的程式碼可以用 update_wrapper 重寫如下:

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return update_wrapper(wrapper, func)
複製程式碼

用於比較的 cmp_to_keytotal_ordering

cmp_to_key

list.sort 和 內建函式 sorted 中都有一個 key 引數,這個引數用來指定取元素的什麼值進行比較,例如按字串元素的長度進行比較:

>>> x = ['hello','abc','iplaypython.com']
>>> x.sort(key=len)
>>> x
['abc', 'hello', 'iplaypython.com']
複製程式碼

也就是說排序時會先對每個元素呼叫 key 所指定的函式,然後再排序。同時,sortedlist.sort 還提供了 cmp 引數來指定如何比較兩個元素,但是在 Python 3 中該引數被去掉了。

cmp_to_key 是 Python 2.7 中新增的函式,用於將比較函式轉換為 key 函式, 這樣就可以應用在接受 key 函式為引數的函式中。比如 sorted()min()max()heapq.nlargest()itertools.groupby() 等。

sorted(range(5), key=cmp_to_key(lambda x, y: y-x))      # [4, 3, 2, 1, 0]
複製程式碼

total_ordering

total_ordering 同樣是 Python 2.7 中新增函式,用於簡化比較函式的寫法。如果你已經定義了__eq__ 方法,以及 __lt____le____gt__ 或者 __ge__() 其中之一, 即可自動生成其它比較方法。官方示例:

@total_ordering
class Student:
    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()))

dir(Student)    # ['__doc__', '__eq__', '__ge__', '__gt__', '__le__', '__lt__', '__module__']
複製程式碼

再看一個示例:

from functools import total_ordering

@total_ordering
class Student:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname

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

print dir(Student)

stu = Student("Huoty", "Kong")
stu2 = Student("Huoty", "Kong")
stu3 = Student("Qing", "Lu")

print stu == stu2
print stu > stu3
複製程式碼

輸出結果:

['__doc__', '__eq__', '__ge__', '__gt__', '__init__', '__le__', '__lt__', '__module__']
True
False
複製程式碼

用於快取的lru_cache

這個裝飾器是在 Python3 中新加的,在 Python2 中如果想要使用可以安裝第三方庫 functools32。該裝飾器用於快取函式的呼叫結果,對於需要多次呼叫的函式,而且每次呼叫引數都相同,則可以用該裝飾器快取呼叫結果,從而加快程式執行。示例:

from functools import lru_cache

@lru_cache(None)
def add(x, y):
    print("calculating: %s + %s" % (x, y))
    return x + y

print(add(1, 2))
print(add(1, 2))  # 直接返回快取資訊
print(add(2, 3))
複製程式碼

輸出結果:

calculating: 1 + 2
3
3
calculating: 2 + 3
5
複製程式碼

由於該裝飾器會將不同的呼叫結果快取在記憶體中,因此需要注意記憶體佔用問題,避免佔用過多記憶體,從而影響系統效能。



相關文件:

blog.windrunner.me/python/func…

kuanghy.github.io/2016/10/26/…

pymotw.com/3/

相關文章