# map と filter ってなに?

map

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


filter

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

Hello, world!

# 1. map 関数

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

例えばリストの要素に2倍にする関数 double を適用したいときは、 for 文を使って次のように書きます。

# リストの各要素を二倍します。
lst = [1, 2, 3, 4, 5, 6, 7]

def double(x):
    return x * 2


new_lst = []
for e in lst:
    new_lst.append(double(e))


print(new_lst)
# [2, 4, 6, 8, 10, 12, 14]

map を使うともっとあっさりと書けます。  for 文を短くかけると言うのが map の1つのメリットです。 

# リストの各要素を二倍します。
lst = [1, 2, 3, 4, 5, 6, 7]

def double(x):
    return x * 2


new_lst = list(map(double, lst))


print(new_lst)
# [2, 4, 6, 8, 10, 12, 14]

map 関数の引数は以下の通りです。

map(関数, イテラブル)

イテラブルとは for 文の in の中にかけるオブジェクトで range, list などが該当します。

#
# 対話モード >>> に
# コピペできます。
#
イテラブル = [0, 1, 2]
for 要素 in イテラブル:
    print(要素)
>>> for 要素 in イテラブル:
...     print(要素)
... 
0
1
2
>>> 

なぜ list を使っていたかというと、

new_lst = list(map(double, lst))

map 関数の返り値の型がイテラブルで、 そのままだと print してもよくわかならいものが表示されるからです。

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

def double(x):
    return x * 2

iterable = map(double, lst)
new_lst = list(iterable)

print(iterable)
print(new_lst)
>>> print(iterable)
<map object at 0x10c55fcf8>   <--- 表示されても
                                   わからない
>>> print(new_lst)
[2, 4, 6, 8, 10, 12, 14]
>>> 

イテラブルは list を使いリストに変換できます。 例えば range はイテラブルなので list を使いリストに変換できます。

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

# 2. filter 関数

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

例えばリストから偶数の要素だけを取り出したいとします。 is_even と for 文を使って次のように書きます。

# 偶数だけ取得します。
lst = [1, 2, 3, 4, 5, 6, 7]

def is_even(x):
    return x % 2


new_lst = []
for e in lst:
    if is_even(e):
        new_lst.append(e)


print(new_lst)
# [1, 3, 5, 7]

これを filter 関数を使うともっとスッキリとします。

# 偶数だけ取得します。
lst = [1, 2, 3, 4, 5, 6, 7]

def is_even(x):
    return x % 2


new_lst = list(filter(is_even, lst))


print(new_lst)
# [1, 3, 5, 7]

このようにして map, filter を使うと for 文を使わずに短く書けるようになる時があります。

# 3. lambda 関数

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

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

# 1) def 関数定義文
lst = [1, 2, 3, 4, 5, 6, 7]

def double(x):
    return x * 2


new_lst = list(map(double, lst))


print(new_lst)
# [2, 4, 6, 8, 10, 12, 14]
# 2) lambda 式
lst = [1, 2, 3, 4, 5, 6, 7]


new_lst = list(map(lambda x: 2 * x, lst))


print(new_lst)
# [2, 4, 6, 8, 10, 12, 14]

lambda 式は def 文と同じで関数を定義しているだけです。

double = lambda x: 2 * x
double(2)
# 4
# lambda 式の書式
lambda 変数: 式
問題

以下のコードを def 文から lambda 式で書き換えてください。

# 偶数だけ取得します。
lst = [1, 2, 3, 4, 5, 6, 7]

def is_even(x):
    return x % 2


new_lst = list(filter(is_even, lst))


print(new_lst)
# [1, 3, 5, 7]

# 3.1. lambda 式と def 文の違い

次の2つは全く同じ動作をします。 では lambda 式と def 文の違いはなんでしょうか?

# lambda 式
double = lambda x: 2 * x
double(2)
# 4
# def 文
def double(x):
    return x

double(2)
# 4

def と lambda の違いは、次の3つになります。

def と lambda の違い
項番deflambda
(1)複数行書ける一行しか書けない
(2)
(3)名前がある名前がない

# 違い その1 - lamda は一行しか書けません。

実は Python はセミコロン ; を使うと 単純文 をつないで一行で書くことができるという裏技があります。 滅多に使ったことがないので、覚える必要はありません。

