はじめに
Pythonのコードを読み始めたとき、こういう行に何度も出くわした。
result = [x * 2 for x in nums if x % 2 == 0]
PHPしか書いてこなかった自分には最初「なんだこの呪文は」だった。
でも慣れたら手放せなくなった。array_map()とarray_filter()をネストして書いていたものが1行で読みやすく書ける。今では積極的に使っている。
リスト内包表記
基本形はこう。
[式 for 変数 in イテラブル]
PHPのarray_map()に相当する。
<?php
$nums = [1, 2, 3, 4, 5];
// array_mapで2倍
$doubled = array_map(fn($x) => $x * 2, $nums);
nums = [1, 2, 3, 4, 5]
# 内包表記で2倍
doubled = [x * 2 for x in nums]
print(doubled) # [2, 4, 6, 8, 10]
条件フィルターつき
[式 for 変数 in イテラブル if 条件]
PHPのarray_filter()に相当する。
<?php
// 偶数だけ取り出す
$evens = array_filter($nums, fn($x) => $x % 2 === 0);
# 偶数だけ取り出す
evens = [x for x in nums if x % 2 == 0]
print(evens) # [2, 4]
map + filterを同時にやる
PHPだとarray_map()とarray_filter()を組み合わせるとネストが深くなる。
<?php
// 偶数だけ取り出して2倍
$result = array_map(
fn($x) => $x * 2,
array_filter($nums, fn($x) => $x % 2 === 0)
);
# 内包表記なら1行
result = [x * 2 for x in nums if x % 2 == 0]
print(result) # [4, 8]
読む順番が「何を → どこから → どれだけ」と自然な語順になっているのが内包表記のいいところ。
辞書内包表記
辞書も内包表記で作れる。
{キー式: 値式 for 変数 in イテラブル}
fruits = ["apple", "banana", "cherry"]
# 文字列とその長さの辞書を作る
length_map = {fruit: len(fruit) for fruit in fruits}
print(length_map)
# {'apple': 5, 'banana': 6, 'cherry': 6}
PHPだとarray_combine()やarray_reduce()、あるいはforeachでループして詰める。
<?php
$fruits = ["apple", "banana", "cherry"];
$length_map = [];
foreach ($fruits as $fruit) {
$length_map[$fruit] = strlen($fruit);
}
辞書のキーと値を入れ替える
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}
これをPHPでやるとarray_flip()だが、条件つきで入れ替えたい場合はPythonの辞書内包表記のほうが柔軟に書ける。
条件フィルターつき
scores = {"田中": 85, "鈴木": 42, "佐藤": 91, "伊藤": 38}
# 60点以上だけ取り出す
passing = {name: score for name, score in scores.items() if score >= 60}
print(passing) # {'田中': 85, '佐藤': 91}
セット内包表記
words = ["apple", "banana", "apple", "cherry", "banana"]
# 重複を除いた単語の文字数セット
unique_lengths = {len(word) for word in words}
print(unique_lengths) # {5, 6}
波括弧を使うが辞書内包表記と違いキー: 値形式ではなく値だけを書く。使う頻度はリスト・辞書と比べて低いが、知っておくと便利。
ネストした内包表記
2重ループも内包表記で書ける。
# 2次元リストをフラットにする
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
読む順番はforを左から右に読んでいく。
# 外側のループ → 内側のループ
[x for row in matrix for x in row]
# ↑ 外 ↑ 内
九九の表を作る
kuku = [[i * j for j in range(1, 10)] for i in range(1, 10)]
print(kuku[2]) # [3, 6, 9, 12, 15, 18, 21, 24, 27](3の段)
ただしネストが深くなると一気に読みにくくなる。2重ネストまでを限度にしておくのが個人的な基準。3重以上は素直にforループを使う。
ジェネレーター式
内包表記に似ているが、[]の代わりに()で囲む。
# リスト内包表記 → 全部メモリに展開する
squares_list = [x ** 2 for x in range(1000000)]
# ジェネレーター式 → 必要なときだけ計算する
squares_gen = (x ** 2 for x in range(1000000))
ジェネレーター式はリストを一気に作らず、要素を1つずつ遅延評価する。大量のデータを扱うとき、リスト内包表記だとメモリを大量消費してしまう。
# sum()やmax()に直接渡せる(リストを作らない)
total = sum(x ** 2 for x in range(1000000))
print(total)
データ処理案件では大きいファイルや大量レコードを扱う場面が出てくるので、この違いは意識しておきたい。
内包表記を使うべきでないケース
便利だからといって何でも内包表記にするのは良くない。
# 読みにくい内包表記(やりすぎ)
result = [
{"name": user["name"], "score": user["score"] * 1.1}
for user in users
if user["active"]
if user["score"] >= 60
if user["role"] != "admin"
]
条件が3つ以上になったり、式が複雑になったりしたら素直にforループで書く。
# こっちのほうが読みやすい
result = []
for user in users:
if not user["active"]:
continue
if user["score"] < 60:
continue
if user["role"] == "admin":
continue
result.append({
"name": user["name"],
"score": user["score"] * 1.1,
})
内包表記は「シンプルなmapとfilterの組み合わせ」に留めるのが読みやすさのラインだと思っている。
まとめ
| 種類 | 構文 | PHPの対応 |
|---|---|---|
| リスト内包表記 | [式 for x in ...] |
array_map() |
| フィルターつき | [式 for x in ... if 条件] |
array_filter() + array_map()
|
| 辞書内包表記 | {k: v for x in ...} |
foreachでループして詰める |
| セット内包表記 | {式 for x in ...} |
array_unique() + array_map()
|
| ジェネレーター式 | (式 for x in ...) |
相当なし(yieldは近い) |
内包表記に慣れるとarray_map(array_filter(...))のネストが懐かしくなってくる。最初の「読めない」という壁を超えれば、PHPより自然に書けると感じる部分の一つ。