Python の map と filter ってなに?






map

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


filter

リストの各要素のうち条件に満たないものを削除します。







1. map


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

f:id:domodomodomo:20180813232739j:plain

# リストの各要素を二乗します。
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


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

f:id:domodomodomo:20180813232746j:plain

# 偶数だけ取得します。
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



1 章...map
2 章...filter
3 章...lambda 式
4 章...map, filter はリストではありません。
5 章...map, filter とリストは、どう違うの?
6 章...map, filter を使うときの注意事項
7 章...map, filter, lambda 式は嫌われもの
8 章...map, filter, lambda 式はなんで導入されたの?
9 章...map, filter があれば for 文はいらない?
10 章...まとめ

3. 無名関数 lambda

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

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

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

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)

3.1. どの辺が "無名" なの?

ちなみに 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)))

プログラミングにあたっての推奨事項 - PEP8
Programming Recommendations - PEP8

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)

3.2. その他の高階関数

map, filter 以外に高階関数はあるのでしょうか? 実は、組み込み関数 max, min, sorted は、関数を引数に取ることができる高階関数だったりします。 import せずに使える関数を 組み込み関数 と言います。

max([0, 1, -2, 4, 10, -11, 2, 3], key=lambda x: x**2)
# -11 <- (-11)**2 が最も大きいので -11 が返されています。 

4. 正確に言えば...


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

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

4.1. 正確に言えば... 引数は...

list, tuple, dict, set など for 文で回せるオブジェクトなら、なんでも引数に取れます。 for 文で回せるオブジェクトのことを iterable と言います。

str も iterable です。なので極端な話 str も引数に取れます。

s =  'abcdefg'

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

# a!
# b!
# c!
# d!
# e!
# f!
# g!

4.2. 正確に言えば... map, filter は関数じゃなくてクラス

map, filter は高階 関数 と書きましたが、 map, filter は、リストを返す関数ではありません。 実は map, filter は、クラスです。 組み込み型はクラス名が大文字でないので誤解しやすい。

例えば map クラスを使うと map オブジェクトが返されます。

map(lambda x: 2 * x, range(3))
# <map object at 0x10ebdb0f0> 

isinstance(map, type)
# True

5. map, filter とリストは、どう違うの?

map, filter のリストと比較したメリットとデメリットをご紹介します。

リストは、for loop が実行される前に、全ての要素を存在しています。 それに対してジェネレータイテレータは、for loop が回るたびに、 処理を起動をして要素を生成し、要素を渡したら処理を中断しています。

5.1. map のデメリット

このようにして、必要になるまで処理を実行しないことを遅延評価と言います。 遅延評価のため、ジェネレータは for 文や next 関数 を使って1つ1つ要素を取り出すことはできますが。

m = map(lambda x: 2 * x, range(10))

next(m)  #  0
next(m)  #  2
next(m)  #  4 
next(m)  #  6
next(m)  #  8
next(m)  # 10
next(m)  # 12
next(m)  # 14
next(m)  # 16
next(m)  # 18


反対に map オブジェクトは、添字表記でいきなり最後の要素を参照したりはできません。 これは next 関数で呼び出されたり、あるいは for 文で呼び出されるたびに計算されているからです。

m = map(lambda x: 2 * x, range(10))

m[9]  # TypeError

5.2. map のメリット

list を返してくれた方がわかりやすそうです。 実際 Python 2 では map はリストを返す関数でした。

>>> # Python 2
>>> map(lambda x: 2*x, [0, 1, 2, 3])
[0, 2, 4, 6]
>>> 


短いリストなどメモリを必要としない場合は問題ありません。 しかし、ファイルのような多くのメモリを必要とするものを取り扱ったりするような場合に、 1度に全てをリストにしてしまうと大量のメモリを消費してしまいます。 例えば 10**8 のような長大なリストを生成てみると、1度に大量のメモリが消費されるのがわかります。

import sys

sys.getsizeof(map(lambda x: 2*x, range(10**8)))
# 56

sys.getsizeof(list(map(lambda x: 2*x, range(10**8))))
#815511904 <- リストにすると大量のメモリを消費する。

sys.getsizeof(object[, default])
オブジェクトのサイズをバイト単位で返します。オブジェクトは、どのような型でも使えます。 全ての組み込み型は、正しい結果を返してくれますが、 サードパーティ製の型は、正しい結果を返してくれるとは限らず、実装によって異なるかもしれません。 属性に直接代入されたオブジェクトが消費したメモリだけ計測され、 属性の属性に代入されたオブジェクトが消費するメモリについては計測しません。
Return the size of an object in bytes. The object can be any type of object. All built-in objects will return correct results, but this does not have to hold true for third-party extensions as it is implementation specific. Only the memory consumption directly attributed to the object is accounted for, not the memory consumption of objects it refers to.

