10.2. functools — 高次関数と呼び出し可能オブジェクトの操作

ソースコード: Lib/functools.py


functools モジュールは高次関数、つまり関数に影響を及ぼしたり他の関数を返したりする関数、のためのものです。一般に、どんな呼び出し可能オブジェクトでもこのモジュールの目的には関数として扱えます。

モジュール functools は以下の関数を定義します:

functools.cmp_to_key(func)

古いスタイルの比較関数を key function に変換します。key 関数を受け取るツール (sorted(), min(), max(), heapq.nlargest(), heapq.nsmallest(), itertools.groupby() など) と共に使用します。この関数は、主に比較関数を使っていた Python 2 からプログラムの移行のための変換ツールとして使われます。

比較関数は2つの引数を受け取り、それらを比較し、 “より小さい” 場合は負の数を、同値の場合には 0 を、 “より大きい” 場合には正の数を返す、あらゆる呼び出し可能オブジェクトです。key 関数は呼び出し可能オブジェクトで、1つの引数を受け取り、ソートキーとして使われる値を返します。

sorted(iterable, key=cmp_to_key(locale.strcoll))  # locale-aware sort order

ソートの例と簡単なチュートリアルは ソート HOW TO を参照して下さい。

バージョン 3.2 で追加.

@functools.lru_cache(maxsize=128, typed=False)

関数をメモ化用の呼び出し可能オブジェクトでラップし、最近の呼び出し最大 maxsize 回まで保存するするデコレータです。高価な関数や I/O に束縛されている関数を定期的に同じ引数で呼び出すときに、時間を節約できます。

結果のキャッシュには辞書が使われるので、関数の位置引数およびキーワード引数はハッシュ可能でなくてはなりません。

maxsize が None に設定されると、LRU 機能は無効化され、キャッシュは限界を超えて大きくなります。LRU 機能は maxsize が 2 の累乗であるときに最も効率がいいです。

typed が真に設定されると、関数の異なる型の引数が別々にキャッシュされます。例えば、f(3)f(3.0) は別の結果をもたらす別の呼び出しとして扱われます。

キャッシュの効率の測定や maxsize パラメタの調整を助けるため、このラッパは cache_info() 関数を装備しています。これは hits, misses, maxsize and currsizenamed tuple を表す named tuple を返します。マルチスレッド環境では、hits と misses は概算です。

このデコレータは、キャッシュの削除と無効化のための cache_clear() 関数も提供します。

元々の基底の関数には、 __wrapped__ 属性を通してアクセスできます。これはキャッシュを回避して、または関数を別のキャッシュでラップして、内観するのに便利です。

An LRU (least recently used) cache works best when the most recent calls are the best predictors of upcoming calls (for example, the most popular articles on a news server tend to change each day). The cache’s size limit assures that the cache does not grow without bound on long-running processes such as web servers.

静的 web コンテンツ の LRU キャッシュの例:

@lru_cache(maxsize=32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'http://www.python.org/dev/peps/pep-%04d/' % num
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'

>>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
...     pep = get_pep(n)
...     print(n, len(pep))

>>> get_pep.cache_info()
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)

Example of efficiently computing Fibonacci numbers using a cache to implement a dynamic programming technique:

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

バージョン 3.2 で追加.

バージョン 3.3 で変更: typed オプションが追加されました。

@functools.total_ordering

ひとつ以上の拡張順序比較メソッド (rich comparison ordering methods) を定義したクラスを受け取り、残りを実装するクラスデコレータです。このデコレータは全ての拡張順序比較演算をサポートするための労力を軽減します:

引数のクラスは、 __lt__(), __le__(), __gt__(), __ge__() の中からどれか1つと、 __eq__() メソッドを定義する必要があります。

例えば、以下の指定:

@total_ordering
class Student:
    def _is_valid_operand(self, other):
        return (hasattr(other, "lastname") and
                hasattr(other, "firstname"))
    def __eq__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

