functools – 関数を巧みに操作するためのツール

目的:他の関数に影響する関数
利用できるバージョン:2.5 以上

functools モジュールは、関数やその他の呼び出し可能オブジェクトを完全に書き換えることなく新たな目的のために適応させたり、拡張したりするために操作するツールを提供します。

デコレータ

functools モジュールが提供する主なツールは、デフォルト引数で呼び出し可能オブジェクトを “ラップ” するために使用される partial クラスです。その結果として返されるオブジェクトはそれ自身が呼び出し可能でオリジナルの関数を経由するように扱われます。オリジナルの呼び出し可能オブジェクトと同じ全引数を受け取り、さらに追加の位置引数やキーワード引数を受け取って実行することができます。

partial

このサンプルは myfunc() 関数のための2つのシンプルな partial オブジェクトの説明をします。 show_details() は渡された関数 func 、引数 args と partial オブジェクトの keywords 属性を表示することに注目してください。

import functools

def myfunc(a, b=2):
    """Docstring for myfunc()."""
    print '\tcalled myfunc with:', (a, b)
    return

def show_details(name, f, is_partial=False):
    """Show details of a callable object."""
    print '%s:' % name
    print '\tobject:', f
    if not is_partial:
        print '\t__name__:', f.__name__
    print '\t__doc__', repr(f.__doc__)
    if is_partial:
        print '\tfunc:', f.func
        print '\targs:', f.args
        print '\tkeywords:', f.keywords
    return

show_details('myfunc', myfunc)
myfunc('a', 3)
print

p1 = functools.partial(myfunc, b=4)
show_details('partial with named default', p1, True)
p1('default a')
p1('override b', b=5)
print

p2 = functools.partial(myfunc, 'default a', b=99)
show_details('partial with defaults', p2, True)
p2()
p2(b='override b')
print

print 'Insufficient arguments:'
p1()

このサンプルの最後で、最初に作成した partial オブジェクトの仮引数 a に値を渡さずに実行するので例外が発生します。

$ python functools_partial.py
myfunc:
        object: <function myfunc at 0x100467c08>
        __name__: myfunc
        __doc__ 'Docstring for myfunc().'
        called myfunc with: ('a', 3)
partial with named default:
        object: <functools.partial object at 0x100456e10>
        __doc__ 'partial(func, *args, **keywords) - new function with partial
 application\n    of the given arguments and keywords.\n'
        func: <function myfunc at 0x100467c08>
        args: ()
        keywords: {'b': 4}
        called myfunc with: ('default a', 4)
        called myfunc with: ('override b', 5)
partial with defaults:
        object: <functools.partial object at 0x100456e68>
        __doc__ 'partial(func, *args, **keywords) - new function with partial
 application\n    of the given arguments and keywords.\n'
        func: <function myfunc at 0x100467c08>
        args: ('default a',)
        keywords: {'b': 99}
        called myfunc with: ('default a', 99)
        called myfunc with: ('default a', 'override b')
Insufficient arguments:
Traceback (most recent call last):
  File "functools_partial.py", line 49, in <module>
    p1()
TypeError: myfunc() takes at least 1 argument (1 given)

update_wrapper

partial オブジェクトはデフォルトで __name____doc__ 属性を持っていません。デコレートされた関数のそういった属性をなくしてしまうことはデバッグをより難しくしてしまいます。 update_wrapper() を使用することでオリジナルの関数から partial オブジェクトに渡された属性をコピー又は追加することができます。

import functools

def myfunc(a, b=2):
    """Docstring for myfunc()."""
    print '\tcalled myfunc with:', (a, b)
    return

def show_details(name, f):
    """Show details of a callable object."""
    print '%s:' % name
    print '\tobject:', f
    print '\t__name__:', 
    try:
        print f.__name__
    except AttributeError:
        print '(no __name__)'
    print '\t__doc__', repr(f.__doc__)
    print
    return

show_details('myfunc', myfunc)

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

print 'Updating wrapper:'
print '\tassign:', functools.WRAPPER_ASSIGNMENTS
print '\tupdate:', functools.WRAPPER_UPDATES
print

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

そのラッパへ追加される属性は functools.WRAPPER_ASSIGNMENTS で定義されます。一方 functools.WRAPPER_UPDATES は変更される値を表示します。

$ python functools_update_wrapper.py
myfunc:
        object: <function myfunc at 0x100469c80>
        __name__: myfunc
        __doc__ 'Docstring for myfunc().'
raw wrapper:
        object: <functools.partial object at 0x100456e68>
        __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__', '__doc__')
        update: ('__dict__',)
updated wrapper:
        object: <functools.partial object at 0x100456e68>
        __name__: myfunc
        __doc__ 'Docstring for myfunc().'

その他の呼び出し可能オブジェクト

partial はメソッドやインスタンスも含め、どのような呼び出し可能オブジェクトでも動作します。

import functools

