pythonのmapって何なの?


結論

メモリを考えるほど大量の処理をしなければ、高階関数として存在することに意味がありそう。
基本的には

x, y = map(int, ["1", "2"])

x, y = [int(one) for one in ["1", "2"]]

のように書き換えられるように、内包表記で代替できる。

これは何?

pythonを3年ぐらい業務で使っていても、自分からmapを使おうとは思わなかった。
それぐらいmapの長所(・短所)がわかっていなかったので、mapについて色々調べてみた。

私が唯一使っていたmap

一時期競技プログラミングをやっていた(3ヶ月ぐらいだが)時に

1 2

上記の入力値に対して、x = 1, y = 2 として受け取りたい時に

x, y = map(int, input().split())

としていたぐらい。

これをよくよく見てみるとinput().split()としているので

x, y = map(int, ["1", "2"])

としていることと同義であり、なんとなくやりたいことはわかる。
だがなんとなくだとわからないのでよく調べ、よく理解してみる。

pythonのmapって何よ?

公式ドキュメント[^1]を引用すると

map(function, iterable, ...)
function を、結果を返しながら iterable の全ての要素に適用するイテレータを返します。追加の iterable 引数が渡されたなら、 function はその数だけの引数を取らなければならず、全てのイテラブルから並行して取られた要素に適用されます。複数のイテラブルが与えられたら、このイテレータはその中の最短のイテラブルが尽きた時点で止まります。関数の入力がすでに引数タプルに配置されている場合は、 itertools.starmap() を参照してください。

まあこれも、すごくなんとなくはわかるが、しっかりと理解したいので

  • iterator
  • iterable

について調べてみる。

iterator

公式ドキュメント[^2]を見てみると

iterator
(イテレータ) データの流れを表現するオブジェクトです。イテレータの __next__() メソッドを繰り返し呼び出す (または組み込み関数 next() に渡す) と、流れの中の要素を一つずつ返します。データがなくなると、代わりに StopIteration 例外を送出します。その時点で、イテレータオブジェクトは尽きており、それ以降は __next__() を何度呼んでも StopIteration を送出します。イテレータは、そのイテレータオブジェクト自体を返す __iter__() メソッドを実装しなければならないので、イテレータは他の iterable を受理するほとんどの場所で利用できます。はっきりとした例外は複数の反復を行うようなコードです。 (list のような) コンテナオブジェクトは、自身を iter() 関数にオブジェクトに渡したり for ループ内で使うたびに、新たな未使用のイテレータを生成します。これをイテレータで行おうとすると、前回のイテレーションで使用済みの同じイテレータオブジェクトを単純に返すため、空のコンテナのようになってしまします。

となっており、特に

(list のような) コンテナオブジェクトは、(中略) for ループ内で使うたびに、新たな未使用のイテレータを生成します。

この文はわかりやすい気がする。

for i in range(3):
    print(i)

これは

for_loop_test.py
test_iterator = iter(range(3))
i = next(test_iterator)
print(i)
i = next(test_iterator)
print(i)
i = next(test_iterator)
print(i)

これをやっている、ということだろう。

せっかくなので、上記の公式ドキュメントの全文を理解してみる。


(イテレータ) データの流れを表現するオブジェクトです。

ふむ。


イテレータの __next__() メソッドを繰り返し呼び出す (または組み込み関数 next() に渡す) と、流れの中の要素を一つずつ返します。

これは上記(for_loop_test.py)でやったことと同じことを言っている。
__next__()の場合もやってみると

for_loop_test2.py
test_iterator = iter(range(3))
i = test_iterator.__next__()
print(i)
i = test_iterator.__next__()
print(i)
i = test_iterator.__next__()
print(i)

ということ。


データがなくなると、代わりに StopIteration 例外を送出します。その時点で、イテレータオブジェクトは尽きており、それ以降は __next__() を何度呼んでも StopIteration を送出します。

for_loop_test3.py
test_iterator = iter(range(3))
i = test_iterator.__next__()
print(i)
i = test_iterator.__next__()
print(i)
i = test_iterator.__next__()
print(i)
i = test_iterator.__next__()
print(i)

0
1
2
StopIteration:

確かに。


イテレータは、そのイテレータオブジェクト自体を返す __iter__() メソッドを実装しなければならないので、イテレータは他の iterable を受理するほとんどの場所で利用できます。はっきりとした例外は複数の反復を行うようなコードです。

この文章自体はあまりよくわからなったが参考文献を読むと、
複数回参照して反復させたい時は適していないよ(一回使ったらメモリから消えるから)、ぐらいの意味だと思った。

一応原文

Iterators are required to have an __iter__() method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted. One notable exception is code which attempts multiple iteration passes.

code which attempts multiple iteration passes と言っているので、その理解で良さそう


(list のような) コンテナオブジェクトは、自身を iter() 関数にオブジェクトに渡したり for ループ内で使うたびに、新たな未使用のイテレータを生成します。これをイテレータで行おうとすると、前回のイテレーションで使用済みの同じイテレータオブジェクトを単純に返すため、空のコンテナのようになってしまします。

これまでの理解で十分理解可能な文。

iterable

ここまで来るとわかる。
iter(a) とできるaのことだ。

iter([1,2])
# <list_iterator at 0x7fcd7d736490>
iter({1,2})
# <set_iterator at 0x7fcd7c043c40>
iter((1,2))
# <tuple_iterator at 0x7fcd7d736eb0>
iter("12")
# <str_iterator at 0x7fcd7d736250>
iter(1)
# TypeError: 'int' object is not iterable

これを見ればわかるように、list, set, tuple, strはiterable。
intはnot iterable。


図示してみたが、iterator→iterable objectへの変換について、str以外はちゃんと動きそうだった。

本題のmap

function を、結果を返しながら iterable の全ての要素に適用するイテレータを返します。

x, y = map(int, ["1", "2"])

この例を考える。
iterableの全ての要素は、"1"と"2"であり、functionはintであるから
int("1")とint("2")がこの順で入っているiteratorが右辺になる。

問題は左辺。
iteratorってこんな感じで値取り出せるの!?ってなる。
(ここまで書いて、これしか知らないのがmapの理解度を大幅に下げている要因だとはっきりわかった)
つまり

x, y = [int(one) for one in ["1", "2"]]

これとやっていることは全く同じであったというわけだ。


追加の iterable 引数が渡されたなら、 function はその数だけの引数を取らなければならず、全てのイテラブルから並行して取られた要素に適用されます。複数のイテラブルが与えられたら、このイテレータはその中の最短のイテラブルが尽きた時点で止まります。

zip() と同じ仕様だということ。


関数の入力がすでに引数タプルに配置されている場合は、 itertools.starmap() を参照してください。

これは余力があれば調べよう。(→次回の記事に含まれる予定)

mapの使い所は?

結局色々調べても全部内包表記で代替できると思ってしまった。
(メモリが小さいとかはあると思いますが、すごい量の処理をしなければわざわざ使うメリットはなさそう)
だがhttps://qiita.com/matsui2019/items/81bfec06798ab572c7ae
この記事を読んで、高階関数(関数を引数、戻り値として扱う関数[^3])として存在する、ということ自体に意味があるのかなと思った。
もしそうでなければコメントしていただけると幸いです。

参考文献

[^1]: https://docs.python.org/ja/3/library/functions.html#map
[^2]: https://docs.python.org/ja/3/glossary.html#term-iterator
[^3]: https://qiita.com/may88seiji/items/8f7e42353b6904af5e9a