注釈

While this decorator makes it easy to create well behaved totally ordered types, it does come at the cost of slower execution and more complex stack traces for the derived comparison methods. If performance benchmarking indicates this is a bottleneck for a given application, implementing all six rich comparison methods instead is likely to provide an easy speed boost.

バージョン 3.2 で追加.

バージョン 3.4 で変更: Returning NotImplemented from the underlying comparison function for unrecognised types is now supported.

functools.partial(func, *args, **keywords)

新しい partial オブジェクトを返します。このオブジェクトは呼び出されると位置引数 args とキーワード引数 keywords 付きで呼び出された func のように振る舞います。呼び出しに際してさらなる引数が渡された場合、それらは args に付け加えられます。追加のキーワード引数が渡された場合には、それらで keywords を拡張または上書きします。大雑把にいうと、次のコードと等価です:

def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*(args + fargs), **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

関数 partial() は、関数の位置引数・キーワード引数の一部を「凍結」した部分適用として使われ、簡素化された引数形式をもった新たなオブジェクトを作り出します。例えば、 partial() を使って base 引数のデフォルトが 2 である int() 関数のように振る舞う呼び出し可能オブジェクトを作ることができます:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
class functools.partialmethod(func, *args, **keywords)

Return a new partialmethod descriptor which behaves like partial except that it is designed to be used as a method definition rather than being directly callable.

func must be a descriptor or a callable (objects which are both, like normal functions, are handled as descriptors).

When func is a descriptor (such as a normal Python function, classmethod(), staticmethod(), abstractmethod() or another instance of partialmethod), calls to __get__ are delegated to the underlying descriptor, and an appropriate partial object returned as the result.

When func is a non-descriptor callable, an appropriate bound method is created dynamically. This behaves like a normal Python function when used as a method: the self argument will be inserted as the first positional argument, even before the args and keywords supplied to the partialmethod constructor.

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

バージョン 3.4 で追加.

functools.reduce(function, iterable[, initializer])

2 引数の functioniterable の要素に対して左から右に累積的に適用し、イテラブルを単一の値に縮約します。例えば、reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])((((1+2)+3)+4)+5) を計算します。左引数 x は累計の値で、右引数 yiterable から取り出した更新値です。オプションの initializer が存在する場合、計算の際にイテラブルの先頭に置かれ、またイテラブルが空の場合のデフォルトになります。initializer が与えられておらず、iterable が単一の要素しか持っていない場合、最初の要素が返されます。

およそ次と等価です:

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
@functools.singledispatch(default)

Transforms a function into a single-dispatch generic function.

To define a generic function, decorate it with the @singledispatch decorator. Note that the dispatch happens on the type of the first argument, create your function accordingly:

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)

To add overloaded implementations to the function, use the register() attribute of the generic function. It is a decorator, taking a type parameter and decorating a function implementing the operation for that type:

