303
260

More than 3 years have passed since last update.

【Python】mapの正しい使い方

Last updated at Posted at 2020-03-04

はじめに

初めてmapに触れたとき、「定義はこんな感じかな?」と思うでしょう。

map(function, list)

しかし正しくはこうです。

map(callable, *iterable)

この真意をmap(function, list)を整形していきながらわかりやすく解説します。

整形途中の定義には?マークを付けます

? map(function, list)

学びたての認識

listの各要素に対して一定の処理を行ったモノを使いたい場合はmap関数を使います。戻り値はmapオブジェクトというもので、一種のイテレータです。イテレータとは、for文で回すことができるオブジェクトのことです。mapオブジェクトはlistではありません。

>>> iterable = map(int, ["1", "2", "3", "4", "5"])
>>> for i in iterable:
...    print(i)
... 
1
2
3
4
5

定義はこんな感じでしょうか?

? map(function, list)

いいえ、実は第二引数はiterable(イテレータになれるオブジェクト)を受け取ります。つまり、for文で回せるようなものならなんでも引数として受け取ることができます。もちろんイテレータ自身もiterableです。

? map(function, iterable)

イテレータになれるオブジェクト(iterable)を受け取る

あまり気にする必要はありませんが、iterableとイテレータは若干違います。

next関数を使って次の要素を取りだすことができるのがイテレータで、iter関数でイテレータに変換できるのがiterableです。例えばiterableなlistオブジェクトは、next関数で先頭の要素を取り出すことはできませんが、iter関数でイテレータ(list_iterator)に変換できます。そして、list_iteratorはnext関数を使って次の要素を取り出すことができます。

a = [100, 200, 300]
# next(a) <- エラー!
a_iter = iter(a)
next(a_iter) # 100が取り出される

iterableなオブジェクトはlist以外にもいろいろあります。以下は例です。

  • tuple
  • dict
  • str
  • ジェネレータ式(イテレータ)
  • mapオブジェクト(イテレータ)
  • fileオブジェクト(イテレータ)

tuple

tupleはfor文で回せますのでmap関数の第二引数に指定することができます。

>>> list(map(int,  ("1", "2", "3", "4", "5")))
[1, 2, 3, 4, 5]

dict

dictはfor文で回すとキーが取り出されます。ですのでdictをmap関数の第二引数に指定すると各キーを用いて処理します。

>>> list(map(lambda k: k+k, {"a": 2, "b": 3, "c": 5, "d": 7}))
['aa', 'bb', 'cc', 'dd']

キーと値の両方取り出したい場合はdictのitemsメソッドを使います。その際、mapメソッドの第一引数に渡した関数には(キー, 値)のtuple(xとします)が渡されますので、キーを使う場合はx[0]、値を使う場合はx[1]とする必要があります。

>>> list(map(lambda x: x[0]*x[1], {"a": 2, "b": 3, "c": 5, "d": 7}.items()))
['aa', 'bbb', 'ccccc', 'ddddddd']

str

strもfor文で回せます。

>>> "".join(map(lambda c: f'{c}"', 'どうしてだよぉお'))
'ど"う"し"て"だ"よ"ぉ"お"'

ジェネレータ式

ジェネレータ式もイテレータですがそれに対してmapを使うのは冗長で可読性も悪いので、全てジェネレータ式で記述したほうがいいです。

微妙
m = map(lambda x: 2*x, (i*i for i in range(9)))
ヨシ
g = (2*i*i for i in range(9)) 

mapオブジェクト

mapオブジェクトもイテレータですので、map関数の第二引数に指定することができます。ジェネレータ式同様この方法は冗長かもしれませんが、可読性はいいかもしれません。

m = map(bin, map(int, ["20","19","12","25"]))

以下のようにも書けます。(性能にそれほど差はありません)

m = map(lambda x: bin(int(x)), ["20","19","12","25"])

工夫すれば見やすくなるかもしれません。

チェーン的な工夫
m = map(int, ["20","19","12","25"])
m = map(bin, m)
l = list(m)

mapオブジェクトはfor文で回されるような処理が走るときに初めて各要素に関数を実行する(遅延評価)ので工夫したコードは、for文が一周だけ回っているというイメージを持つことができます。例えば、上記の例だとlistを作る際にfor文を回している(※)のでそのときに初めてintとbinが実行されます。以下のようなイメージを持っていただけると良いでしょう(※)
(※ listは実際にはCライブラリを使っているのでこのイメージコードの性能は悪いです)

