Pythonのmapの動作と振る舞い: リストの各要素に計算を適用する
- 作成日: 2020-09-03
- 更新日: 2024-01-05
- カテゴリ: Python
Pythonのmapの動作
Pythonには組み込み関数にmap
があります。
これはリストのようなイテレーション可能なオブジェクトの各要素に関数を適用したいときに使われます。
map
は例えば↓のように使います。
lis = [1, 2, 3]
func = lambda el: el * 2
for el in map(func, lis):
print(el)
実行結果。
2
4
6
上記の例ではfunc
がmap()
に渡す関数です。
そしてlis
がイテレーション可能なオブジェクトでリストになります。
lis
の要素である1, 2, 3
が順にfunc()
に渡されて結果をyield
で返します。
for文で参照した場合はyield
で生成された結果が順次取り出されて、print()
で出力されます。
mapの構造
Pythonのmap
は↓のような構造を持ちます。
map(関数, イテレーション可能なオブジェクト, ...)
「関数」はラムダ式やdef
などの関数で、その引数にはリストなどの要素が渡されます。
「イテレーション可能なオブジェクト」はたとえばリストなどです。これは後ろに続く引数に複数渡すことが出来ます。
map
の戻り値はイテレーション可能なmap object
です。
このオブジェクトはfor
文に渡したり、next
に渡したりすることが出来ます。
map()
の演算の結果はyield
で生成されますので、map()
に渡した段階ではリストなどには変換されません。
map
はリストなどの先頭から要素を走査し、その要素を引数の関数に渡します。
この関数内で要素に対して処理をして結果を返すことで、リストなどに対して一括処理が可能になります。
ラムダ式とイテレーション可能なオブジェクト
リスト(list)
以下のmap
にラムダ式とリストを渡すサンプルでは、map
の第1引数にラムダ式、第2引数にリストを渡しています。
そしてmap
の戻り値をprint
で表示しています。
lis = [1, 2, 3]
func = lambda el: el * 2
result = map(func, lis)
print(list(result))
# [2, 4, 6]
タプル(tuple)
イテレーション可能なオブジェクトにはタプルも含まれます。
以下はタプルをmap()
に渡している例です。
tup = (1, 2, 3)
func = lambda el: el * 2
result = map(func, tup)
print(tuple(result))
# (2, 4, 6)
集合(set)
集合もmap()
に渡すことができます。
以下はmap()
に集合を渡しているところです。
s = {1, 2, 3}
func = lambda el: el * 2
result = map(func, s)
print(set(result))
# {2, 4, 6}
辞書(dict)
辞書も渡せます。
以下はmap()
に辞書を渡しているところです。
d = {'a': 1, 'b': 2, 'c': 3}
func = lambda el: el * 2
result = map(func, d)
print(list(result))
# ['bb', 'aa', 'cc']
辞書の場合はキーがfunc()
に渡されます。
キーは文字列ですのでそれを2倍すると上記のように2倍の長さの文字列になります。
辞書で明示的にキーや値をmap()
に渡したい場合は辞書のkeys()
とvalues()
を使います。
func = lambda el: el * 2
# キーをd.keys()で取得
d = {'a': 1, 'b': 2, 'c': 3}
result = map(func, d.keys())
print(list(result))
# ['bb', 'aa', 'cc']
# 値をd.values()で取得
d = {'a': 1, 'b': 2, 'c': 3}
result = map(func, d.values())
print(list(result))
# [2, 4, 6]
mapの評価タイミング
map
は要素を走査して関数にその要素を渡すタイミングが特徴的です。
これをわかりやすく視覚的に確認するにはnext
を使います。
lis = [1, 2, 3]
func = lambda el: el * 2
i = map(func, lis)
print(next(i)) # 2
print(next(i)) # 4
print(next(i)) # 6
next
にイテレーターが渡されたときにはじめて次の要素が関数に渡されているのがわかります。
つまり、map
はリスト内包表記のように事前に一括で要素に対して適用せずに、順次必要になったら処理を適用していくわけです。
lis = [el * 2 for el in range(4)]
print(lis)
# [0, 2, 4, 6]
i = map(lambda el: el * 2, range(4))
print(i)
# <map object at 0x000002082AE2E668>
↑のコードを見ると、リスト内包表記とmap
では結果としてのリストの生成のタイミングが違っています。
リスト内包表記は文が終了した段階で要素がすべて生成されていますが、map
はmap object
が生成された段階ではリストを生成していません。
mapの使いどころ
以上の理由からmap
は大量の要素に処理を適用したい場合に適していると言えます。
たとえばリスト内包表記で巨大なrange
を処理しようとすると時間がかかりますが、map
では順次生成するのですぐに戻り値にアクセスできます。
# こっちは生成に時間がかかるけど
lis = [el * 2 for el in range(100000000)]
# こっちはすぐに終わる
i = map(lambda el: el * 2, range(100000000))
map objectのlistへの変換
map
の戻り値であるmap object
はlist
に渡すことでリストに変換することが可能です。
i = map(lambda el: el * 2, [1, 2, 3])
lis = list(i)
print(lis)
出力結果。
[2, 4, 6]
しかし、同じイテレーターを使って再度list
に変換しようとすると空のリストになります。
これはmap
の戻り値のイテレーターがStopIteration
まで進んでしまったためです。
i = map(lambda el: el * 2, [1, 2, 3])
lis = list(i)
print(lis)
lis = list(i)
print(lis)
出力結果。
[2, 4, 6]
[]
map objectのtupleへの変換
map
の戻り値のイテレーターはlist
と同様にtuple
にも変換できます。
i = map(lambda el: el * 2, [1, 2, 3])
tup = tuple(i)
print(tup)
tup = tuple(i)
print(tup)
出力結果。
(2, 4, 6)
()
map objectをfor文で回す
map
の戻り値のイテレーターはfor
文で回すことが出来ます。
i = map(lambda el: el * 2, [1, 2, 3])
for el in i:
print(el)
出力結果。
2
4
6
複数のイテレーション可能なオブジェクトを処理する
複数のイテレーション可能なオブジェクトを処理する場合は↓のようにします。
i = map(lambda a, b: a * b, range(4), range(3, -1, -1))
print(list(i))
# [0, 2, 2, 0]
ラムダ式の引数a
にはrange(4)
の要素が渡され、引数b
にはrange(3, -1, -1)
の要素が渡されます。
range(4)
は0, 1, 2, 3
という数列を生成し、range(3, -1, -1)
は3, 2, 1, 0
という数列を生成します。
それらの要素を先頭から掛けていくので上のような結果になります。
つまり演算を簡単にすると
0 * 3 = 0
1 * 2 = 2
2 * 1 = 2
3 * 0 = 0
と言う具合で結果が生成されていきます。
複数のイテレーション可能なオブジェクトを渡した場合は関数はそれのらオブジェクトと同数の引数を取らなくてはなりません。
イテレーション可能なオブジェクトが複数の場合、そのうちで最も短いオブジェクトが使いつくされた時点で処理は止まります。
# range(4)が終わったら終わり
i = map(lambda a, b: a * b, range(4), range(10))
print(list(i))
# [0, 1, 4, 9]
itertools.starmap()
map()
の引数を事前に圧縮して渡したい場合があります。
たとえばイテレーション可能なオブジェクト(イテラブル)が複数ありそれをリストなどにまとめている場合です。
そういう時はitertools.starmap()
が使えます。
itertools.starmap() --- 効率的なループ実行のためのイテレータ生成関数 — Python 3.12.1 ドキュメント
from itertools import starmap
iterable = [(2, 3), (4, 5)]
i = starmap(lambda a, b: a * b, iterable)
print(list(i))
# [6, 20]
# ...
# 2 * 3 = 6
# 4 * 5 = 20
上記の結果を見てもわかるように、関数の引数には1つのイテラブルの要素がすべて*
でアンパックされて渡されます。
mapのCPythonの実装
GitHub - python/cpython: The Python programming language
CPythonはPythonのC言語による実装で、人気がありスタンダードです。
それでmap()
は組み込み関数になりますが、これは実装はcpython/Python/bltinmodule.c
に書かれています。
cpython/Python/bltinmodule.c at main · python/cpython · GitHub
map()
でmap object
を生成する場合はmap_new()
というC言語で書かれた関数が呼ばれます。
map_new() · python/cpython · GitHub
C言語にはクラスがなく、代わりに構造体というクラスからメソッドを取り払ったものが使われます。
C言語でオブジェクト指向的なコードを書きたい場合はmap_new()
のように関数名の最初にオブジェクトの名前を書き、それをアンダースコアで繋げてメソッド名を書きます。つまりmap_new()
のnew()
がメソッド名ですね。
こういった関数の第1引数に構造体のオブジェクトを渡すことでクラスのような振る舞いをさせるのがC言語では一般的です。
map_new()
ではmapobject
という構造体のオブジェクトに関数オブジェクト(func
)とイテレーター(iters
)
をセットしています。
map()
の引数が2個より下の場合はmap() must have at least two arguments.
というエラーが出ます。
イテレーションを進める場合はmap_next()
が呼ばれます。
map_next() at main · python/cpython · GitHub
この関数ではiters
(タプル)からイテレーターを取り出し、それをstack
という動的配列に積んでいます。
そして_PyObject_VectorcallTstate()
という関数を呼び出して処理を任せています。
使用例
map
の使用例です。
偶数と奇数のboolマップを作る
range
で数列を生成し、その数列に対して偶数と奇数のフラグを立てます。
i = map(lambda v: v % 2 == 0, range(4))
lis = list(i)
print(lis)
出力結果。
[True, False, True, False]
問題
Q1: map
の第1引数として適当なものを答えよ
- 辞書
- リスト
- 関数
Q2: map
の第2引数として適当なものを答えよ
- 関数
- イテレーション可能なオブジェクト
- 辞書
Q3: リスト内包表記とmap
の相違点として合っているものを答えよ
- リスト内包表記は事前生成するが、mapは事前生成しない
- リスト内包表記はリストを生成するが、mapからはリストを生成できない
- リスト内包表記は速く、mapは遅い
正解
Q1: 3
Q2: 2, 3
Q3: 1