リスト内包表記 vs map() vs for文…… 速度比較してみた

リスト内包表記 vs map() vs for文…… 速度比較してみた

Pythonでデータを処理するとき、「リストの各要素に同じ処理を適用したい」という場面は非常に多いです。

このとき、書き方は主に3通りあります。

方法 特徴 一言で言うと
for 最も基本的で直感的 「一つずつ順番に処理する」
リスト内包表記 Pythonらしい簡潔な書き方 「一行でリストを作る」
map() 関数型プログラミングのスタイル 「関数を全要素に適用する」

今回は、それぞれの書き方の違いと、実際の速度を比較してみましょう。

for文:最も基本的なループ処理

 for文とは?

for文は、リストなどの「繰り返し可能なオブジェクト(イテラブル)」の要素を、一つずつ順番に取り出して処理するための構文です。

「イテラブル」という言葉が出てきましたが、簡単に言えば「for文で回せるもの」のことです。リスト、タプル、文字列、辞書などがイテラブルに該当します。

 

 基本的な動きを確認する

まずは、for文がどのように動くのか確認してみましょう。

以下のコードでは、リストの各要素を2倍にした新しいリストを作ります。

# 元のリスト(これを加工したい)
numbers = [1, 2, 3, 4, 5]

# 結果を格納するための空のリストを用意
doubled = []

# リストの各要素を順番に処理
for number in numbers:
    doubled.append(number * 2)  # 2倍にして追加

print(doubled)

 

以下、実行結果です。

[2, 4, 6, 8, 10]

 

元のリストの各要素が2倍になっていることが確認できます。

 

 処理の流れを可視化する

for文の内部で何が起きているのか、もう少し詳しく見てみましょう。

以下のコードでは、各ステップで何が行われているかを表示します。

numbers = [1, 2, 3, 4, 5]
doubled = []

for i, number in enumerate(numbers):
    result = number * 2
    doubled.append(result)
    print(f"ステップ{i+1}: {number}×2={result} → {doubled}")

 

以下、実行結果です。

ステップ1: 1×2=2 → [2]
ステップ2: 2×2=4 → [2, 4]
ステップ3: 3×2=6 → [2, 4, 6]
ステップ4: 4×2=8 → [2, 4, 6, 8]
ステップ5: 5×2=10 → [2, 4, 6, 8, 10]

 

1ステップずつ処理が進む様子が見えます。

 

 for文のメリット・デメリット

for文のメリットは、最も直感的で理解しやすいこと、複雑な処理も書きやすいこと、デバッグがしやすいことです。

一方、デメリットとしては、コードが長くなりがち(最低3行必要)で、「空リスト用意→append」というパターンを毎回書く必要があることが挙げられます。

 

リスト内包表記:Pythonらしい簡潔な書き方

 リスト内包表記とは?

リスト内包表記(List Comprehension)は、リストを生成するためのPython独自の簡潔な書き方です。

for文で3行以上かかる処理を、たった1行で書けるのが特徴です。

 

 基本構文を理解する

リスト内包表記の基本構文は [式 for 変数 in イテラブル] です。

これを日本語で読むと、「イテラブルの各要素を変数に入れて、式を評価した結果のリストを作る」となります。

実際にfor文と同じ処理を書いてみましょう。

numbers = [1, 2, 3, 4, 5]

# リスト内包表記
doubled = [number * 2 for number in numbers]

print(doubled)

 

以下、実行結果です。

[2, 4, 6, 8, 10]

 

for文と同じ結果が、たった1行で得られました。

この簡潔さがリスト内包表記の最大の魅力です。

 

 条件付きリスト内包表記

リスト内包表記には条件(if)を追加することもできます。

これにより、特定の条件を満たす要素だけを処理できます。以下のコードでは、偶数だけを2倍にしています。

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 偶数だけを2倍にする(if条件を追加)
doubled_evens = [n * 2 for n in numbers if n % 2 == 0]

print(doubled_evens)

 

以下、実行結果です。

[4, 8, 12, 16, 20]

 

n % 2 == 0 は「2で割った余りが0」、つまり偶数を意味します。

このように、フィルタリングと変換を1行で書けるのがリスト内包表記の強力な点です。

データ分析で「特定の条件のデータだけを抽出して加工する」といった場面で活躍します。

 

map():関数を全要素に適用する

 map()とは?

map() は、リストなどの全要素に対して、指定した関数を適用するための組み込み関数です。

「関数型プログラミング」というプログラミングスタイルに由来する機能で、「データの変換」を表現するのに適しています。

 

 基本的な使い方

map()の基本構文は map(関数, イテラブル)です。

