【Python】内包表記がいまだにパッとわからないので整理してみた

開発ログ

はじめに

リストの内包表記って、最初ぱっと見では全然わからないですよね。 僕も競プロの問題を解いたとき、解答コードで内包表記をよく目にするのですが、「うーん、直感的にわからんな〜」となって、結局慣れている for 文で書いてしまいます。

それでまた後から解答を見て、「あ〜、内包表記で書けばよかった」と後悔する……そんなことを繰り返していました。 いい加減しっかり整理したいなと思ったので、今回まとめてみることにします。 「自分も内包表記、実は苦手なんだよなあ」という人は、ぜひ一緒に見ていってください。

リストの内包表記って?

簡単に言うと、**「リスト生成を1行で簡潔に書ける、Python特有の便利構文」**のようなものです。 例えば、こんなコードがあります。

Python

squares = [x**2 for x in range(10)]

これ、ぱっと見ても最初は「?」となりますよね。 ということで、まずは「読み方」のルールを確認してみましょう。 構造を日本語で分けてみると、以下のようになります。

[ ① 出力する値(式) ② ループ(for) ③ 条件(if) ]

処理の流れをイメージすると、こんな感じです:

  1. **②(ループ)**から要素を1つずつ取り出す
  2. その要素を**③(条件)**に基づいて判定する
  3. 条件をパスしたら、その要素を**①(式)**の形に加工して値を決定する
  4. すべての要素に対して1〜3を行い、結果をリストに詰め込む

先ほどのコード例にあてはめてみます(今回は③の条件がないパターンです)。

  1. range(10) なので0〜9の要素を1つずつ取り出す
  2. その要素を2乗する(**2 なので)
  3. 全要素に対して1〜2を行った結果を、リストに内包する

なので、squares の内容は以下のようになります。

 [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

こうして日本語で手順を整理すると、少しわかりやすくなる気がしませんか?

いつもの書き方と比べてみる

次に、内包表記を使わない場合と比べて、どれくらいコードが変わるか見てみます。 「1〜10の数字から偶数だけを取り出す」という例で比較してみましょう。

【いつもの書き方(for文)】

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

even_list = []
for num in numList:
    if num % 2 == 0:
        even_list.append(num)

【リストの内包表記】

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

even_list = [num for num in numList if num % 2 == 0]

処理の流れだけいうと

  1. numListから要素を1つずつnumとして取り出す
  2. その要素を「if num % 2 == 0」の条件を基に判定
  3. その判定が通ったらnumとして値を決定
  4. 1~3を行った結果をリストに内包

となります。

4行かかっていた処理がたった1行にまとまり、だいぶスッキリしました!
わざわざ「空のリスト」を作って、append で追加するという手間が省けるので、コードの見通しが良くなります。

内包表記が「向いてなさそうな」パターン

「1行で書けるなら全部これでいいじゃん!」となりそうですが、逆に読みにくくなるので避けた方がいいパターンもあります。

ループが重なるとき

例えばこんな3重ループ。これ読みたくないですよね(笑)。
(そもそもパフォーマンス的に3重ループ自体の回避を考えたいですが)

# 3重ループを無理やり内包表記に
result = [x * y * z for x in range(5) for y in range(5) for z in range(5) if x > 0]

この場合は、素直に for 文を重ねて書いたほうが、構造がはっきりしてましな気がします。

result = []
for x in range(5):
    for y in range(5):
        for z in range(5):
            if x > 0 and y > 0 and z > 0:
                result.append(x * y * z)

条件(if)が多すぎるとき

条件が重なると、内包表記はどんどん右に長くなっていきます。実務だと「どこの条件に引っかかって除外されたのか」が追いにくく、デバッグも大変です。

# 条件が3つ以上重なると、解読が難しい
result = [x for x in data if x > 0 if x % 2 == 0 if x < 100]

これなら、以下のように内包表記を使わず、continue を使って「当てはまらないものはスキップ!」と門前払いしていく書き方のほうが、読みやすくて親切です。
(ほかにいい書き方があると思いますが今回は置いといて…)

result = []
for x in data:
    if x <= 0: continue    # 0以下ならスキップ
    if x % 2 != 0: continue # 奇数ならスキップ
    if x >= 100: continue  # 100以上ならスキップ
    
    result.append(x)

言葉にして書き出してみたら、自分の中でも少し理解が深まった気がします。

コードがスッキリする便利な構文ですが、使いどころによっては逆に読みやすさを落としてしまうので、実務で使うならケースバイケースですね(笑)。
ただ、競プロでは頻出の書き方なので、覚えておいて損はないはずです。

読んでくださった方も、何となく「内包表記」のイメージを掴んでいただけていたらうれしいです。
ありがとうございました!

コメント

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