歡迎光臨
每天分享高質量文章

Python神器列傳:函式神器functools模組全解析

作者:j_hao104

來源:見文末

functools 模組提供用於調整或擴充套件函式和其他可呼叫物件的工具,而無需完全重寫它們。

裝飾器

partial 類是 functools 模組提供的主要工具, 它可以用來“包裝”一個可呼叫的物件的預設引數。它產生的物件本身是可呼叫的,可以看作是原生函式。它所有的引數都與原來的相同,並且可以使用額外的位置引數或命名引數來呼叫。使用 partial 代替 lambda 來為函式提供預設引數,同時保留那些未指定的引數。

Partial 物件

下麵列子是對 myfunc 方法的兩個 partial 物件,show_details() 用於輸出partial物件的 func 、 args 和 keywords 屬性:

import functoolsdef myfunc(a, b=2):
    """Docstring for myfunc()."""
    print('  傳入引數:', (a, b))def show_details(name, f, is_partial=False):
    """Show details of a callable object."""
    print('{}:'.format(name))
    print('  object:', f)    if not is_partial:
        print('  __name__:', f.__name__)    if is_partial:
        print('  func:', f.func)
        print('  args:', f.args)
        print('  keywords:', f.keywords)    returnshow_details('myfunc', myfunc)
myfunc('a', 3)
print()# # 給'b'重新設定一個不同的預設引數# # 呼叫時仍需提供引數'a'p1 = functools.partial(myfunc, b=4)
show_details('partial 修改關鍵字引數', p1, True)
p1('傳入 a')
p1('重寫 b', b=5)
print()## # 給 'a' 和 'b' 都設定預設引數.p2 = functools.partial(myfunc, '預設 a', b=99)
show_details('partial 設定預設引數', p2, True)
p2()
p2(b='重寫 b')
print()

print('引數缺失時:')
p1()

示例中最後呼叫第一個 partial 物件而沒有傳遞 a 的值,導致異常。

myfunc:
  object: <function myfunc at 0x00000180005077B8>  __name__: myfunc
  傳入引數: ('a', 3)partial 修改關鍵字引數:
  object: functools.partial(<function myfunc at 0x00000180005077B8>, b=4)
  func: <function myfunc at 0x00000180005077B8>  args: ()
  keywords: {'b': 4}
  傳入引數: ('傳入 a', 4)
  傳入引數: ('重寫 b', 5)

partial 設定預設引數:
  object: functools.partial(<function myfunc at 0x00000180005077B8>, '預設 a', b=99)
  func: <function myfunc at 0x00000180005077B8>  args: ('預設 a',)
  keywords: {'b': 99}
  傳入引數: ('預設 a', 99)
  傳入引數: ('預設 a', '重寫 b')

引數缺失時:
Traceback (most recent call last):
  File "functools_partial.py", line 51, in <module>
    p1()
TypeError: myfunc() missing 1 required positional argument: 'a'

獲取函式屬性

預設情況下, partial 物件沒有 __name__ 和 __doc__ 屬性。 這樣不利於被裝飾的函式進行除錯。可以使用 update_wrapper() 從原函式複製或新增屬性到 partial 物件。

import functoolsdef myfunc(a, b=2):
    """Docstring for myfunc()."""
    print('  傳入引數:', (a, b))def show_details(name, f):
    """Show details of a callable object."""
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')    try:
        print(f.__name__)    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    print()


show_details('myfunc', myfunc)

p1 = functools.partial(myfunc, b=4)
show_details('raw wrapper', p1)

print('Updating wrapper:')
print('  assign:', functools.WRAPPER_ASSIGNMENTS)
print('  update:', functools.WRAPPER_UPDATES)
print()

functools.update_wrapper(p1, myfunc)
show_details('updated wrapper', p1)

新增到裝飾器的屬性在 WRAPPER_ASSIGNMENTS 中定義,而 WRAPPER_UPDATES 列出要修改的值。

myfunc:  object: 0x000002315C123E18

>
 __name__: myfunc
 __doc__ ‘Docstring for myfunc().’raw wrapper:  object: functools.partial(0x000002315C123E18>, b=4)
 __name__: (no __name__)
 __doc__ ‘partial(func, *args, **keywords) – new function with partial application\n    of the given arguments and keywords.\n’Updating wrapper:
 assign: (‘__module__’, ‘__name__’, ‘__qualname__’, ‘__doc__’, ‘__annotations__’)
 update: (‘__dict__’,)

updated wrapper:  object: functools.partial(0x000002315C123E18>, b=4)
 __name__: myfunc
 __doc__ ‘Docstring for myfunc().’

