Python の map, filter, reduce ってなに?




map, filter, reduce を使えば、こんなことができるようになります


f:id:domodomodomo:20180813232739j:plain f:id:domodomodomo:20180813232746j:plain f:id:domodomodomo:20180813232751j:plain



無理やり一言でまとめれば

  1. map ... リストの各要素に関数を適用します。

  2. filter ... リストから条件を満たす要素だけを取り出します。

  3. reduce ... リストの要素を関数を元に累積します。


Lisp 由来の表記みたいですね。

12 年前にに Python は lambda, reduce, filter そして map を獲得した。 礼儀正しい(と私は信じている)Lispハッカーが lambda, reduce, filter, map が恋しくなり working pathces を提出した。
About 12 years ago, Python aquired lambda, reduce(), filter() and map(), courtesy of (I believe) a Lisp hacker who missed them and submitted working patches.
The fate of reduce() in Python 3000 by Guido van van Rossum


1. map

リストの各要素に関数を適用します。

# リストの各要素を二乗します。
def double(x):
    return x * 2

lst = [1, 2, 3, 4, 5, 6, 7]

for e in map(double, lst):
    print(e)

# 2
# 4
# 6
# 8
# 10
# 12
# 14

2. filter

リストから条件を満たす要素だけを取り出します。

# 偶数だけ取得します。
def is_even(x):
    return x % 2

lst = [1, 2, 3, 4, 5, 6, 7]

for e in filter(is_even, lst):
    print(e)

# 2
# 4
# 6

3. reduce

リストの要素を関数を元に累積します。

# 総乗を求めます。
import functools

def multiply(x, y):
    return x * y

lst = [1, 2, 3, 4, 5, 6, 7]

product = functools.reduce(multiply, lst)
print(product)
# 5040

無名関数 lambda

関数を引数に取ることができる関数を「高階関数」と呼びます。map, filter, reduce は、高階関数です。

上で見たサンプルコードの関数は double, is_even, multiply は、いずれもとても簡単なものですね。 1行で定義から関数の実引数の代入までできたら便利です。

それを実現するのが lambda 式です。lambda 式を使い上の例を書き換えて見ましょう。関数を def で定義することなく簡潔に表現できています。

import functools

lst = [1, 2, 3, 4, 5, 6, 7]

for e in map(lambda x: 2 * x, lst):
    print(e)

for e in filter(lambda x: x % 2, lst):
    print(e)

product = functools.reduce(lambda x * y: x * y)
print(product)


ちなみに lmabda は、無名関数という名前の通り名前がありません。

# 無名関数
double = lambda x: x * 2
double.__name__  # '<lambda>'

# 関数
def double(x):
    return 2 * x

double.__name__  # double


PEP 8 では lambda は、関数の定義では使用しないように指示されています。lambda を使用するのは、原則、関数オブジェクトを引数にとる関数に代入するときだけ。また変数などに代入せず、そのまま引数として与えてください。

# OK
list(map(lambda x: x**2, range(10)))

# NG
f = lambda x: x**2
list(map(f, range(10)))

lambda 式を識別子に直接束縛する代入文ではなく、常に def 文を使ってっください。
Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier.

Yes:
def f(x): return 2*x

No:
f = lambda x: 2*x

最初の形式は、結果としてえられる関数オブジェクトの名前が、一般的な <lambda> ではなく 'f' という名前がつけられていることを意味しています。関数オブジェクトに文字列の名前が与えられていることは、一般に例外が発生した時にそれをトレースバックさせたり、関数名を文字列で出力させる際に役立ちます。代入文を使うことは(代入文で lambda 式を変数に束縛してから map, filiter などの高階関数に引数として与えることは)、 def 文にはなく lambda 式にある、たった1つの利点(すなわち、lambda 式は、より大きな式の中に埋め込められるということ)を無意味なものにしてしまいます。
The first form means that the name of the resulting function object is specifically 'f' instead of the generic '<lambda >'. This is more useful for tracebacks and string representations in general. The use of the assignment statement eliminates the sole benefit a lambda expression can offer over an explicit def statement (i.e. that it can be embedded inside a larger expression)
Programming Recommendations - PEP8

Guido は lambda, map, filter, reduce が嫌い