class MyClass(object):
    """Demonstration class for functools"""
    
    def meth1(self, a, b=2):
        """Docstring for meth1()."""
        print '\tcalled meth1 with:', (self, a, b)
        return
    
    def meth2(self, c, d=5):
        """Docstring for meth2"""
        print '\tcalled meth2 with:', (self, c, d)
        return
    wrapped_meth2 = functools.partial(meth2, 'wrapped c')
    functools.update_wrapper(wrapped_meth2, meth2)
    
    def __call__(self, e, f=6):
        """Docstring for MyClass.__call__"""
        print '\tcalled object with:', (self, e, f)
        return

def show_details(name, f):
    """Show details of a callable object."""
    print '%s:' % name
    print '\tobject:', f
    print '\t__name__:', 
    try:
        print f.__name__
    except AttributeError:
        print '(no __name__)'
    print '\t__doc__', repr(f.__doc__)
    return
    
o = MyClass()

show_details('meth1 straight', o.meth1)
o.meth1('no default for a', b=3)
print

p1 = functools.partial(o.meth1, b=4)
functools.update_wrapper(p1, o.meth1)
show_details('meth1 wrapper', p1)
p1('a goes here')
print

show_details('meth2', o.meth2)
o.meth2('no default for c', d=6)
print

show_details('wrapped meth2', o.wrapped_meth2)
o.wrapped_meth2('no default for c', d=6)
print

show_details('instance', o)
o('no default for e')
print

p2 = functools.partial(o, f=7)
show_details('instance wrapper', p2)
p2('e goes here')

このサンプルはインスタンスから partial やインスタンスのメソッドを作成します。

$ python functools_method.py
meth1 straight:
        object: <bound method MyClass.meth1 of <__main__.MyClass object at