m = map(int, ["20","19","12","25"])
m = map(bin, m)
l = list(m)

# 上記のlist(m)は以下のようなイメージ
l = []
for x in ["20","19","12","25"]:
    int_x = int(x)
    bin_x = bin(int_x)
    l.append(bin_x)

リスト内包表記を使う際に上記のようなチェーン的な工夫をしてしまうと、無駄にfor文が回っているという解釈になる(実際には微妙に違うので言葉を濁しています)ので気を付けましょう。

# 二周for文が回ってしまう。
l = [int(x) for x in ["20","19","12","25"]]
l = [bin(x) for x in l]

ジェネレータ式にすればmapと同様for文が一周だけ回るイメージになります。

g = (int(x) for x in ["20","19","12","25"])
g = (bin(x) for x in g)
l = list(g)

fileオブジェクト

open関数で生成されるfileオブジェクトもイテレータです。for文で回すと一行ずつ文字列として取り出されます。

script.txt
どうも
どうも
ご、ご趣味は?
LISPを少々...
り...?
>>> print(*map(lambda line: f"「{line.strip()}」", open("script.txt")), sep="\n")
どうも
どうも
ご趣味は?」
LISPを少々...
...?」

その他さまざまなiterableに適用できます。

関数

? map(function, iterable)

独自関数

functionにあたる第一引数には、defキーワードで定義した関数も指定することが可能です。

>>> def f(x):
...    a = x
...    # ...すごい処理
...    # ...
...    return a
>>> list(map(f, [1,2,3,4,5,6]))
[43248956, 613134354, 6435432, 543575356, 45457623, 243543566]

複数の引数を受け取る関数

mapの第一引数には、複数の引数を受け取る関数も指定することができます。
その場合はmapに渡すiterableがその分増えます

? map(N個引数のあるfunction, iterable1, iterable2, ..., iterableN)

例えば以下のような関数、listについて

def f(first, second, third):
   ...

iterable1 = [100, 200, 300, 400]
iterable2 = ["a", "b", "c", "d"]
iterable3 = ["A", "B", "C", "D"]

以下のようにmap関数を使った場合の呼び出しは下記表のようになります。

map(f, iterable1, iterable2, iterable3)
処理の順番 iterable1 iterable2 iterable3 関数fの呼び出し
0 100 "a" "A" f(100, "a", "A")
1 200 "b" "B" f(200, "b", "B")
2 300 "c" "C" f(300, "c", "C")
3 400 "d" "D" f(400, "d", "D")

任意の数のiterableを受け取るので、mapの第二引数以降は可変長引数です。ですので正しくはこういう記述になります。

? map(function, *iterable)

実際に使う例としてはoperatorモジュールのaddmulがあります。

addは「+」を関数化したものです。

>>> add(2, 3)
5

mulは「*」を関数化したものです。

>>> mul(4, 5)
20

mul引数を二つ受け取る関数ですので、map関数を使う場合はiterableを二つ用意します。

>>> from operator import mul
>>> list(map(mul, [1,2,3,4,5], [5,4,3,2,1]))
[5, 8, 9, 8, 5]

内積もスッキリ記述できます。

>>> sum(map(mul, [1,2,3,4,5], [5,4,3,2,1]))
35

関数だけじゃない

第一引数に渡せるのは関数だけではありませんcallableなオブジェクトならなんでも大丈夫です。callableなオブジェクトとは「かっこ()」で呼び出せるものです。

? map(callable, *iterable)

関数の他の代表的な例はクラスです。定義したクラス自体もcallableなオブジェクトなのでmapの第一引数に指定することができます。

class Man:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"Man('{self.name}')"

print(*map(Man, ["ジョン", "ひでお", "ミッチェル", "チャタライ"]))
# Man('ジョン') Man('ひでお') Man('ミッチェル') Man('チャタライ')

もちろん、コンストラクタで複数の引数を受け取る場合は、その分のiterableを指定すれば良いです。

これで完成です。

まとめ

map関数の定義は以下のようになっています。

map(callable, *iterable)

第一引数には、一つ以上の引数を持つcallableなオブジェクト(関数やクラス)

第二引数以降には、第一引数のcallableが求める引数の個数ぶん、iterableなオブジェクト(listなどイテレータになれるオブジェクト)を指定します

303
260
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
303
260