print('Hello, world!'); print('Nihao, shijie!'); print('こんにちは、世界!')
>>> print('Hello, world!'); print('Nihao, shijie!'); print('こんにちは、世界!')
Hello, world!
Nihao, shijie!
こんにちは、世界!
>>> 

lambda 式は ; を使ったりしても、 どう足掻いても複数行で書くことはできません。

lambda x: print(x); 2 * x
>>> lambda x: print(x); 2 * x
                      ^ ここで区切られている。
<function <lambda> at 0x108904598>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>> 

どう足掻いても絶望
ホラーゲーム SIREN のキャッチコピー

絶望はしませんが笑 ちなみに if 文, for 文, def を使う関数定義文は、 複数行使うので 複合文 と呼ばれ、 単純文とは区別されています。 セミコロン ; で複合文をつなぐことはできません。

問題
複数行で書けるのは、どちらですか?

# 違い その2 - lambda には名前がありません。

ちなみに lmabda 式は、たまに  無名関数  と呼ばれていたりします。 なぜ、無名関数と呼ばれるのでしょうか? ぱっと見 double という変数に代入しているので、 無名ではなさそうに見えます。

しかし実際に名前の通り、名前がないからです。 関数の名前は、特殊属性 __name__ で調べることができます。 ところが lambda で定義した関数は、すべて名前が <lambda> になってしまうのです。

# 無名関数
double = lambda x: 2 * x
triple = lambda x: 3 * x
def quad(x):
    return 4 * x

double.__name__
triple.__name__
quad.__name__
>>> double.__name__
'<lambda>'  # <-- lambda になる
>>> triple.__name__
'<lambda>'  # <-- lambda になる
>>> quad.__name__
'quad'
>>> 

関数を引数に取ることができる関数を難しい言葉で  高階関数  と呼びます。map, filter は、高階関数です。

実際、高階関数を自分で定義して、引数として lambda 式を受け取ると、 その関数の名前が何かわからないのです。

これは一見ささいなようですが、デバックをするときに この子は誰?みたいなことになって苦しくなったりすることがあるそうです。

f = lambda x: x**2

def g(f):
    print(f.__name__)

g(f)
>>> g(f)
<lambda>
>>> 

無名関数, lambda 式という言葉は JavaScript など他のプログラミング言語でもでてくる用語なので、 覚えておいても良い単語かなと思ったりします。

問題
文と式という言葉があります。式は、どちらですか?

# 違い その3 - lambda は「式」です。

lambda 式は「式」なので、変数に代入できます。 しかし def を使う関数定義文は「文」なので、直接、変数に代入はできません。

def f(x): return x

f(2)

h = def g(y): return y
>>> def f(x): return x
... 
>>> f(2)
2
>>> 
>>> h = def g(y): return y
  File "<stdin>", line 1
    h = def g(y): return y
          ^
SyntaxError: invalid syntax
>>> 


問題

str 型で関数名を定義できるのは、どちらですか?

# 3.2. PEP 8 - コーディング規約

 PEP 8  という  コーディング規約  があります。 コーディング規約とは、みんなでこうやって書こうねと決めた「書き方」決まりごとです。 例えば、メソッドを定義するときに、いつも self と書いてるのも、この PEP 8 で定められています。

その 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 式を map, filter などの高階関数以外で使うと、 読みづらくなるからだそうです。 最初は、大袈裟やなーと思っていたのですが、 JavaScript を触るようになって、多用される無名関数に大変辛い思いをしています笑

おそらく lambda が一行しか使えないように制限されているのも、 そう言ったところにあるのかなと思ったりします。 ちょっと長いですが、以下 PEP 8 の文章を抜粋、引用します。

プログラミングにあたっての推奨事項 - 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.3. その他の高階関数

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

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

また、高階関数に渡される関数を「コールバック関数」と、たまに呼ばれていたりします。 上の例で言えば lambda x: x**2 はコールバック関数です。

いろんな関数がでて来たので、最後に列挙しておきます。 名前を覚える必要は全くないのですが、 たまに Qiita とかで見かけて知らないと混乱するので、 心の片隅にあってもいいかなと。

  1. 高階関数 ... 関数を引数に取る関数
  2. コールバック関数 ... 引数に渡される関数
  3. 無名関数 ... 名前のない関数
  4. 組み込み関数 ... import しなくても使える関数

# 4. 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
...  # <- 何も起こらない。
>>> 

