ユーニックス総合研究所

  • home
  • archives
  • python-map

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  

上記の例ではfuncmap()に渡す関数です。
そして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では結果としてのリストの生成のタイミングが違っています。
リスト内包表記は文が終了した段階で要素がすべて生成されていますが、mapmap 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 objectlistに渡すことでリストに変換することが可能です。

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引数として適当なものを答えよ

  1. 辞書
  2. リスト
  3. 関数

Q2: mapの第2引数として適当なものを答えよ

  1. 関数
  2. イテレーション可能なオブジェクト
  3. 辞書

Q3: リスト内包表記とmapの相違点として合っているものを答えよ

  1. リスト内包表記は事前生成するが、mapは事前生成しない
  2. リスト内包表記はリストを生成するが、mapからはリストを生成できない
  3. リスト内包表記は速く、mapは遅い

正解

Q1: 3
Q2: 2, 3
Q3: 1