0x10046b3d0>>
        __name__: meth1
        __doc__ 'Docstring for meth1().'
        called meth1 with: (<__main__.MyClass object at 0x10046b3d0>, 'no d
efault for a', 3)
meth1 wrapper:
        object: <functools.partial object at 0x100456f18>
        __name__: meth1
        __doc__ 'Docstring for meth1().'
        called meth1 with: (<__main__.MyClass object at 0x10046b3d0>, 'a go
es here', 4)
meth2:
        object: <bound method MyClass.meth2 of <__main__.MyClass object at
0x10046b3d0>>
        __name__: meth2
        __doc__ 'Docstring for meth2'
        called meth2 with: (<__main__.MyClass object at 0x10046b3d0>, 'no d
efault for c', 6)
wrapped meth2:
        object: <functools.partial object at 0x100456e68>
        __name__: meth2
        __doc__ 'Docstring for meth2'
        called meth2 with: ('wrapped c', 'no default for c', 6)
instance:
        object: <__main__.MyClass object at 0x10046b3d0>
        __name__: (no __name__)
        __doc__ 'Demonstration class for functools'
        called object with: (<__main__.MyClass object at 0x10046b3d0>, 'no
default for e', 6)
instance wrapper:
        object: <functools.partial object at 0x100456f70>
        __name__: (no __name__)
        __doc__ 'partial(func, *args, **keywords) - new function with parti
al application\n    of the given arguments and keywords.\n'
        called object with: (<__main__.MyClass object at 0x10046b3d0>, 'e g
oes here', 7)

wraps

ラップされた呼び出し可能オブジェクトのプロパティ更新は、変換された関数が最終的にオリジナルのプロパティ、”bare”、関数になるのでデコレータで行うとかなり便利です。

import functools

def show_details(name, f):
    """Show details of a callable object."""
    print '%s:' % name
    print '\tobject:', f
    print '\t__name__:', 
    try:
        print f.__name__
    except AttributeError:
        print '(no __name__)'
    print '\t__doc__', repr(f.__doc__)
    print
    return

def simple_decorator(f):
    @functools.wraps(f)
    def decorated(a='decorated defaults', b=1):
        print '\tdecorated:', (a, b)
        print '\t',
        f(a, b=b)
        return
    return decorated

def myfunc(a, b=2):
    print '\tmyfunc:', (a,b)
    return

show_details('myfunc', myfunc)
myfunc('unwrapped, default b')
myfunc('unwrapped, passing b', 3)
print

wrapped_myfunc = simple_decorator(myfunc)
show_details('wrapped_myfunc', wrapped_myfunc)
wrapped_myfunc()
wrapped_myfunc('args to decorated', 4)

functoolswraps() という便利な関数を提供します。それはデコレートされた関数に対して update_wrapper() を適用します。

$ python functools_wraps.py
myfunc:
        object: <function myfunc at 0x10046c050>
        __name__: myfunc
        __doc__ None

        myfunc: ('unwrapped, default b', 2)
        myfunc: ('unwrapped, passing b', 3)

wrapped_myfunc:
        object: <function myfunc at 0x10046c0c8>
        __name__: myfunc
        __doc__ None

        decorated: ('decorated defaults', 1)
                myfunc: ('decorated defaults', 1)
        decorated: ('args to decorated', 4)
                myfunc: ('args to decorated', 4)

比較

Python 2 では、クラスは、そのオブジェクトが比較されるアイテムより小さい、等しい、大きいかに基づいて -1, 0 または 1 を返す __cmp__() メソッドを定義できます。Python 2.1 では、1つの比較比較演算を実行してブーリーアン値を返す 拡張比較(rich comparison) メソッド API である __lt__(), __le__(), __eq__(), __ne__(), __gt__()__ge__() を導入しました。Python3 では、 __cmp__() はこれらのメソッドへ機能を引き継ぐので廃止されました。そのため functools モジュールは Python3 の新しい比較要求に応じた Python2 クラスを書き易くするツールを提供します。

拡張比較(Rich Comparison)

拡張比較(rich comparison) API は、クラスが効率的な方法でテストを実装できるように複雑な比較を許容するように設計されています。しかし、比較が比較的シンプルなクラスのために、それぞれの拡張比較(rich comparison)メソッドを手動で作成しても意味がありません。 total_ordering() クラスデコレータは、幾つかのメソッドを提供して、そういったクラスに残りのメソッドを追加するクラスを扱います。

import functools
import inspect
from pprint import pprint

@functools.total_ordering
class MyObject(object):
    def __init__(self, val):
        self.val = val
    def __eq__(self, other):
        print '  testing __eq__(%s, %s)' % (self.val, other.val)
        return self.val == other.val
    def __gt__(self, other):
        print '  testing __gt__(%s, %s)' % (self.val, other.val)
        return self.val > other.val

print 'Methods:\n'
pprint(inspect.getmembers(MyObject, inspect.ismethod))

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

print '\nComparisons:'
for expr in [ 'a < b', 'a <= b', 'a == b', 'a >= b', 'a > b' ]:
    print '\n%-6s:' % expr
    result = eval(expr)
    print '  result of %s: %s' % (expr, result)

クラスは __eq__() の実装とその他の拡張比較(rich comparison)メソッドのどれか1つを提供しなければなりません。デコレータは提供された比較機能を使用して動作する他のメソッドの実装を追加します。

$ python functools_total_ordering.py
Methods:

[('__eq__', <unbound method MyObject.__eq__>),
 ('__ge__', <unbound method MyObject.__ge__>),
 ('__gt__', <unbound method MyObject.__gt__>),
 ('__init__', <unbound method MyObject.__init__>),
 ('__le__', <unbound method MyObject.__le__>),
 ('__lt__', <unbound method MyObject.__lt__>)]

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 で廃止されるので sort() のような関数に対する cmp 引数も今後はサポートされません。比較関数を使用する Python2 プログラムは、 照合キー を返す関数に対して比較関数を変換するために cmp_to_key() を使用することができます。それは最終的なシーケンスの位置を決めるために使用されます。

import functools

class MyObject(object):
    def __init__(self, val):
        self.val = val
    def __str__(self):
        return 'MyObject(%s)' % self.val

def compare_obj(a, b):
    """Old-style comparison function.
    """
    print 'comparing %s and %s' % (a, b)
    return cmp(a.val, b.val)

# Make a key function using cmp_to_key()
get_key = functools.cmp_to_key(compare_obj)

def get_key_wrapper(o):
    """Wrapper function for get_key to allow for print statements.
    """
    new_key = get_key(o)
    print 'key_wrapper(%s) -> %s' % (o, new_key)
    return new_key
        
objs = [ MyObject(x) for x in xrange(5, 0, -1) ]

for o in sorted(objs, key=get_key_wrapper):
    print o
    

Note

通常 cmp_to_key() は直接使用されますが、このサンプルでは、そのキー関数が呼び出されるときにより詳細な情報を表示するために拡張ラッパ関数が提供されています。

キーを生成するためにシーケンス内の要素毎に get_key_wrapper() を呼び出すことで sorted() が開始することを出力結果が示しています。 cmp_to_key() が返すキーは、旧スタイルの比較関数で提供された返り値に基づいて拡張比較(rich comparison) API を実装する functools で定義されたクラスのインスタンスです。全てのキーが作成された後、そのシーケンスはその作成されたキーを比較することでソートされます。

$ python functools_cmp_to_key.py
key_wrapper(MyObject(5)) -> <functools.K object at 0x100466590>
key_wrapper(MyObject(4)) -> <functools.K object at 0x1004665c8>
key_wrapper(MyObject(3)) -> <functools.K object at 0x100466600>
key_wrapper(MyObject(2)) -> <functools.K object at 0x100466638>
key_wrapper(MyObject(1)) -> <functools.K object at 0x100466670>
comparing MyObject(4) and MyObject(5)
comparing MyObject(3) and MyObject(4)
comparing MyObject(2) and MyObject(3)
comparing MyObject(1) and MyObject(2)
MyObject(1)
MyObject(2)
MyObject(3)
MyObject(4)
MyObject(5)

See also

functools
本モジュールの標準ライブラリドキュメント
Rich comparison methods
Python リファレンスガイドの拡張比較(rich comparison)メソッドの説明
Bookmark and Share