なぜ、このようなことが起こるのでしょうか? それは map, filter がリストではなくイテレータだからです。 この原因の詳細は、この次で見ていきたいと思います。

とりあえず対応策だけ知りたい方は、こちらからどうぞ。 3つの対応策を示しています。

# 1) もう一回呼ぶ
iterator = map(lambda x: x**2, range(3))

# 2) リストにする
list_ = list(map(lambda x: x**2, range(3)))

# 3) コンテナクラスを作る
class Container:
    def __init__(self, container):
        self._container = container
    
    def __iter__(self):
        return map(lambda x: x**2, self._container)

container = Container(range(3))

上のコードで作った Container クラスのオブジェクトは、 何回 for 文で回しても空にはなりません。 3つ目の「コンテナクラスを作る」の仕組みについても、 「イテレータってなに?」 の中で見ていきたいと思います。

>>> for element in container:
...     element
... 
0
1
4
>>> for element in container:
...     element
... 
0
1
4
>>> 

# 5. じゃあ map, filter は何者なの?

実はちょっと嘘をついていました。

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

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

# 5.1. 正確に言えば... 引数は...

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

str もイテラブルです。なので極端な話 str も引数に取れます。

s =  'abcdefg'

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

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

# 5.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

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

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

リストは、for 文が実行される前に、全ての要素を存在しています。 それに対して map, filter は、for 文が回るたびに、 処理を起動をして要素を生成し、要素を渡したら処理を中断しています。

# 6.1. map のデメリット

このようにして、必要になるまで処理を実行しないことを遅延評価と言います。 遅延評価のため map, filter は 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 オブジェクトは、 リストのような lst[0] 添字表記 subscription でいきなり最後の要素を参照したりはできません。 これは next 関数で呼び出されたり、あるいは for 文で呼び出されるたびに計算されているからです。

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

m[9]  # TypeError

ちなみに subscription という言葉自体は、 添字式 subscript expression か添字表記法 subscript notation を短縮した Python 独自の用語かと思われます(alc 調べ)。 subscription 自体で購読とかの意味合いはあるのですが、 表記を表す意味合いは alc で調べた限りなさそうでした。

# 6.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.

# 6.3. まとめ

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

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

ジェネレータ式の詳細については、このさき、以下の記事で見ていきたいと思います。

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

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



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

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

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

# 2. map を使った改善例 1 より少し綺麗になる
[y for y in map(f, lst) if y % 2 ==0]

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

ちなみに Python 3.8 から 代入式 が使えるようになると、 次のように書けるようになります。

[y for x in lst if y:=f(x) == 0]

# 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()を使った関数表記よりも見やすくなることが分かったからである。

問題

map 関数, filter 関数とジェネレータ式で、どちらを使うべきか迷ったら、使うのはどっち?

問題

PEP 8 では lambda 式を変数に代入して 使わ無いように定めています。これはなぜですか? 下記のうち適切なものを選択してください。

  • 関数名が文字列として保存されないので、 例外が起こったときに関数名が表示されず、 わかり辛くなってしまうから。
  • 1行で定義されてしまうので、 小さく見づらく、 どこで関数が定義されているか、 わからなくなってしまうから。

# 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 ... リストの要素を  関数を元に累積  します。

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

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

The fate of reduce() in Python 3000 by Guido van van Rossum
今度は 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.

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

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