>>> @fun.register(int)
... def _(arg, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> @fun.register(list)
... def _(arg, verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

To enable registering lambdas and pre-existing functions, the register() attribute can be used in a functional form:

>>> def nothing(arg, verbose=False):
...     print("Nothing.")
...
>>> fun.register(type(None), nothing)

The register() attribute returns the undecorated function which enables decorator stacking, pickling, as well as creating unit tests for each variant independently:

>>> @fun.register(float)
... @fun.register(Decimal)
... def fun_num(arg, verbose=False):
...     if verbose:
...         print("Half of your number:", end=" ")
...     print(arg / 2)
...
>>> fun_num is fun
False

When called, the generic function dispatches on the type of the first argument:

>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615

Where there is no registered implementation for a specific type, its method resolution order is used to find a more generic implementation. The original function decorated with @singledispatch is registered for the base object type, which means it is used if no better implementation is found.

To check which implementation will the generic function choose for a given type, use the dispatch() attribute:

>>> fun.dispatch(float)
<function fun_num at 0x1035a2840>
>>> fun.dispatch(dict)    # note: default implementation
<function fun at 0x103fe0000>

To access all registered implementations, use the read-only registry attribute:

>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
          <class 'decimal.Decimal'>, <class 'list'>,
          <class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>

バージョン 3.4 で追加.

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

wrapper 関数を wrapped 関数に見えるようにアップデートします。オプション引数はタプルで、元の関数のどの属性がラッパ関数の対応する属性に直接代入されるか、またラッパ関数のどの属性が元の関数の対応する属性でアップデートされる(updated)か、を指定します。これらの引数のデフォルト値はモジュールレベル定数 WRAPPER_ASSIGNMENTS (ラッパ関数の __name____module____annotations__ そしてドキュメンテーション文字列 __doc__ に代入します) と WRAPPER_UPDATES (ラッパ関数の __dict__、すなわちインスタンス辞書をアップデートします) です。

内観や別の目的 (例えば、 lru_cache() のようなキャッシュするデコレータの回避) のために元の関数にアクセスできるように、この関数はラップされている関数を参照するラッパに自動的に __wrapped__ 属性を追加します。

この関数は主に関数を包んでラッパを返すデコレータ (decorator) 関数の中で使われるよう意図されています。もしラッパ関数がアップデートされないとすると、返される関数のメタデータは元の関数の定義ではなくラッパ関数の定義を反映してしまい、これは通常あまり有益ではありません。

update_wrapper() は、関数以外の呼び出し可能オブジェクトにも使えます。 assigned または updated で指名され、ラップされるオブジェクトに存在しない属性は、すべて無視されます (すなわち、ラッパ関数にそれらの属性を設定しようとは試みられません)。しかし、updated で指名された属性がラッパ関数自身に存在しないなら AttributeError が送出されます。

バージョン 3.2 で追加: __wrapped__ 属性の自動的な追加。

バージョン 3.2 で追加: デフォルトで __annotations__ 属性がコピーされます。

バージョン 3.2 で変更: 存在しない属性によって AttributeError を発生しなくなりました。

バージョン 3.4 で変更: The __wrapped__ attribute now always refers to the wrapped function, even if that function defined a __wrapped__ attribute. (see issue 17482)

@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

これはラッパ関数を定義するときに update_wrapper() を関数デコレータとして呼び出す便宜関数です。これは partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated) と等価です。例えば:

>>> from functools import wraps
>>> def my_decorator(f):
...     @wraps(f)
...     def wrapper(*args, **kwds):
...         print('Calling decorated function')
...         return f(*args, **kwds)
...     return wrapper
...
>>> @my_decorator
... def example():
...     """Docstring"""
...     print('Called example function')
...
>>> example()
Calling decorated function
Called example function
>>> example.__name__
'example'
>>> example.__doc__
'Docstring'

このデコレータ・ファクトリーを使わなければ、上の例中の関数の名前は 'wrapper' となり、元々の example() のドキュメンテーション文字列は失われたところです。

10.2.1. partial オブジェクト

partial オブジェクトは、 partial() 関数によって作られる呼び出し可能オブジェクトです。オブジェクトには読み取り専用の属性が三つあります:

partial.func

呼び出し可能オブジェクトまたは関数です。 partial オブジェクトの呼び出しは新しい引数とキーワードと共に func に転送されます。

partial.args

最左の位置引数で、 partial オブジェクトの呼び出し時にその呼び出しの際の位置引数の前に追加されます。

partial.keywords

partial オブジェクトの呼び出し時に渡されるキーワード引数です。

partial オブジェクトは function オブジェクトのように呼び出し可能で、弱参照可能で、属性を持つことができます。重要な相違点もあります。例えば、 __name____doc__ 両属性は自動では作られません。また、クラス中で定義された partial オブジェクトはスタティックメソッドのように振る舞い、インスタンスの属性問い合わせの中で束縛メソッドに変換されません。