Pythonのmap関数で、イテレート可能なオブジェクトに関数を適用

map list

Pythonのmap関数の使い方です。
「map関数」の使い方は簡単ですが、説明に使われる用語が難しいです。
「map関数」まわりの用語解説を中心にまとめてみます。

なお、実行環境は、Python 3系です。
map関数は、Python 2系と仕様が変わっていますので、ご注意ください。

map関数: map(function, iterable, …)

map関数は、Pythonの組み込み関数です。
第一引数に「任意の関数」、第二引数以降に「イテレート可能なオブジェクト」を取ります。

組み込み関数: すぐに使える関数(ライブラリをインポートする「import 〇〇」が不要)
function: 任意の関数
iterable: イテレート可能なオブジェクト(≒配列)

戻り値は、配列の各要素に対して、任意の関数を実行した結果を要素とするmapオブジェクトです。
配列のように各要素がバラバラになったデータではなく、各要素を取り出す必要がある塊になったデータ(mapオブジェクト)として返ってきます。

これは、使用例のコードで確認します。

「イテレート可能なオブジェクト」とは

「イテレート可能なオブジェクト」には、以下のようなものがあります。

オブジェクト
文字列(str)‘12345’
リスト(list)[1, 2, 3, 4, 5]
タプル(tuple)(1, 2, 3, 4, 5)
辞書(dict){‘A’: 1, ‘B’: 2}

「イテレート」(iterate)とは、繰り返し処理することです。
「イテレート可能」(iterable)とは、オブジェクトから順に要素が取り出せることです。

具体例として、for文を使って、オブジェクトから順に要素を取り出してみます。

# 文字列
for i in '12345':
    print(i)
1
2
3
4
5

文字列「’12345’」から、順に各要素が取り出せました。

# リスト
for i in [1, 2, 3, 4, 5]:
    print(i)
1
2
3
4
5

リスト「 [1, 2, 3, 4, 5] 」から、順に各要素が取り出せました。
リストは、各要素の区切り「,」がはっきりしているので、わかりやすいです。

これで、オブジェクトから順に要素が取り出せること「イテレート可能」のイメージが掴めたと思います。

「イテレータ」とは

「イテレータ」とは、連続する一連のデータへのアクセスを提供するオブジェクトです。
map関数の戻り値「mapオブジェクト」は、イテレータです。

イテレート可能なオブジェクトを、イテレータに変換してみます。

# 文字列をイテレータに変換
itr = iter('12345')
itr
<str_iterator at 0x1e764e09860>

文字列「’12345’」が、イテレータに変換されました。

イテレータのままでは、塊になったデータなので、各要素を取り出せません。
next関数を使用して、各要素を取り出します。

next(itr)
'1'

1つ目の要素が取り出せました。
next関数を続けて使用すると、順に次の要素が取り出せます。

最後の要素「’5’」を取り出すと、イテレータ「itr」は空になります。
更に、next関数を使用すると、エラーが返ります。

---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-217-6693cc261707> in <module>
----> 1 next(itr)

StopIteration: 


イテレータの特徴は、連続する一連のデータを順に1回だけ取り出せるところです。

「イテレート可能なオブジェクト」の具体例で、for文を使用しました。
for文のinで使用した文字列やリストは、for文の実行時に「イテレータ」に変換されます。
ループ毎に、イテレータから順に要素が取り出され、イテレータが空になるとループが終了します。

次は、 list関数を使用して、各要素を取り出します。
list関数では、すべての要素が順に取り出されます。

# 文字列をイテレータに変換
itr = iter('12345')

# list関数で、各要素を取り出す
list(itr)
['1', '2', '3', '4', '5']

list関数で、イテレータ「itr」からすべての要素を取り出しました。
イテレータ「itr」は空になっているので、再度取り出すことはできません。

# list関数で、空のイテレータから各要素を取り出す
list(itr)
[]

空のリストが返ってきました。

イテレータからデータを取り出せるのは、1回だけだからです。

これで、「イテレータ(mapオブジェクト)」から順に要素を取り出すイメージが掴めたと思います。
イテレータは、次の要素だけを1回だけ取り出せるオブジェクトです。
リストのように要素を自由に取り出すことができませんが、メモリが節約できるメリットがあります。

map関数の使用例

map関数の使い方です。
map関数は、2つ以上の引数をとります。map(function, iterable, …)
第一引数が「任意の関数」、第二引数以降が「イテレート可能なオブジェクト」です。
第三引数があるときは、第一引数に、2つの引数を取る関数を指定します。

第二引数以降:「イテレート可能なオブジェクト」

map関数の第二引数以降は、 「イテレート可能なオブジェクト」 を指定します。

文字列(str)

第一引数に指定する関数は、組み込み関数「int」です。
組み込み関数「int」 は、文字列を整数に変換します。

string = '56789'
number = map(int, string)
number
<map at 0x1e764dfe978>

第二引数に指定した文字列「’56789’」から順に各要素を取り出し、整数に変換します。
戻り値はイテレータの「mapオブジェクト」です。

