Python標準庫筆記(9) — functools模塊

functools 做用於函數的函數python

functools 模塊提供用於調整或擴展函數和其餘可調用對象的工具,而無需徹底重寫它們。算法

裝飾器

partial 類是 functools 模塊提供的主要工具, 它能夠用來「包裝」一個可調用的對象的默認參數。它產生的對象自己是可調用的,能夠看做是原生函數。它全部的參數都與原來的相同,而且可使用額外的位置參數或命名參數來調用。使用 partial 代替 lambda 來爲函數提供默認參數,同時保留那些未指定的參數。緩存

Partial 對象

下面列子是對 myfunc 方法的兩個 partial 對象,show_details() 用於輸出partial對象的 funcargskeywords 屬性:app

import functools


def 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)
    return


show_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 的值,致使異常。ide

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 functools


def 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: <function myfunc at 0x000002315C123E18>
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

raw wrapper:
  object: functools.partial(<function myfunc at 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(<function myfunc at 0x000002315C123E18>, b=4)
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

其餘調用對象

partial適用於全部可調用可對象,並非僅可用於獨立函數。性能

import functools


class 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__))
    return


o = 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__.MyClass object at 0x000002DE7C2CD2E8>
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x000002DE7C2CD2E8>, 'e goes here', 6)

instance wrapper:
  object: functools.partial(<__main__.MyClass object at 0x000002DE7C2CD2E8>, e='default for e', f=8)
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x000002DE7C2CD2E8>, 'default for e', 8)

方法和函數

partial() 返回一個能夠直接調用的對象, partialmethod() 返回一個可調用的爲某個對象準備的未綁定的方法。再下面例子中,同一個獨立函數被兩次添加到類 MyClass 屬性。使用 partialmethod() 生成 method1(), partial() 生成 method2():ui

import functools


def 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 0x00000214B4459B70>, 1, 2)
  self.attr = instance attribute

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

在裝飾器中使用

使用裝飾器時保持函數的屬性信息有時很是有用。可是使用裝飾器時不免會損失一些本來的功能信息。因此functools提供了 wraps() 裝飾器能夠經過 update_wrapper() 將原函數對象的指定屬性複製給包裝函數對象。

from functools import wraps

def logged1(func):
    def with_login(*args, **kwargs):
        print(func.__name__ + "was called")
        return func(*args, **kwargs)

    return with_login

@logged1
def f1(x):
    """ function doc"""
  return x + x * 1

def logged2(func):
    @wraps(func)
    def with_login(*args, **kwargs):
        print(func.__name__ + "was called")
        return func(*args, **kwargs)

    return with_login

@logged2
def f2(x):
    """ function doc """
  return x + x * 1

print("不使用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等於或大於被比較項返回-一、0或1。Python2.1開始引入了 富比較 方法API(__lt__(), __le()__, __eq__(), __ne__(), __gt__()__ge__()),用於執行比較操做返回一個布爾值。Python3中 __cmp__() 放棄支持這些新方法,由 functools 提供工具,以便於編寫符合Python3中新的比較需求的類。

富比較

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

import functools
import inspect
from pprint import pprint


@functools.total_ordering
class 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: True

a <= b:
  testing __gt__(1, 2)
  result of a <= b: True

a == b:
  testing __eq__(1, 2)
  result of a == b: False

a >= 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 ^ 2


MAX = 2

print('初次調用:')
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,maxsizecurrsize 。當第二次調用時命中緩存的調用將直接返回緩存內容,cache_clear() 用於清空當前緩存。

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

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

清空緩存後:
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

再次調用:
called demo with 0
called demo with 1
CacheInfo(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.singledispatch
def 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 functools


class A:
    pass


class B(A):
    pass


class C(A):
    pass


class D(B):
    pass


class E(C, D):
    pass


@functools.singledispatch
def 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沒有與任何已註冊的泛型函數匹配,因此根據其類的繼承順序進行選擇。

Python標準庫筆記(9) — functools模塊

相關文章
相關標籤/搜索