其他呼叫物件

partial適用於所有可呼叫可物件,並不是僅可用於獨立函式。

import functoolsclass MyClass:
    """Demonstration class for functools"""

    def __call__(self, e, f=6):
        "Docstring for MyClass.__call__"
        print('  called object with:', (self, e, f))def show_details(name, f):
    """"Show details of a callable object."""
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')    try:
        print(f.__name__)    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))    returno = MyClass()

show_details('instance', o)
o('e goes here')
print()

p = functools.partial(o, e='default for e', f=8)
functools.update_wrapper(p, o)
show_details('instance wrapper', p)
p()

上面例子使用 MyClass 類的實體的 __call__() 方法建立了partial物件。照樣正常工作:

instance:  object: <__main__. class="hljs-type" style="box-sizing: inherit;-webkit-tap-highlight-color: transparent;color: rgb(239, 239, 143);">MyClass object at 0x000002DE7C2CD2E8>
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__. class="hljs-type" style="box-sizing: inherit;-webkit-tap-highlight-color: transparent;color: rgb(239, 239, 143);">MyClass object at 0x000002DE7C2CD2E8>, 'e goes here', 6)instance wrapper:  object: functools.partial(<__main__. class="hljs-type" style="box-sizing: inherit;-webkit-tap-highlight-color: transparent;color: rgb(239, 239, 143);">MyClass object at 0x000002DE7C2CD2E8>, e='default for e', f=8)
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__. class="hljs-type" style="box-sizing: inherit;-webkit-tap-highlight-color: transparent;color: rgb(239, 239, 143);">MyClass object at 0x000002DE7C2CD2E8>, 'default for e', 8)

方法和函式

partial() 傳回一個可以直接呼叫的物件, partialmethod() 傳回一個可呼叫的為某個物件準備的未系結的方法。再下麵例子中,同一個獨立函式被兩次新增到類 MyClass 屬性。使用 partialmethod() 生成 method1()partial() 生成 method2():

import functoolsdef standalone(self, a=1, b=2):
    """獨立函式"""
    print('  called standalone with:', (self, a, b))    if self is not None:
        print('  self.attr =', self.attr)class MyClass:
    """"functools 示例類"""

    def __init__(self):
        self.attr = 'instance attribute'

    method1 = functools.partialmethod(standalone)
    method2 = functools.partial(standalone)


o = MyClass()

print('standalone')
standalone(None)
print()

print('method1 as partialmethod')
o.method1()
print()

print('method2 as partial')try:
    o.method2()except TypeError as err:
    print('ERROR: {}'.format(err))

method1() 可以被 MyClass 實體呼叫,和普通類方法一樣,實體作為第一個引數傳入。method2() 沒有被成功系結為類方法。因此其 self 引數必須顯式傳入,所以此例丟擲 TypeError 異常:

standalone
  called standalone with: (None, 1, 2)

method1 as partialmethod
  called standalone with: (<__main__.myclass object="" at="" class="hljs-number" style="box-sizing: inherit;-webkit-tap-highlight-color: transparent;color: rgb(140, 208, 211);">0x00000214B4459B70>, 1, 2)
  self.attr = instance attribute

method2 as partialERROR: standalone() missing 1 required positional argument: 'self'

在裝飾器中使用

使用裝飾器時保持函式的屬性資訊有時非常有用。但是使用裝飾器時難免會損失一些原本的功能資訊。所以functools提供了 wraps() 裝飾器可以透過 update_wrapper() 將原函式物件的指定屬性複製給包裝函式物件。

from functools import wrapsdef logged1(func):
    def with_login(*args, **kwargs):
        print(func.__name__ + "was called")        return func(*args, **kwargs)    return with_login@logged1def f1(x):
    """ function doc"""
  return x + x * 1def logged2(func):    @wraps(func)
    def with_login(*args, **kwargs):
        print(func.__name__ + "was called")        return func(*args, **kwargs)    return with_login@logged2def f2(x):
    """ function doc """
  return x + x * 1print("不使用functools.wraps時:")
print("__name__:  " + f1.__name__)
print("__doc__:  ", end=" ")
print(f1.__doc__)
print()

print("使用functools.wraps時:")
print("__name__:  " + f2.__name__)
print("__doc__:  ", end=" ")
print(f2.__doc__)
不使用functools.wraps時:__name__:  with_login__doc__:   None使用functools.wraps時:__name__:  f2__doc__:    function doc 

比較

