Python filter, reduce, partialの使い方

前回の記事でmap関数について説明しました。今回はmap関数以外の便利な高階関数を以下の3つ紹介します。

組み込み関数:filter
functoolsモジュール:reduce, partial

filter(predicate, iterable)

filter関数を使うと、イテレータの中から特定の条件を満たす要素をイテレータとして取得できます。

引数のpredicate(プレディケイト)はある条件に対する真偽値を返す関数です。iterable(イテラブル)はイテレータとして走査可能な型です。

filter関数は、iterの各要素からpredicate関数の結果がTrueになるイテレータを返します。

以下の例では、filter関数を使って0から9の整数の中から、奇数を返すイテレーターを取得しています。

def is_odd(x):
    return (x % 2) != 0

result = list(filter(is_odd, range(10)))
print(result)

【結果】

[1, 3, 5, 7, 9]

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

reduce関数以降紹介する関数はfunctoolsモジュールの関数なので、functoolsをimportする必要があります。

functionは引数を2つ取り、値を1つ返す関数です。

イテラブルから返される最初の2つの要素AとBを取って func(A, B) を計算します。この結果を3番目の要素 C と組み合わせて func(func(A, B), C) を計算します。この処理をイテラブルが尽きるまで続けます。 

initializerがあるときは、最初の計算で func(initializer, A) が実行されます。その次はfunc(func(initializer, A), B)と続きます。

例)リストの要素の総和に100を加算した結果を求める。

import functools, operator

nums  =  [1, 2, 3, 4, 5]
result = functools.reduce(operator.add, nums, 100)
print(result)

【結果】

115

functools.reduce(operator.add, nums, 100)はnumsの要素を順番に取得し、add関数を実行しています。initializerに100を指定しているので、最初はadd(100, 1)が実行されます。

次にadd(100, 1)の結果の101を用いて、add(101, 2)が実行されます。

以下のように順次計算され、最終的に115を得ます。

add(100, 1) = 101
add(101, 2) = 103
add(103, 3) = 106
add(106, 4) = 110
add(110, 5) = 115

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

新しいpartialオブジェクトを返します。オブジェクトですが、呼び出し可能オブジェクトのため関数のように使います。(オブジェクトを得られますが関数のように使えるため以後は関数として扱います。)これはfuncに位置引数やキーワード引数を部分適用した関数のように振舞います。

(※位置引数の’/’は、funcが1つ以上の引数を必要とする関数で、このfuncに最低でも1つの引数を部分適用をすることを意図しているのではないかなと思います)

こちらも使用例を読んだ方が分かりやすいため以下に部分適用のパターンを3つ紹介します。

import functools

def f(a, device, id='A000'):
    print('send %s to %s(id=%s) '%(a, device, id))

g = functools.partial(f, 2020) # (1)
h = functools.partial(f, id='A111') # (2)
i = functools.partial(f, 2030, 'DB') # (3)

g('DB')
h(2000, 'screen')
i('B000')

【結果】

send 2020 to DB(id=A000) 
send 2000 to screen(id=A111) 
send 2030 to DB(id=B000)

(1) functools.partial(f, 2020)

functools.partial(f, 2020)は関数fの引数aに2020を適用した以下の関数と同等の関数を返します。f関数の第1引数が2020に置き換わっています。

def g(device, id='A000'):
    print('send %s to %s(id=%s) '%(2020, device, id))

このようにパラメータの一部を埋める処理を「関数の部分適用」と言います。

(2) functools.partial(f, id=’A111′)

functools.partial(f, id=’A111′)は関数fのキーワード引数idに’A111’を適用した、以下の関数と同等の関数を返します。f関数の第1キーワード引数が’A111’に置き換わっています。

def h(a, device):
    print('send %s to %s(id=%s) '%(a, device, 'A111'))

(3) functools.partial(f, 2030, ‘DB’)

functools.partial(f, 2030, ‘DB’)は関数fのパラメータに複数部分適用している例です。以下の関数と同等です。キーワード引数も同様に複数部分適用できます。

def i(id='A000'):
    print('send %s to %s(id=%s) '%(2030, 'DB', id))

partialを有効に使うためには?

partial関数を有効に使うためには、

変更の多い引数ほど右側に定義する

ことが重要になります。例えば上の例で、

g = functools.partial(f, 2020) # (1)
g('DB')

のようにf関数の第一引数を2020で部分適用しました。この場合、関数fを使わなくても、関数gを使って簡潔に”複数のデバイスに2020を送る処理(print文)”を実行できます。

g('DB1')
g('DB2')
g('DB3')

しかし、関数fが以下のように定義されていた場合、

def f(device, a, id='A000'):
    print('send %s to %s(id=%s) '%(a, device, id))

g関数と同じ関数を定義するには

g1 = functools.partial(f, a=2020)

のよう部分適用したい引数が位置引数でもキーワード引数のように指定する必要があります。

このように引数の並びによって、宣言の仕方が変わり可読性に影響が出ます。そのため、変更の多い引数ほど右側に定義することをお勧めします。

スポンサーリンク
スポンサーリンク