5.3. まとめ

map, filter は、リストではありません。 デメリットは、リストのように途中の値を参照することはできず、すこし使い勝手が悪いです。 メリットはメモリの消費量を抑えることができます。 大量のオブジェクトを取り扱う時に威力を発揮します。

6. map, filter を使うときの注意事項

map, filter オブジェクトは、1度 for 文で回すと空になります。 このことに引っかかって、時間を費やしてしまった方を Twitter や Qiita でたまに見かけます。

map/zip/filter オブジェクトに対して、list を2回やると空っぽになります。 最初何が起こったのかわからずバグじゃないかとか、破壊的メソッドか!?などと思ったりしたわけですが、仕様らしいです。
python の map オブジェクトを list にした後は何も残らない - Qiita


簡単に確認してみます。

m = map(lambda x: x**2, range(3))

for e in m: e

for e in m: e
>>> for e in m: e
... 
0
1
4
>>> for e in m: e
...  # <- 何も起こらない。
>>> 


なぜ、このようなことが起こるのでしょうか? それはジェネレータイテレータイテレータだからです。 この原因の詳細は、このあと「イテレータってなに?」の中で説明させていただきます。

とりあえず対応策だけ知りたい方は、こちらからどうぞ。3つの対応策を示しています。
Python のイテレータってなに? - いっきに Python に詳しくなるサイト

7. Guido は lambda, map, filter が嫌い

Python の開発者である Guido van Rossum は、lambda, map, filter が嫌いだそうです。 嫌いって言われても... って感じですが、まったく同じ機能を持つジェネレータ式を使って欲しいとのことです。


7.1. ジェネレータ式

ジェネレータ式とは何でしょうか?ジェネレータ式は map, filter と全く同じ機能を持ったものです。

lst = [0, 1, 2, 3]

# map
m = map(lst, lambda x: x * x)
list(m)

# ジェネレータ式
g = (x * x for x in lst)
list(g)
lst = [0, 1, 2, 3]

# filter
f = filter(lambda x: x % 2, lst)
list(f)

# ジェネレータ式
g = (x for x in lst if x % 2)
list(g)


ジェネレータ式の詳細については 「yield, ジェネレータってなに?」 で説明させていただきました。

実は、この記事は「for 文を理解する5講座」の3回目になります。 1回目の記事は簡単な導入なので飛ばしていただいて、2回目の 「ジェネレータってなに?」から読んでいただけるように頑張って書いてみました。
f:id:domodomodomo:20181103193404j:plain

7.2. map, filter とジェネレータ式との使い分けは?

果たしてどちらを使うべきでしょうか? 基本的には読みやすい方を。 どちらでも良い場合は、Guido は map, filter を嫌っているので、ジェネレータ式を使った方が良いかなと思ったりもします。


反対に、既に関数が定義されているようなシーンでは map を使うといいかなと。 簡単に言えば lambda 式を使うくらいならジェネレータ式やリスト内包表記で書いた方が良いかなと思ったりもします。

# 0. 元のやり方
#     関数 f を 2 回呼び出しててちょっと嫌だな...
[f(x) for x in lst if f(x) % 2 == 0]

# 1. map を使った改善例
[y for y in map(f, lst) if y % 2 ==0]

# 2. ジェネレータ式を使った改善例
[y for y in (f(x) for x in lst) if y % 2 == 0]

# 3. かと行って lambda を使ってまで高階関数を使うと、なんだか汚くなる
list(filter(lambda y: y % 2 == 0, map(f, lst)))

7.3 どのくらい嫌われているの?

結構、嫌われています。以下の文章で、「リスト内包表記」は「ジェネレータ式」に読み替えてください。 以下の文章は Python 2 の頃のもので、その頃はまだ map, filter がリストを返す関数だったためです。

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

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

I've never liked lambda

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

Python Regrets

map(), filter()

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

map(), filter()

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

Python Regrets

リスト内包表記〜ジェネレータ式 -The History of Python.jp

リスト内包表記は、組み込み関数のmap()とfilter()の代替手段となっている。 map(f, S)は、[f(x) for x in S]と同じ意味となるし、filter(P, S)は[x for x in S if P(x)]と同じ意味となる。 map()やfilter()を使った表現の方がコンパクトであるため、 リスト内包表記の方が推奨されていないのでは?と思う人もいるだろう。

しかし、より現実的なサンプルを見ると見解が変わるだろう。 与えられたリストの全ての要素に数字の1が足された、新しいリストを作りたいとする。 リスト内包表記を使った場合には、[x+1 for x in S]と表現できる。 map()を使うと、map(lambda x: x+1, S)となる。 "lambda x: x+1"はインラインで無名関数を作るPythonの書き方である。