まず変換処理を関数として定義し、それをmap()に渡します。

numbers = [1, 2, 3, 4, 5]

# 2倍にする関数を定義
def double(x):
    return x * 2

# map()で全要素に関数を適用
doubled = list(map(double, numbers))

print(doubled)

 

以下、実行結果です。

[2, 4, 6, 8, 10]

 

重要なポイントは、map()はそのままでは「mapオブジェクト」を返すため、list() で囲んでリストに変換する必要があることです。

 

 lambda式との組み合わせ

「2倍にするだけの関数」をわざわざ定義するのは面倒ですよね。

そこで活躍するのが lambda(ラムダ)式 です。

lambda式は「名前のない小さな関数」を1行で書くための構文で、lambda 引数: 式 という形式で書きます。

numbers = [1, 2, 3, 4, 5]

# lambda式を使うと関数定義が不要
doubled = list(map(lambda x: x * 2, numbers))

print(doubled)

 

以下、実行結果です。

[2, 4, 6, 8, 10]

 

lambda x: x * 2 は「引数xを受け取って、x * 2を返す」という小さな関数を表しています。

ただし、lambda式を多用するとコードが読みにくくなることもあるので、使いどころを見極めることが大切です。

 

 組み込み関数との組み合わせ

map()は、Pythonに最初から用意されている「組み込み関数」と組み合わせると特に便利です。

以下は、文字列のリストを数値のリストに変換する例です。

# 文字列になっている数字
str_numbers = ["1", "2", "3", "4", "5"]

# int関数を全要素に適用して整数に変換
int_numbers = list(map(int, str_numbers))

print(int_numbers)
print(type(int_numbers[0]))  # 型を確認

 

以下、実行結果です。

[1, 2, 3, 4, 5]
<class 'int'>

 

int は文字列を整数に変換する組み込み関数です。

lambda式を書かずに関数名だけを渡せるので、非常にシンプルです。

 

速度比較

 計測の準備

ここからが本題です。3つの方法で同じ処理を行い、実際の処理時間を比較します。

まず、計測条件を確認しましょう。

# 計測条件
DATA_SIZE = 100000000  # 1億個のデータ
REPEAT = 10  # 10回繰り返す

# 処理対象のデータ
data = list(range(DATA_SIZE))

 

1億個という大きなデータを使うことで、3つの方法の速度差が明確に現れます。

今回は、各要素を2乗するという処理を実施します。

 

 計測するためのtimeitモジュールの使い方

Pythonには処理時間を計測するための専用モジュール timeit があります。手動で時間を計測するよりも正確で、使い方も簡単です。

import timeit

# 計測条件
DATA_SIZE = 100000000  # 1億個のデータ
REPEAT = 10  # 10回繰り返す

# 計測対象のコードを文字列で渡す
# setup: 事前準備(計測対象外)
# number: 実行回数
time_for = timeit.timeit(
    'result = []\nfor x in data: result.append(x**2)', # 計測対象のコード
    setup = f'data = list(range({DATA_SIZE}))', # 事前準備(計測対象外)
    number=REPEAT # 実行回数
)

# 計測結果を表示
print(f"for文: {time_for:.3f}秒")

 

以下、実行結果です。

for文: 55.106秒

 

 3つの方法を一度に比較する

それでは、3つの方法すべてを計測してみましょう。以下のコードをそのまま実行できます。

import timeit

# 計測条件
DATA_SIZE = 100000000  # 1億個のデータ
REPEAT = 10  # 10回繰り返す

# 共通の事前準備(計測対象外)
setup = f'data = list(range({DATA_SIZE}))'

# for文
t_for = timeit.timeit(
    'r = []\nfor x in data: r.append(x**2)',
    setup=setup, 
    number=REPEAT
)

# リスト内包表記
t_comp = timeit.timeit(
    '[x**2 for x in data]',
    setup=setup, 
    number=REPEAT
)

# map()
t_map = timeit.timeit(
    'list(map(lambda x: x**2, data))',
    setup=setup, 
    number=REPEAT
)

print(f"for文:       \t{t_for:.3f}秒")
print(f"リスト内包表記:\t{t_comp:.3f}秒")
print(f"map():       \t{t_map:.3f}秒")

 

以下、実行結果です。

for文:       	55.226秒
リスト内包表記:	52.700秒
map():       	78.657秒

 

この結果から、リスト内包表記が最も速いことがわかります。

 

まとめ

for文は最も直感的で柔軟ですが、コードは長くなりがちです。

map()は関数適用を明確に表現できますが、可読性や速度面で注意が必要です。

速度と読みやすさの両立を考えると、リスト内包表記が最も実用的と言えるでしょう。