- Python
Python内包表記|リスト・辞書・集合の書き方と速度比較一覧
Pythonの内包表記とは、[式 for 変数 in イテラブル if 条件] の1行で、新しいリスト・辞書・集合を作る書き方です。 for文と append で4行かかる処理が1行になり、多くの場合は実行も少し速くなります。
この記事では、リスト・辞書・集合・ジェネレータ式の書き方を1枚の早見表にまとめ、迷いやすい if と if-else の位置の違い、二重ループの順番、zip・enumerate・セイウチ演算子 := との組み合わせをすべて実行結果つきで示します。さらに、for+append・map・filterとの速度を timeitでPython 3.11と3.14の両方で計測し、3.12で入った「内包表記のインライン化」がどう効くかを確かめます。最後に、ループで書いたほうが読みやすい場面の判断基準と、実際のエラー文を紹介します。
検証環境:Linux、Python 3.11.15/Python 3.14.7。
内包表記の早見表
| 種類 | 書き方 | 実行結果(nums = [1, 2, 3, 4, 5, 6]) |
|---|---|---|
| リスト | [n * 10 for n in nums] |
[10, 20, 30, 40, 50, 60] |
| リスト+絞り込み | [n for n in nums if n % 2 == 0] |
[2, 4, 6] |
| リスト+if-else | ["偶数" if n % 2 == 0 else "奇数" for n in nums] |
['奇数', '偶数', '奇数', '偶数', '奇数', '偶数'] |
| if-else+絞り込み | [n * 10 if n > 3 else n for n in nums if n != 5] |
[1, 2, 3, 40, 60] |
| 辞書 | {name: price for name, price in zip(names, prices)} |
{'ノート': 120, 'ペン': 80, '付箋': 200} |
| 辞書+絞り込み | {name: p for name, p in zip(names, prices) if p >= 100} |
{'ノート': 120, '付箋': 200} |
| 集合 | {n % 3 for n in nums} |
{0, 1, 2} |
| ジェネレータ式 | sum(n * n for n in nums) |
91 |
| タプルを作る | tuple(n for n in nums if n > 4) |
(5, 6) |
| 番号つき | [f"{i}:{name}" for i, name in enumerate(names, start=1)] |
['1:ノート', '2:ペン', '3:付箋'] |
| 二重ループ | [(r, c) for r in range(2) for c in "ab"] |
[(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')] |
| 平らにする | [x for row in matrix for x in row] |
[1, 2, 3, 4, 5, 6] |
| 行列の転置 | [[row[i] for row in matrix] for i in range(3)] |
[[1, 4], [2, 5], [3, 6]] |
| 辞書のキーと値を入れ替え | {v: k for k, v in stock.items()} |
{5: 'ノート', 0: 'ペン', 12: '付箋'} |
| 辞書を絞り込む | {k: v for k, v in stock.items() if v > 0} |
{'ノート': 5, '付箋': 12} |
| 計算結果で絞る(3.8+) | [y for x in nums if (y := x * x) > 10] |
[16, 25, 36] |
前提データ:names = ["ノート", "ペン", "付箋"]、prices = [120, 80, 200]、matrix = [[1, 2, 3], [4, 5, 6]]、stock = {"ノート": 5, "ペン": 0, "付箋": 12}。
| 覚え方 | 内容 |
|---|---|
| かっこで種類が決まる | [] リスト、{k: v} 辞書、{x} 集合、() ジェネレータ式(タプル内包表記はない) |
| if だけなら後ろ | 絞り込み(要素を減らす) |
| if-else なら前 | 値の切り替え(要素数は変わらない) |
| for は外側から順に書く | 普通の入れ子のforと同じ順番 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python内包表記|リスト・辞書・集合の書き方と速度比較一覧」
リスト内包表記の基本形
[式 for 変数 in イテラブル] は、「イテラブルから1つずつ取り出して、式の結果を集める」という意味です。 次の2つは同じリストを作ります。
# Python 3.8+
nums = [1, 2, 3, 4, 5, 6]
# for文で書く
out = []
for n in nums:
out.append(n * 10)
print(out)
# 内包表記で書く
print([n * 10 for n in nums])
実行結果:
[10, 20, 30, 40, 50, 60]
[10, 20, 30, 40, 50, 60]
文字列の整形のように、メソッドを続けて呼ぶ処理とも相性がよい書き方です。
print([w.strip().lower() for w in [" Apple", "BANANA "]])
print([c for c in "a1b2c3" if c.isdigit()])
['apple', 'banana']
['1', '2', '3']
ifとif-elseの位置の違い
if だけ(絞り込み)は for の後ろ、if-else(値の切り替え)は for の前に書きます。 前に書く A if 条件 else B は「条件式(三項演算子)」という1つの式なので、式を書く場所に置きます。
flowchart LR
A["[ 式 ]"] --> B["for 変数 in イテラブル"] --> C["if 条件"]
A -.- D["式の部分に A if 条件 else B を書ける"]
C -.- E["ここは絞り込み専用(else は書けない)"]
| やりたいこと | 書き方 | 要素数 |
|---|---|---|
| 条件に合うものだけ残す | [n for n in nums if n > 3] |
減る |
| 条件で値を変える | [n if n > 3 else 0 for n in nums] |
同じ |
| 両方 | [n * 10 if n > 3 else n for n in nums if n != 5] |
減る |
| 条件を2つ以上で絞る | [n for n in nums if n > 1 if n < 5](and と同じ) |
減る |
位置を取り違えたときのエラー
[n for n in range(5) if n % 2 == 0 else 0]
File "<string>", line 1
[n for n in range(5) if n % 2 == 0 else 0]
^^^^
SyntaxError: invalid syntax
[n if n > 2 for n in range(5)]
SyntaxError: expected 'else' after 'if' expression
どちらも3.11・3.14で同じメッセージでした。後ろの if に else は書けず、前の if には else が必要、と覚えておくと迷いません。
辞書内包表記と集合内包表記
辞書内包表記は {キー: 値 for ...}、集合内包表記は {値 for ...} です。 コロンの有無で区別されます。
# Python 3.8+
names = ["ノート", "ペン", "付箋"]
prices = [120, 80, 200]
stock = {"ノート": 5, "ペン": 0, "付箋": 12}
print({name: price for name, price in zip(names, prices)})
print({k: v for k, v in stock.items() if v > 0})
print({v: k for k, v in stock.items()})
print({k: sorted(v) for k, v in {"a": [3, 1], "b": [2]}.items()})
print({len(name) for name in names})
実行結果(Python 3.14.7):
{'ノート': 120, 'ペン': 80, '付箋': 200}
{'ノート': 5, '付箋': 12}
{5: 'ノート', 0: 'ペン', 12: '付箋'}
{'a': [1, 3], 'b': [2]}
{2, 3}
| やりたいこと | 内包表記 | 内包表記以外の書き方 |
|---|---|---|
| 2つのリストから辞書 | {k: v for k, v in zip(ks, vs)} |
dict(zip(ks, vs))(変換しないならこちらが短い) |
| 全キーに同じ初期値 | {k: 0 for k in ks} |
dict.fromkeys(ks, 0) |
| 値を変換 | {k: v * 1.1 for k, v in d.items()} |
— |
| 重複を除いた値の一覧 | {x.lower() for x in words} |
set(map(str.lower, words)) |
dict.fromkeys(ks, []) のように変更できる値を初期値にすると、全キーが同じリストを共有します。 キーごとに別のリストが欲しいときは {k: [] for k in ks} と内包表記で書きます。辞書そのものの操作は、別記事「Python辞書(dict)の使い方」で詳しく扱っています。
ジェネレータ式
ジェネレータ式は (式 for ...) で、値を1つずつ作りながら渡す書き方です。 リストを丸ごと作らないため、sum・max・any・"".join などに渡すときにメモリを節約できます。
import sys
a = [n for n in range(1000)]
g = (n for n in range(1000))
print(sys.getsizeof(a), sys.getsizeof(g))
print(sum(n * n for n in range(1, 7))) # 関数の唯一の引数ならかっこを省略できる
print(any(n > 5 for n in [1, 3, 7]))
実行結果(Python 3.14.7):
8856 200
91
True
1000要素のリストは8,856バイト、ジェネレータ式は要素数に関係なく200バイトでした(sys.getsizeof はオブジェクト本体の大きさ)。ジェネレータの詳しい仕組みは、別記事「Pythonジェネレータとyield」で解説しています。
二重ループ・zip・enumerate・セイウチ演算子
二重ループは「外側のforから順に」書く
内包表記の for は、普通の入れ子のfor文を上から順に1行に並べた順番です。
matrix = [[1, 2, 3], [4, 5, 6]]
# for文
out = []
for row in matrix: # 外側
for x in row: # 内側
out.append(x)
# 内包表記(同じ順番で並べる)
print([x for row in matrix for x in row])
[1, 2, 3, 4, 5, 6]
順番を逆にすると、まだ定義されていない変数を参照することになります。
[x for x in row for row in [[1, 2], [3, 4]]]
NameError: name 'row' is not defined. Did you mean: 'pow'?
「内包表記の中に内包表記」([[...] for ...])は、結果も入れ子のリストになります。転置の例 [[row[i] for row in matrix] for i in range(3)] は、外側の for i が先に回り、内側のリストを1つずつ作ります。
zip・enumerate と組み合わせる
names = ["ノート", "ペン", "付箋"]
prices = [120, 80, 200]
print([f"{n}={p}円" for n, p in zip(names, prices)])
print([f"{i}:{n}" for i, n in enumerate(names, start=1)])
print({n: i for i, n in enumerate(names)})
['ノート=120円', 'ペン=80円', '付箋=200円']
['1:ノート', '2:ペン', '3:付箋']
{'ノート': 0, 'ペン': 1, '付箋': 2}
セイウチ演算子 := で計算を1回にする(3.8+)
(y := 式) で、条件判定に使った計算結果をそのまま値として使えます。 重い関数を2回呼ばずに済みます。
nums = [1, 2, 3, 4, 5, 6]
print([y for x in nums if (y := x * x) > 10])
print([(last := n) for n in range(3)], last)
[16, 25, 36]
[0, 1, 2] 2
2行目のとおり、:= で代入した変数は内包表記の外側に残ります(ループ変数とは扱いが違います)。合計を途中経過として残したいときなどに使えますが、読み手が驚きやすいので必要な場面に絞るとよいでしょう。
for・map・filterとの速度比較(3.11と3.14)
10万件ではリスト内包表記がfor+appendより約1割速く、map・filterにlambdaを渡す書き方より約4割速くなりました。 次のコードで、各関数を交互に8巡実行し、最小値をとりました。
# bench.py(Python 3.8+)
import sys, timeit
def loop_append(data):
out = []
for x in data:
out.append(x * 2)
return out
def map_lambda(data):
return list(map(lambda x: x * 2, data))
def listcomp(data):
return [x * 2 for x in data]
def loop_filter(data):
out = []
for x in data:
if x % 2 == 0:
out.append(x)
return out
def filter_lambda(data):
return list(filter(lambda x: x % 2 == 0, data))
def listcomp_if(data):
return [x for x in data if x % 2 == 0]
def loop_dict(data):
out = {}
for x in data:
out[x] = x * 2
return out
def dictcomp(data):
return {x: x * 2 for x in data}
def map_str(data):
return list(map(str, data))
def listcomp_str(data):
return [str(x) for x in data]
big = list(range(100_000))
small = list(range(10))
funcs = [loop_append, map_lambda, listcomp, loop_filter, filter_lambda,
listcomp_if, loop_dict, dictcomp, map_str, listcomp_str]
res = {f.__name__: [float("inf"), float("inf")] for f in funcs}
for _ in range(8):
for f in funcs:
b = min(timeit.repeat(lambda: f(big), number=20, repeat=3)) / 20
s = min(timeit.repeat(lambda: f(small), number=50_000, repeat=3)) / 50_000
res[f.__name__] = [min(res[f.__name__][0], b), min(res[f.__name__][1], s)]
print("Python", sys.version.split()[0])
for name, (b, s) in res.items():
print(f"{name:<14} 10万件 {b*1e3:6.2f} ms 10件 {s*1e9:6.0f} ns")
計測結果(他の処理とCPUを共有する環境のため、数%のぶれがあります):
| 処理 | 書き方 | 10万件 3.11 | 10万件 3.14 | 10件 3.11 | 10件 3.14 |
|---|---|---|---|---|---|
| 2倍する | for+append | 2.83 ms | 3.03 ms | 269 ns | 262 ns |
| map+lambda | 4.25 ms | 4.34 ms | 541 ns | 535 ns | |
| リスト内包表記 | 2.57 ms | 2.69 ms | 302 ns | 231 ns | |
| 偶数だけ | for+if+append | 2.68 ms | 2.67 ms | 360 ns | 343 ns |
| filter+lambda | 4.23 ms | 3.97 ms | 660 ns | 599 ns | |
| 内包表記+if | 2.36 ms | 2.56 ms | 393 ns | 317 ns | |
| 辞書を作る | for で代入 | 4.73 ms | 4.75 ms | 399 ns | 322 ns |
| 辞書内包表記 | 4.77 ms | 4.61 ms | 464 ns | 317 ns | |
| 文字列に変換 | map(str, …) | 6.77 ms | 5.69 ms | 648 ns | 597 ns |
[str(x) for x in …] |
6.26 ms | 6.06 ms | 626 ns | 547 ns |
表から読み取れるポイントです。
- lambdaを使うなら内包表記のほうが速い:map・filterは要素ごとにlambda(Python関数)を呼ぶためです。
- 組み込み関数をそのまま渡すならmapも互角:
map(str, data)は3.14では内包表記より速い結果でした。 - 10万件での差は1割前後:速度だけを理由に書き換えるほどの差ではなく、読みやすさで選んでよい範囲です。
- 3.11では10件のような小さいリストで内包表記のほうが遅い:3.11までは内包表記を呼ぶたびに内部で関数を1つ作って呼んでいたためです。
3.12のインライン化(PEP 709)で小さいリストが速くなった
Python 3.12から、リスト・辞書・集合の内包表記は関数として作られず、その場に展開(インライン化)されるようになりました(PEP 709)。 バイトコードで確かめます。
# inline.py
import dis, sys
def f(data):
return [x * 2 for x in data]
names = [i.opname for i in dis.get_instructions(f)]
print(sys.version.split()[0], "MAKE_FUNCTION" in names, "LOAD_FAST_AND_CLEAR" in names)
3.11.15 True False
3.14.7 False True
3.11は内包表記のために MAKE_FUNCTION(関数を作る命令)を使い、3.14では使っていません。その結果、上の表の 10件のケースで、内包表記は3.11の302 nsから3.14では231 nsに短縮され、for+append(262 ns)を上回りました。ジェネレータ式はインライン化の対象外です。
インライン化はエラー表示にも表れます。3.11のトレースバックには内包表記用の in <listcomp> という行が出ていましたが、3.14では関数 g の行だけになります(g() を呼んだ行とファイルのパスは省略)。
# Python 3.11.15
File "inline.py", line 7, in g
return [1 / x for x in [1, 0]]
^^^^^^^^^^^^^^^^^^^^^^^
File "inline.py", line 7, in <listcomp>
return [1 / x for x in [1, 0]]
~~^~~
ZeroDivisionError: division by zero
# Python 3.14.7
File "inline.py", line 7, in g
return [1 / x for x in [1, 0]]
~~^~~
ZeroDivisionError: division by zero
スコープ:ループ変数は外に漏れない
内包表記のループ変数は内包表記の中だけで有効で、外側の同じ名前の変数を上書きしません。 for文とは違う点です。インライン化された3.12以降も、この動作は変わりません。
x = "外側"
squares = [x * 2 for x in range(3)]
print("内包表記のあと x =", x)
for x in range(3):
pass
print("forループのあと x =", x)
g = [y for y in range(3)]
try:
print(y)
except NameError as e:
print("NameError:", e)
実行結果(3.11.15・3.14.7とも同じ):
内包表記のあと x = 外側
forループのあと x = 2
NameError: name 'y' is not defined
クラスの中ではクラス変数が見えない
クラス本体に書いた内包表記からは、for の最初のイテラブル以外の場所でクラス変数を参照できません。
class Shelf:
RATE = 2
items = [1, 2, 3]
try:
doubled = [x * RATE for x in items]
except NameError as e:
print("NameError:", e)
NameError: name 'RATE' is not defined
3.11・3.14とも同じ結果です(items は最初のイテラブルなので読めます)。メソッドの中で計算するか、最初のイテラブルにまとめて渡すと動きます。
from itertools import repeat
class Shelf:
RATE = 2
items = [1, 2, 3]
doubled = [x * r for x, r in zip(items, repeat(RATE))] # zip(...) はクラスの中で評価される
print(Shelf.doubled)
[2, 4, 6]
なお、[x * r for r in [RATE] for x in items] のように2つ目の for でクラス変数を読むと、今度は NameError: name 'items' is not defined になりました(3.11・3.14とも)。
内包表記にするか、ループで書くかの判断基準
「1行で読めて、副作用がない」なら内包表記、それ以外はfor文を目安にすると読みやすいコードになります。
| 状況 | おすすめ | 理由 |
|---|---|---|
| 変換・絞り込みをして新しいリストを作る | 内包表記 | 意図が1行で伝わる |
for が3つ以上、または条件が3つ以上 |
for文 | 読む順番を追いにくくなる |
print・ファイル書き込み・DB更新が目的 |
for文 | 内包表記は「値を作る」ための書き方 |
| try/exceptが必要 | for文、または変換を関数に切り出す | 内包表記の中にtryは書けない |
途中で break したい |
for文、または next(...)・any(...) |
内包表記にbreakはない |
| 合計・最大・存在確認 | ジェネレータ式を sum・max・any に渡す |
リストを作らずに済む |
| 結果を1回だけ順に使う大量データ | ジェネレータ式 | メモリを節約できる |
読みにくくなる例と書き直し
# 直す前:条件と変換が混ざって読みにくい
orders = [{"id": 1, "qty": 3, "status": "ok"}, {"id": 2, "qty": 0, "status": "ok"},
{"id": 3, "qty": 5, "status": "hold"}]
result = [o["id"] * 100 + (o["qty"] if o["qty"] > 0 else -1) for o in orders
if o["status"] == "ok" if o["id"] != 99]
print(result)
# 直した後:変換を関数に切り出して、内包表記は「何を集めるか」だけにする
def order_code(o):
qty = o["qty"] if o["qty"] > 0 else -1
return o["id"] * 100 + qty
def is_target(o):
return o["status"] == "ok" and o["id"] != 99
print([order_code(o) for o in orders if is_target(o)])
実行結果(どちらも同じ):
[103, 199]
副作用だけが目的の内包表記(例:[print(x) for x in items])は、使わない None のリストが作られます。表示や書き込みはfor文で書きましょう。
よくあるエラーとつまずき
ジェネレータ式は1回しか使えない
ジェネレータ式は一度最後まで読むと空になります。 2回目はエラーにならず、空の結果が返る点に気をつけます。
gen = (n * 2 for n in range(3))
print("1回目:", list(gen))
print("2回目:", list(gen))
print("sum:", sum(gen))
1回目: [0, 2, 4]
2回目: []
sum: 0
何度も使うならリスト内包表記にするか、使うたびにジェネレータ式を作り直します。また、ジェネレータ式には len() が使えません(TypeError: object of type 'generator' has no len())。
エラー文の早見表
| 書いたコード | エラー文(3.11・3.14共通) | 直し方 |
|---|---|---|
[n for n in r if c else 0] |
SyntaxError: invalid syntax |
if-elseは前へ:[n if c else 0 for n in r] |
[n if n > 2 for n in r] |
SyntaxError: expected 'else' after 'if' expression |
elseを足すか、ifを後ろへ |
[x, x * 2 for x in r] |
SyntaxError: did you forget parentheses around the comprehension target? |
タプルはかっこで:[(x, x * 2) for x in r] |
[x for x in row for row in m] |
NameError: name 'row' is not defined |
外側のforを先に書く |
{k: v for k, v in [1, 2]} |
TypeError: cannot unpack non-iterable int object |
要素が2つ組か確認(d.items() の付け忘れなど) |
len(n for n in r) |
TypeError: object of type 'generator' has no len() |
sum(1 for ...) か、リストにする |
辞書のキーが重なると後の値で上書きされる
print({len(w): w for w in ["ペン", "ノート", "付箋"]})
{2: '付箋', 3: 'ノート'}
同じキーが2回出ると、後に出た値が残ります。 すべて残したいときは、for文と setdefault や collections.defaultdict(list) でグループにまとめます。
よくある質問
Q. タプル内包表記はありますか?
A. ありません。(x for x in ...) はジェネレータ式になります。タプルが欲しいときは tuple(x for x in ...) と書きます。
Q. 内包表記はfor文より必ず速いですか? A. 今回の計測では10万件で約1割速い程度で、3.11の小さいリストではfor+appendのほうが速い結果でした。速さより「1行で意図が伝わるか」で選ぶのがおすすめです。
Q. 内包表記の中でtry/exceptは使えますか? A. 書けません。例外処理を含む変換を関数に切り出し、内包表記からその関数を呼ぶと、読みやすさも保てます。
Q. 内包表記をデバッグしにくいです。
A. いったんfor文に展開して print を入れるか、式の部分を関数にしてその中で確認します。3.12以降はトレースバックに内包表記の行がそのまま表示されるため、エラー位置は追いやすくなっています。
まとめ
- かっこで種類が決まる:
[]リスト、{k: v}辞書、{x}集合、()ジェネレータ式。 - if だけなら後ろ(絞り込み)、if-else なら前(値の切り替え)。
- 二重ループは外側のforから順に書く。順番を逆にすると
NameErrorになります。 - 速度は10万件でfor+appendより約1割、map+lambdaより約4割速い結果でした。
- 3.12のインライン化で、小さいリストの内包表記が速くなりました(10件で302 ns→231 ns)。
- ループ変数は外に漏れない。ただし
:=で代入した変数は外に残ります。 - 3つ以上のforや副作用が目的の処理は、for文や関数への切り出しで読みやすくします。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python内包表記|リスト・辞書・集合の書き方と速度比較一覧」
株式会社RJCでは、Pythonの読みやすい書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。