在Python2之前,類中可以定義 __cmp__() 方法,該方法根據物件是否小於、d等於或大於被比較項傳回-1、0或1。Python2.1開始引入了 富比較 方法API(__lt__()__le()____eq__()__ne__()__gt__() 和 __ge__()),用於執行比較操作傳回一個布林值。Python3中 __cmp__() 放棄支援這些新方法,由 functools 提供工具,以便於編寫符合Python3中新的比較需求的類。

富比較

富比較API旨在允許具有複雜比較的類以最有效的方式實現每種計算。但是,對於比較相對簡單的類,手動建立每種富比較方法沒有意義。total_ordering() 類裝飾器可以使被裝飾的類只需要定義 __lt__(),__le__().__gt__()__ge__() 中的其中一個和 __eq__(), 剩下的由該裝飾器自動提供。這簡化了定義所有富比較操作的工作量。

import functoolsimport inspectfrom pprint import pprint@functools.total_orderingclass MyObject:

    def __init__(self, val):
        self.val = val    def __eq__(self, other):
        print('  testing __eq__({}, {})'.format(
            self.val, other.val))        return self.val == other.val    def __gt__(self, other):
        print('  testing __gt__({}, {})'.format(
            self.val, other.val))        return self.val > other.val


print("MyObject's Methods:\n")
pprint(inspect.getmembers(MyObject, inspect.isfunction))

a = MyObject(1)
b = MyObject(2)

print('\nComparisons:')for expr in ['a < b', 'a <= b', 'a == b', 'a >= b', 'a > b']:
    print('\n{:<6}:'.format(expr))
    result = eval(expr)
    print('  result of {}: {}'.format(expr, result))
MyObject's Methods:

[('__eq__', <function MyObject.__eq__ at 0x0000021DE4DB4048>),
 ('__ge__', <function _ge_from_gt at 0x0000021DDDE5D268>),
 ('__gt__', <function MyObject.__gt__ at 0x0000021DE4DB40D0>),
 ('__init__', <function MyObject.__init__ at 0x0000021DDDE877B8>),
 ('__le__', <function _le_from_gt at 0x0000021DDDE5D2F0>),
 ('__lt__', <function _lt_from_gt at 0x0000021DDDE5D1E0>)]

Comparisons:

a < b :
  testing __gt__(1, 2)  testing __eq__(1, 2)  result of a < b: Truea <= b:
  testing __gt__(1, 2)  result of a <= b: Truea == b:
  testing __eq__(1, 2)  result of a == b: Falsea >= b:
  testing __gt__(1, 2)
  testing __eq__(1, 2)
  result of a >= b: False

a > b :
  testing __gt__(1, 2)
  result of a > b: False

雖然該裝飾器能很容易的建立完全有序型別,但衍生出的比較函式執行的可能會更慢,以及產生更複雜的堆疊跟蹤。如果效能基準測試表明這是程式的瓶頸,則實現所有六個富比較函式可能會提高速度。

排序規則

在Python3中已經廢棄了舊時的比較(cmp)函式,因此例如 sorted(),min(),max()等方法不在支援 cmp引數, 但仍然支援key函式。functools提供了 cmp_to_key() 用於將cmp函式轉換成key函式。

例如給定一個正整數串列,輸出用這些正整數能夠拼接成的最大整數。如果是Python2的程式可以是這樣:

L = [97, 13, 4, 246]def my_cmp(a, b):
    """ 將比較的兩個數字拼接成整數, 比較數值大小"""
    return int(str(b) + str(a)) - int(str(a) + str(b))

L.sort(cmp=my_cmp)
print(''.join(map(str, L)))# 輸出 97424613

但Python3的 sort 函式已廢棄 cmp 引數,可以使用 cmp_to_key 將cmp函式轉換成key函式:

from functools import cmp_to_key

L = [97, 13, 4, 246]def my_cmp(a, b):
    """ 將比較的兩個數字拼接成整數, 比較數值大小"""
    return int(str(b) + str(a)) - int(str(a) + str(b))


L.sort(key=cmp_to_key(my_cmp))
print(''.join(map(str, L)))# 輸出 97424613

cmp 函式接收兩個引數,比較它們,如果小於傳回負數,相等傳回0,大於傳回正數。 key 函式接收一個引數,傳回用於排序的鍵。

快取

lru_cache() 裝飾器是 快取淘汰演演算法(最近最少使用)的一種實現。其使用函式的引數作為key結果作為value快取在hash結構中(因此函式的引數必須是hashable),如果後續使用相同引數再次呼叫將從hash從傳回結果。同時裝飾器還添加了檢查快取轉態方法(cache_info())和清空快取方法(cache_clear())給函式。