ここでの本当の問題はPythonのラムダ記法が冗長すぎることで、 この表記がもっと簡潔になればmap()を使った記法がより魅力的になるはずだ、ということがずっと主張されてきた。 私個人の見解としてはこれには反対である。 リスト内包表記の方が、 特にマップされる式の複雑さが増加するとmap()を使った関数表記よりも見やすくなることが分かったからである。

8. map, filter, lambda 式の由来


こんな嫌われているもの、いったいどういう経緯で組み込まれたのでしょうか? 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

8.1. reduce 関数

map, filter, lambda 以外に reduce という文字が見えます。reduce とは何をしてくれる関数でしょうか? ちなみに嫌われ過ぎて組み込み関数から削除された重要度の低い関数なので、覚える必要はあまりないかなと思います。

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

f:id:domodomodomo:20180813232751j:plain

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

# 総乗を求めます。
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


ちなみに reduce は、関数を引数に取るので高階関数になります。

8.2. reduce 関数 - 組み込み関数から外される

Python 2 では reduce 関数が、組み込み関数として使えました。 Python 3 では reduce 関数は、組み込み関数 から除外されて 標準ライブラリ の1つである functools から import することになりました。

import しないと使えなくなったということは1軍から2軍に格下げされたということです。 なぜ格下げされたかと言うと読み辛いからだそうです。

reduce()

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

reduce()

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

Python Regrets

今度は 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

9. map, filter さえあれば for 文はいらない?

JavaScript の Qiita の記事ですが、すごく感情的なコメントが多い記事を見つけました。 なぜこんなに感情的になったのかは、いくつか要因があると思います。


その記事では for 文を使わず map と filter という関数型言語由来の表記を使えば、可読性はよくなると主張されています。上のコードよりも下のコードの方が読みやすいと主張されています。

# これより
totalOfEvenNumberUnder100 = 0
for i in range(100):
    if i % 2 == 0:
        totalOfEvenNumberUnder100 += i

print(totalOfEvenNumberUnder100) # 2450
# こっちが読みやすい
from functools import reduce
from0To100Array = list(range(100))
isEvenNumber = lambda i: i % 2 == 0
addAll = lambda total, i: total + i
totalOfEvenNumberUnder100 = reduce(addAll, filter(isEvenNumber, from0To100Array))

print(totalOfEvenNumberUnder100)  # 2450

原因 1. 糖衣構文をどのような場合にも使えるとしてしまった。

主たる要因は for 文を禁止にはできないと言うことかなと思います。 map, filter を使えば読みやすくなるときもありますが、そうではないときもあります。 map, filter を使えば読みやすくなるのは、ごく限られば場合、単純なケースだけです。

map, filter は ジェネレータ式と、同じ機能を提供します。ジェネレータ式は、ジェネレータ関数の糖衣構文です。 GooglePython のコーディング規約には、単純な場合にだけ、ジェネレータ式を使うように示しています。 ひっくり返せば map, filter も単純な時にだけ効果的だと言うことです。

単純なケースでは使ってもいいよ!
Okay to use for simple cases.
2.7 Comprehensions & Generator Expressions

糖衣構文 - Wikipedia
糖衣構文は、プログラミング言語において、読み書きのしやすさのために導入される書き方であり、 複雑でわかりにくい書き方と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書くことができるもののことである。 構文上の書き換えとして定義できるものであるとも言える[1]。


Python には他にも糖衣構文として三項演算子があります。 三項演算子にも同じような指摘があります。

複雑な式で三項演算子を使うと、途端にわかりにくくなる。
三項演算子?:は悪である。- Qiita


ちなみに Python ではこのコードは、ジェネレータ式を使って、もっと綺麗にかけます。sum 関数を使っていて、卑怯ですが、それでも Guido が map 関数よりもジェネレータ式を好んだ理由が端的に伝わるのではないでしょうか。

totalOfEvenNumberUnder100 = sum(i for i in range(100) if i % 2 == 0)

print(totalOfEvenNumberUnder100)  # 2450

原因 2. 長すぎる変数名を書いてしまった。

副次的な要因としては、関数名が長いことだと思います。長い関数名には、圧迫感があります。 書籍 Readable Code では変数は、初めての人が読んでわかるかどうか?を基準にして決めることを紹介していました。

長い関数名そのものには、全く問題がないと思うのですが。 "100 以下の偶数の合計を計算する" と言う、あまりにもわかりきった処理に対して長い関数名を当ててしまっています。

それがおそらく "冗長すぎるやろ" という認識を起こしてしまったのだと思います。 この関数が、業務ロジックのような、ちゃんと読まないとわからないコードだったら話は違ったと思うのですが。