ただ Guido は lambda 式は嫌いらしいです。

私は lambda が、いいと思ったことがない。

  • 不自由(たった1行しか書けない)
  • 紛らわしい(引数リストの括弧がない)
  • 普通の関数定義で代用できる。

I've never liked lambda

  • crippled (only one expression)
  • confusing (no argument list parentheses)
  • can use a local function instead

map(), filter()

  • Python の関数を使うのは遅い。
  • リスト内包表記は同じことをよりよく実行する。

map(), filter()

  • using a Python function here is slow
  • list comprehensions do the same thing better

reduce()

  • 誰も使ってない、少しの人しか理解してない。
  • for ループの方が理解しやすいし、たいていの場合速い

reduce()

  • nobody uses it, few understand it
  • a for loop is clearer & (usually) faster

Python Regrets


「リスト内包表記は同じことをよりよく実行する。」という文言は、「ジェネレータ式は同じことをよりよく実行する。」に読み替えてください。詳細は、おって説明します。

◯ 組み込み関数から外された reduce

Python 2 では reduce 関数が組み込み関数として使えました。 Python 3 では reduce 関数は、組み込み関数から除外されて functools から import することになりました。

なぜかと言うと読み辛いからだそうです。以下のブログは Guido のブログからの引用です。

今度は reduce 関数について考えよう。これは実際私がいつも最も憎むものだ。+ もしくは * を含む2、3の例を除けば reduce 関数はいつもパッと見ではわからないような関数を引数にとって呼び出されている。
So now reduce(). This is actually the one I've always hated most, because, apart from a few examples involving + or *, almost every time I see a reduce() call with a non-trivial function argument,

私は reduce 関数が何をしているかを理解する前に、引数として与えられた関数に、実際に何が与えられているのかを図示するために紙とペンを取らないといけない。
I need to grab pen and paper to diagram what's actually being fed into that function before I understand what the reduce() is supposed to do.

したがって私の中では reduce 関数が適用できるのは、結合演算子にごく限定される(訳注: 結合演算子 ... 例えば +, -, *, / などの四則演算子)。それ以外の事例では明示的に累積ループを書いた方がよい。
So in my mind, the applicability of reduce() is pretty much limited to associative operators, and in all other cases it's better to write out the accumulation loop explicitly.
The fate of reduce() in Python 3000 by Guido van van Rossum

# 
import functools
lst = [1, 2, 3, 4, 5]

# 1. 明示的に累積ループで書く
def product(s):
    p = 1
    for e in s:
        p = p * e
    return p
product(lst)  # 120

# 2. reduce で書く
functools.reduce(lambda x, y: x * y, lst)  # 120

# 3. Python2 では import しなくても書けた
# reduce(lambda x, y: x * y, lst)  # 120
# reduce で最大値を求めてみる。
import functools
lst = [3, 4, 5, 1, 2, 0]

# 1. 明示的に累積ループで書く
def max(lst):
    x = lst[0]
    for y in lst:
        x = x if x > y else y
    return x
max(lst)  # 5

# 2. reduce で書く
functools.reduce(lambda x, y: x if x > y else y, lst)  # 5

せっかく覚えたのに、推奨されてないなんて...

推奨されてないわけではないですが、Guido は嫌いみたいです。もしジェネレータ式よりも map, fileter の方が綺麗にかけるなら、map, filter を使ってもいいんじゃないかなと思ったりもします。

Guido が、どう感じていたかを知ることが、この記事の主な目的だったりもします。

正確に言えば...

iterable であればなんでも引数に取れます。なので極端な話 str も引数に取れます。iterable とは for 文の in の中に書くことができるクラスのオブジェクトのことです。dict, tuple などが該当します。そんなに難しいものではありません。

s =  'abcdefg'

for e in map(lambda c: c + '!', s):
    print(e)

さらに正確に言えば...

map, filter は、リストを返す関数ではありません。 map, filter は、クラスです。そして map, filter クラスは、イテレータでもあります。

# X (間違いではないけど...)
リスト = map(関数, リスト)

# O (正確には)
イテレータ = map(関数, イテラブル)


では、イテレータとは何でしょうか?イテレータは for 文と密接に絡んでいます。 また map, filter と全く同じ効果をもつジェネレータ式についても紹介します。