リスト関数を使用して、各要素を取り出します。

list(number)
[5, 6, 7, 8, 9]


次は、第一引数に、組み込み関数「str」を指定します。
組み込み関数「str」 は、引数に指定したオブジェクトを文字列に変換します。

string = '56789'
string_split = map(str, string)
list(string_split)
['5', '6', '7', '8', '9']

リスト(list)

第一引数に指定する関数は、組み込み関数「len」です。
組み込み関数「len」 は、引数に指定した文字列の文字数が取得できます。

list_str = ['A', 'Aa', 'Aaa', 'Aaaa', 'Aaaaa']
length = map(len, list_str)
list(length)
[1, 2, 3, 4, 5]

第二引数に指定したリストから、順に各要素を取り出し、文字数を取得しました。

これで、「イテレート可能なオブジェクト」に「任意の関数」を実行するイメージが掴めたと思います。

第一引数:「任意の関数」

map関数の第一引数は、 「任意の関数」 を指定します。
関数の種類は、以下のようなものがあります。
・組み込み関数
・lambda式(無名関数)
・def文で定義した関数

lambda式やdef文で定義した関数を使用する具体例として、長方形の面積を求めてみます。

lambda式 (無名関数)

vertical = [3, 4, 5, 6, 7]
horizontal = [5, 4, 3, 2, 1]

area = map(lambda v, h: v * h, vertical, horizontal)
list(area)
[15, 16, 15, 12, 7]

第二引数と、第三引数に指定したリストから、順に各要素を取り出し、lambda式で面積を求めました。

lambda式の解説は、以下をご覧ください。
【Python入門】lambda(無名関数、ラムダ式)がわからない

def文で定義した関数

vertical = [3, 4, 5, 6, 7]
horizontal = [5, 4, 3, 2, 1]

# 面積を求める関数を定義
def square(h, w):
    return h * w

area = map(square, vertical, horizontal)
list(area)
[15, 16, 15, 12, 7]

第二引数と、第三引数に指定したリストから、順に各要素を取り出し、def文で定義した関数を使用して面積を求めました 。

def文で定義した関数を使用すると、後日にコードを確認する際の可読性が低下します。
(独自に定義したsquare関数を確認しないと、コードが理解できない。)
map関数には、lambda式を使用したほうが良いでしょう。

もちろん、if文を組み合わせるような複雑な関数を定義するときは、無理やりlambda式を使用するより、def文で定義することをおすすめします。

map関数の書き換え例

長方形の面積を求める例を書き換えてみます。

for文

vertical = [3, 4, 5, 6, 7]
horizontal = [5, 4, 3, 2, 1]

# 空のリストを用意
area = []
# リストに追加するメソッド
area_add = area.append

for v, h in zip(vertical, horizontal):
    area_add(v * h)

print(area)
[15, 16, 15, 12, 7]

長方形の面積は、イテレータではなく、リストとして取得しています。
空のリストを用意するのと、各要素の計算結果をリストに追加する処理が必要になります。
当然のように、処理は遅くなります。

余談ですが、for文が遅くなる理由に、append属性の参照があります。
以下のように、ループの中でappend属性を参照しない方が良いです。

# ループの中でappend
for v, h in zip(vertical, horizontal):    
    area.append(v * h)

リスト内包表記

vertical = [3, 4, 5, 6, 7]
horizontal = [5, 4, 3, 2, 1]

area = [v * h for v, h in zip(vertical, horizontal)]

print(area)
[15, 16, 15, 12, 7]

長方形の面積は、イテレータではなく、リストとして取得しています。
リスト内包表記の基本構文は、以下のようになります。
リスト内包表記: [function(引数) for 引数 in iterable]

リスト内包表記は、一般的にはmap関数やfor文より処理が速いです。

ジェネレータ式

戻り値として、イテレータの方が良いとき(データが大きく、メモリを節約したいときなど)は、ジェネレータ式を使用します。
リスト内包表記の角括弧「[]」を、丸括弧「()」にしたものです。

vertical = [3, 4, 5, 6, 7]
horizontal = [5, 4, 3, 2, 1]

area = (v * h for v, h in zip(vertical, horizontal))
area
<generator object <genexpr> at 0x000001E764DEBA20>

長方形の面積は、リストではなく、ジェネレータイテレータとして取得しています。
mapオブジェクトと同じように、リスト関数を適用して要素を取り出します。
(全要素をメモリにのせたら、イテレータにした意味がありませんが、あしからず。)

list(area)
[15, 16, 15, 12, 7]

まとめ

  • map関数まわりの用語(イテレートなど)が理解できた
  • map関数で、イテレート可能なオブジェクトに、同じ処理をかけることができた
  • map関数の戻り値が、イテレータ(mapオブジェクト)であることが理解できた
  • map関数と同じ処理を、リスト内包表記やジェネレータ式で記述できた

早くメモリの消費が意識できるレベルになりたいです。

関連情報

タイトルとURLをコピーしました