はじめに
Python の「リスト内包表記」を知っていますか?
numbers = [i for i in range(5)] # [0, 1, 2, 3, 4]
みたいなやつです。
これの何がいいのか、そしてどう活用したらいいかについての自論を述べてみようと思います。
あくまで個人的な自論・一例です。参考程度にお願いします。
リスト内包表記は何がいいのか?
これは主に三つです。
- 実行速度が早いから
- 1行で書けるから
- 可読性が高いから
実行速度が早い
多くの場合、リスト内包表記はfor文よりだいぶ早いです。
(と言うより、Pythonはfor文が遅いです。インタプリタ言語の繰り返し処理は遅くなりがちです。)
Pythonの繰り返し処理の高速化では、
- map などの高階関数を使う
- pypy などの実行環境を使う
- numpy などの外部ライブラリを使う
などもありますが、今回はリスト内包表記についてのみ紹介します。
numpy を紹介しないのはピューリズムではないかとのご指摘を受け、追加しました。
numpy が問題なく使える環境下では、numpy の方が早い & 扱いやすいことは事実ですし、 numpy は互換性の高いライブラリなので、積極的に活用すべきです。
ただし、「数値以外の処理」「歪な多次元配列」「様々な型の要素を含む場合」などはリスト内包表記が優位な時もあると考えます。
なぜ早いかは以下のリンクの記事等をみていただけるといいと思いますが、一言で言うと、「内部的に最適化されたC言語によるループを使っているから」でいいと思います。
コードを短くできる
これに関しては一目瞭然です。
簡単にしすぎて例として不適切だったので変更しました。
for文の例
numbers = []
for i in range(1, 10):
if i % 2 == 0:
numbers.append(i**2)
高階関数mapの例
numbers = list(map(lambda i: i**2, filter(lambda i: i % 2 == 0, range(1, 10))))
リスト内包表記の例
numbers = [i**2 for i in range(1, 10) if i % 2 == 0]
リスト内包表記の方が、短いし、シンプルですよね?
可読性が高いから
(ネストしすぎなければ、)基本的に処理の内容がすぐにわかることが多いので、可読性も高いと言われています。
(慣れるまでは読みづらいですが。。)
リスト内包表記の活用
単純な演算
numbers = [(i+1)**2 for i in range(10)]
print(numbers) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
配列の配列
a = [[(i+1)**(j+1) for j in range(5)] for i in range(5)]
print(a) # [[1, 1, 1, 1, 1], [2, 4, 8, 16, 32], [3, 9, 27, 81, 243], [4, 16, 64, 256, 1024], [5, 25, 125, 625, 3125]]
全く同じ配列からなる配列を作りたいとき
a = [[0] for i in range(5)] # [[0], [0], [0], [0], [0]]
ちなみに、以下のコードの方が短くて良いように思いますが。。
b = [[0]]*5 # [[0], [0], [0], [0], [0]]
b の方は同じオブジェクトへの「参照」をコピーしているので、全ての配列を書き換えてしまいます。
a[0][0] = 1
b[0][0] = 1
print(a) # [[1], [0], [0], [0], [0]]
print(b) # [[1], [1], [1], [1], [1]]
以下のように、 プリミティブ イミュータブルな値を扱うときに使いましょう。
(Python には厳密にはプリミティブはありませんでした。訂正します。)
c = [3]*5 # [3, 3, 3, 3, 3]
c[0] = 5
print(c) # [5, 3, 3, 3, 3]
条件による抽出
even_numbers = [x for x in range(1, 11) if x % 2 == 0] # [2, 4, 6, 8, 10]
条件に応じて処理を分岐
numbers = [x if x % 2 == 0 else x**2 for x in range(1, 11)]
# [1, 2, 9, 4, 25, 6, 49, 8, 81, 10]
複数リストの内容の組み合わせ
list1 = [1, 2, 3, 4]
list2 = [5, 6, 7, 8]
combined = [(a, b) for a in list1 for b in list2]
# [(1, 5), (1, 6), (1, 7), (1, 8), (2, 5), (2, 6), (2, 7), (2, 8), (3, 5), (3, 6), (3, 7), (3, 8), (4, 5), (4, 6), (4, 7), (4, 8)]
独自関数の適用
def collatz(num):
if num % 2 == 0:
return num // 2
return num * 3 + 1
numbers = [collatz(x) for x in range(1, 20)]
# [4, 1, 10, 2, 16, 3, 22, 4, 28, 5, 34, 6, 40, 7, 46, 8, 52, 9, 58]
2次元配列の平坦化
two_d_list = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat_list = [item for sublist in two_d_list for item in sublist]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
2次元配列の扱いについて
上記のコードでは、以下のfor文を前から追加していくと言う考え方がわかりやすいです。
flat_list = []
for sublist in two_d_list: # 最初のループ
for item in sublist: # 二番目のループ(ネストされたループ)
flat_list.append(item)
- item だけ先に入れる
- 最初のループである
for sublist in two_d_list
を入れる - 次のループである
for item in sublist
を入れる
3次元配列だと
three_d_list = [[[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]]
flat_list = [item for sublist in three_d_list for inner_list in sublist for item in inner_list]
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
やっていることは一緒ですね。
文字列操作
(numpyなどではやりづらい例の一つとして追記しました。)
words = ["apple", "banana", "cherry"]
capitalized = [word.capitalize() for word in words]
おわりに
リスト内包表記で何ができるのかのイメージが湧いたり、皆さんにとっての備忘録になれば幸いです。
各処理の説明が少ないので、気になったものは調べてもらえるとさらにわかりやすいと思います。
ぜひ、皆さんならではのリスト内包表記の使い方がありましたら、コメントしていただけますと幸いです!!