sum(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:
        if x <= y:
            x = y
    return x

max(lst)  # 5

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

# 3. Python2 では import しなくても書けた
# reduce(lambda x, y: x if x > y else y, lst)  # 120

Python では reduce を使うことなく、組み込み関数として min, max, sum を使うことができます。 よく使うものは組み込み関数に入れる、使わないものは入れないというバランス感覚も、 やはり Guido は絶妙だなと思ってしまうのです。

The fate of reduce() in Python 3000
reduce と関連のある演算子は多くはない (関連のある演算子 X というのは (a X b) X c と a X (b X c) が等しくなるようものだ)。 私は +, *, &, |, ^ などにごく限定されると思う。 Python には、すでに sum 関数がある。 もしやらねばならないのなら reduce 関数を捨てて product 関数を喜んで入れる。 +, * という reduce 関数の最も一般的な2つの使用例に対処するために。
There aren't a whole lot of associative operators. (Those are operators X for which (a X b) X c equals a X (b X c).) I think it's just about limited to +, *, &, |, ^, and shortcut and/or. We already have sum(); I'd happily trade reduce() for product(), so that takes care of the two most common uses.

上記の Guido のブログを読んでいると Guido が map, filter, reuduce を組み込み関数から絶対に外すマンになっていてちょっと面白いです。 誰にどう説得されたのかわからないのですが、map と filter は残りました。

# 9. Python ではじめる関数型プログラミング

できることなら副作用がない方が良いことを、見てきました。 では具体的に副作用のないコードを書くにはどうすればいいのでしょうか。

Qiita で JavaScript に関する記事が大炎上しているのを発見しました。

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

# for 文を使うより
total_of_even_number_under_100 = 0
for i in range(100):
    if i % 2 == 0:
        total_of_even_number_under_100 += i

print(total_of_even_number_under_100) # 2450
# filter, reduce を使った方が
# 読みやすい
#   filter と reduce が使われていたので
#   復習がてらにちょうどいいかなと思いました。
from functools import reduce
from0to100 = list(range(100))
is_even_number = lambda i: i % 2 == 0
add_all = lambda total, i: total + i
total_of_even_number_under_100 = reduce(add_all, filter(is_even_number, from0to100))

print(total_of_even_number_under_100)  # 2450

この章では、炎上した Qiita の記事に書かれたコードのリファクタリングを通して map, filter を使った関数型言語らしい、書き方について考えていきたいと思います。 ただ、僕自身は Haskell などの関数型言語でコードを書いたことが無いので、 話半分で適当に読み流していただけると嬉しいです。

変数名等も元の JavaScript のコードに準拠しました。 JavaScript のコードの解説はこちらに書きました。 Array でハマって辛かったです笑

# 9.1. 炎上した原因

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

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

map, filter は ジェネレータ式と、同じ機能を提供します。ジェネレータ式は、ジェネレータ関数の糖衣構文です。

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

Google の Python のコーディング規約には、単純な場合にだけ、ジェネレータ式を使うように示しています。 ひっくり返せば map, filter も単純な時にだけ効果的だと言うことです。

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

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

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

これらのことを踏まえると、 糖衣構文は簡単な場合にしか使えなさそうです。

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

可読性について議論された記事で長い変数名を書かれていると、 なぜか本旨と関係ないところで、炎上しやすい気がします。

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

逆どのような時なら変数名を短くしていいかを考えた時に、 既にお互いわかっていることは書かかなくても良い、ということかなと思います。 これも議論の分かれるところで炎上しやすい話だとは思うのですが。

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

それがおそらく "冗長すぎるやろ" という認識を起こしてしまったのだと思います。 この関数が、業務ロジックのような、ちゃんと読まないとわからないコードだったら話は違ったと思うのですが。 他にも例えば、グローバル変数なら長めな方が良いですし、 関数内の引数ではないローカル変数(自由変数というらしいです)なら短い方が良いかなと思います。

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

  1. from0To100Array の 100 は、n にして抽象度合いを上げて削除
  2. isEvenNumber の Number は、自明なので削除
  3. addAll の All は、All を add してないので削除
from functools import reduce
n = 100
array = list(range(n))
is_even = lambda i: i % 2 == 0
add = lambda total, i: total + i
sum_of_even_array = reduce(add, filter(is_even, array))

print(sum_of_even_array)  # 2450

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

# 9.1.3. 原因 3. コードの書き方そのものに、もしかしたら問題があるかもしれません。

さらに言えば今回の場合、無名関数を変数に代入しない方がいいかなと思いました。 複雑なら変数に代入した方がわかりやすいと思うのですが add と is_even はすこし自明過ぎます。

値のみを代入する、変数にオブジェクトを代入する処理を中心に書けば、もっといいかなと思いました。 なぜなら、どういう値が導き出されているか、見やすくなるからです。 functools, operator は 関数型プログラミング用モジュール です。 おそらく Qiita の筆者も本当はこっちが言いたかったんじゃないかなと思ってしまいます。

from functools import reduce
from operator import add
n = 100
array = list(range(n))                            # 1. リスト
even_array = filter(lambda x: x % 2 == 0, array)  # 2. 偶数のリスト
sum_of_even_array = reduce(add, even_array)       # 3. 偶数のリストの和

print(sum_of_even_array)                          # 4. 結果

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

print(sum(i for i in range(100) if i % 2 == 0))  # 2450

fileter, ジェネレータという趣旨を逸脱していいなら、これがいいですね。

print(sum(range(0, 100, 2)))  # 2450

# 9.2. 関数型プログラミングとはなにか?

# 9.2.1. 参照透過性

関数型言語は、 参照透過性 を前提にした言語です。 「参照透過性」というのは、関数に引数を与えて実行したときに必ず結果が一意に定まることを指しています。

関数型プログラミングとは Referential Transparency を徹底的に追求するプログラミングです ... 後略
megumin1 氏のブクマ

# 9.2.2. 副作用

また基本的に 副作用 を認めてくれません。 簡単に言えば、変数や属性に1度代入したら、再代入はできないということです。

オブジェクト指向でプログラミングをしているときでも、 もちろん「参照透過性」を保つことはできるのですが、 基本的にオブジェクトは「状態」を持ってしまいます。

「状態」というのは Python で言えば属性に代入されたオブジェクトのことです。 属性に代入されたオブジェクトに基づいて関数(メソッド)の動作が変わってしまうので、 クラス定義文で定義されたクラスのオブジェクトのメソッドには基本的には「参照透過性」は無いと言えます。

# 9.2.3. メモ化

ここで「参照透過性」があることがわかっていると、 一度計算したことについては、引数と返り値をセットにしておけば、 再計算をする必要がなくなるということです。 このような高速化の技法を メモ化 とか言われたりします。

標準ライブラリ functools のなかにある lru_cache は、 このメモ化の機能を提供してくれます。

# 9.2.4. 現場での採用

ちなみに完全に「参照透過性」を担保しようとすると、 「関数型言語」でも、結構大変だったりするらしいです。 「状態」を持たせないこと自体結構大変だったりするのかもしれません。

参照透過性を厳密に担保している Haskell とかとてもいい言語らしく、ぜひ触ってみたいのですが、 あまり実地で使われているという話を聞いたことがありません。

純粋関数型言語である Haskell は、とても言語として素晴らしいらしく、 自分もいつかぜひ触ってみたいと思ってはいるのですが、

現場での採用という例はあまりないようです。 副作用を絶対に認めないというのは、やはりちょっと厳しい気がします。

関数型言語でも副作用を認めてくれる Elixir などは、 あまりどれくらいの浸透度合いなのかわからないのですが、 コミュニティが盛んなのを Twitter でよく見かけます。

# 9.2.5. オブジェクト指向と相反する考えなのか?

たまにオブジェクト指向と相反するものとして、 関数型言語が言及されてたりするのを、何度か見かけた気がします。

オブジェクト指向でも参照透過性を実現することはできます。 オブジェクト指向と関数型というのは、 決して相反する概念というものではないのかなと思ったりもします。

関数型言語は、オブジェクト指向の一種で、 再代入を許さないオブジェクト指向言語だと個人的に思っています。

問題

純粋な関数型言語の特徴として、間違っているものは、どれですか?

# 9.3. まとめ

map, filter, lambda を使うことで for 文をより簡潔に書くことができる場合があります。 特に for 文のネストを外せるのが嬉しいです。

ネストは浅い方が良い。
Flat is better than nested.
PEP 20 - The Zen of Python

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

まず、「主語が大きい」という言葉は、すごく乱暴で好きではないのですが、 Google 先生が言ったりもするので、そういう判断でいいかなと思ったりします。

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

使えるシーンは限られるとは思うのですが、 計算量が悪化しても 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'>
>>> 

PyCon の動画を見つけました。引用したものの一切理解していません...

# 10. まとめ

ここまで以下のような流れで見てきました。

# 10.1. map と filter のまとめ

Hello, world!

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

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

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

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

そして最後に map, filter, ジェネレータを使って for 文を分割して書く方法を見てきました。

# 10.2. 名前空間のまとめ

名前空間からはじめ、オブジェクト指向、そして関数型に触れてきました。 名前 -> 名前空間(オブジェクト指向)-> 名前の再束縛の禁止(関数型プログラミング) とこう言った流れなのかなと個人的に思っています。

  1. アセンブラ ... 名前
  2. オブジェクト指向 ... 名前空間
  3. 関数型プログラミング ... 名前の再束縛の禁止

以上になります。誠にありがとうございました。

Last Updated: 11/11/2019, 11:17:29 PM