// JavaScript
// 元のコード
const from0To100Array = Array.from(Array(100).keys());
const isEvenNumber = i => i % 2 === 0;
const addAll = (total, i) => total + i;
const totalOfEvenNumberUnder100 = from0To100Array.filter(isEvenNumber).reduce(addAll);
=>

この矢印はアロー関数と呼ばれるもので Python のラムダ式と等価です。
JavaScript はかなりアロー関数や無名関数が多用されていて辛いです。

最初は何故 Guido が lambda を嫌っていたのかわからなかったのですが
JavaScript を触ってると Guido が嫌った理由がわかる気がします。
Guido はやっぱりすごいと思ってしまう訳です。

// JavaScript
i => i % 2 === 0

# Python
lambda i: i % 2 == 0
const

// JavaScript
頭の const は定数宣言です。
const が頭につくと変更できない、再代入することができない変数になります。
これを「定数」と言います

# Python
Python では None, True, Flase が「定数」ですが(正確には class などと同じ「予約語」らしい)、
自分で定数を作ることはできません。

なぜわざわざこの人が const を書いているかというと
一般に変数に再代入しない方が、副作用がない方が
可読性のよいコードになると言われているからです。
http://nihaoshijie.hatenadiary.jp/entry/2016/10/14/234418#side-effect


可読性が上がるかどうかは別にして、変数名を短くすると精神的な負荷が低いコードになります。

// JavaScript
// 少し書き換えてみる。
const n = 100
const array = Array.from(Array(n).keys());
const isEven = i => i % 2 === 0;
const add = (a, b) => a + b;
const sumOfEvenArray = array.filter(isEven).reduce(add);


  1. from0To100Array の 100 は、n にして抽象度合いを上げて削除
  2. isEvenNumber の Number は、自明なので削除
  3. addAll の All は、All を add してないので削除


テストコードや設定など定数を書き入れる場合は 100 と書いてもいいと思うのですが、 Qiita で書くサンプルコードなら 100 は切っておいた方がいいかもしれないと思いました。



◯ まとめ

① 適用できる範囲が本来はもう少し限定されるのに for 文はいらないと言ってしまったのと、 ② 長い変数名を使ってしまったことが、炎上の原因かなと思ったりもします。

長い変数名を使うなら、取り上げる例を、もう少し業務ロジック的なもの、 長くならざる得ないものを取り上げていれば、温度感も、もう少し違ったのかなと思ったりもします。

使えるシーンは限られるとは思うのですが、 計算量が悪化しても for 文を分割してかく書き方が、他の方に説明しやすいコードになるので、 必ずしも悪くはないのかなと思ったりもします。

# for 文を
#     処理が1つにまとまっていて説明しにくい...

for cls in __builtins__.__dict__.values():
    if isinstance(cls, type) and hasattr(cls, '__iter__'):
        print(cls)
# 分割してしまう
#     処理が分割されていて説明しやすい。

#     Step 1. 組み込み型の一覧を取り出す。
builtin_types\
    = (cls for cls in __builtins__.__dict__.values() if isinstance(cls, type))

#     Step 2. イテラブルな組み込み型の一覧を取り出す。
builtin_iterbale_types\
    = (cls for cls in builtin_types if hasattr(cls, '__iter__'))

#     Step 3. イテラブルな組み込み型の一覧を表示する。
print(*builtin_iterbale_types, sep='\n')
>>> print(*builtin_iterbale_types, sep='\n')
<class 'bytearray'>
<class 'bytes'>
<class 'dict'>
<class 'enumerate'>
<class 'filter'>
<class 'frozenset'>
<class 'list'>
<class 'map'>
<class 'range'>
<class 'reversed'>
<class 'set'>
<class 'str'>
<class 'tuple'>
<class 'zip'>
>>> 

10. まとめ

map, filter はジェネレータ式と全く同じ機能を有しています。 Guido は map, filter が嫌いなので、どちらを使うか迷ったら ジェネレータ式を使うようにするのがいいかなと思います。

また reduce を通して組み込み関数は1軍、標準ライブラリは2軍と言った 温度感についても簡単に触れて見ました。

ジェネレータ, filter, map から生成されるオブジェクトは、イテレータに分類されます。 そして next 関数を使いイテレータを実際に触れ、リストのように添字表記 lst[0] とは参照できず使い勝手が悪い反面、省メモリではあることを見てきました。

そして最後に炎上した記事を通して、map, filter は関数ですが、糖衣構文は簡単なケースでしか有効には使えないことを、実際のコード例を示せてはいませんが、Google のコーディング規約から確認しました。

次は、map, filter そしてジェネレータの上位概念である「イテレータ」とは何か、について触れていきたいと思います。