import functools@functools.lru_cache()def demo(a):
    print('called demo with {}'.format(a))    return a ^ 2MAX = 2print('初次呼叫:')for i in range(MAX):
    demo(i)
print(demo.cache_info())

print('\n第二次呼叫:')for i in range(MAX + 1):
    demo(i)
print(demo.cache_info())

print('\n清空快取後:')
demo.cache_clear()
print(demo.cache_info())

print('\n再次呼叫:')for i in range(MAX):
    demo(i)
print(demo.cache_info())

程式碼中多次呼叫 demo() 方法。首次呼叫後結果存在快取中。cache_info() 傳回一個命名元組,包括 hits,misses,maxsize 和 currsize 。當第二次呼叫時命中快取的呼叫將直接傳回快取內容,cache_clear() 用於清空當前快取。

初次呼叫:
called demo with 0called demo with 1CacheInfo(hits=0, misses=2, maxsize=128, currsize=2)

第二次呼叫:
called demo with 2CacheInfo(hits=2, misses=3, maxsize=128, currsize=3)

清空快取後:
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

再次呼叫:
called demo with 0called demo with 1CacheInfo(hits=0, misses=2, maxsize=128, currsize=2)

為了防止快取在長時間執行的流程中無限制地增長,特別設定了 maxsize 引數, 預設是128,設定為None時,則禁用LRU功能,快取可以無限增長。同時還提供了 typed 引數,用於設定是否區別引數型別,預設為Fals。如果設定為True,那麼類似如 demo(1) 和 demo(1.0) 將被視為不同的值不同的呼叫。

Reduce方法

Python3中取消了全域性名稱空間中的 reduce() 函式,將 reduced() 放到了 functools 模組中,要使用 reduce() 的話,要先從 functools 中載入。

from functools import reduce

print(reduce(lambda a, b: a + b, range(11)))# 計算1加到10 結果 55

函式多載

在動態型別的語言(如Python)中,如果需要根據引數的型別執行不同的操作,簡單直接的方法就是檢查引數的型別。但在行為差異明顯的情況下需要分離成單獨的函式。 functools 提供 singledispatch() 裝飾器註冊一組通用函式基於函式的第一個引數的型別自動切換,類似於強型別語言中的函式多載。

import functools@functools.singledispatchdef myfunc(arg):
    print('default myfunc({!r})'.format(arg))@myfunc.register(int)def myfunc_int(arg):
    print('myfunc_int({})'.format(arg))@myfunc.register(list)def myfunc_list(arg):
    print('myfunc_list({})'.format(' '.join(arg)))


myfunc('string argument')
myfunc(1)
myfunc(2.3)
myfunc(['a', 'b', 'c'])

被 singledispatch() 裝飾的函式是預設實現, 使用其 register() 屬性裝飾接收其他型別引數的函式。呼叫時會根據 register() 中註冊的型別自動選擇實現函式。沒有則使用預設實現。

default myfunc('string argument')
myfunc_int(1)
default myfunc(2.3)
myfunc_list(a b c)

另外再有繼承的情況下,當型別沒有精確匹配時,將根據繼承順序,選擇最接近的型別。

import functoolsclass A:
    passclass B(A):
    passclass C(A):
    passclass D(B):
    passclass E(C, D):
    pass@functools.singledispatchdef myfunc(arg):
    print('default myfunc({})'.format(arg.__class__.__name__))@myfunc.register(A)def myfunc_A(arg):
    print('myfunc_A({})'.format(arg.__class__.__name__))@myfunc.register(B)def myfunc_B(arg):
    print('myfunc_B({})'.format(arg.__class__.__name__))@myfunc.register(C)def myfunc_C(arg):
    print('myfunc_C({})'.format(arg.__class__.__name__))


myfunc(A())
myfunc(B())
myfunc(C())
myfunc(D())
myfunc(E())
myfunc_A(A)
myfunc_B(B)
myfunc_C(C)
myfunc_B(D)
myfunc_C(E)

在上面程式碼中,類D和E沒有與任何已註冊的泛型函式匹配,所以根據其類的繼承順序進行選擇。

作者:j_hao104

來源:https://my.oschina.net/jhao104/blog/1810699

《Python人工智慧和全棧開發》2018年07月23日即將在北京開課,120天衝擊Python年薪30萬,改變速約~~~~

*宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。

– END –


更多Python好文請點選【閱讀原文】哦

↓↓↓

贊(0)

分享創造快樂