内包表記 — 1行のループ
リスト・辞書・セット内包表記、フィルタリング用ifと選択用if/elseの違い、そして内包表記が不適切なとき。
どうやって学びますか?
解決する問題
この6章で、あるコードの形が何度も現れました:
marks = [72, 45, 90, 33]
doubled = []
for mark in marks:
doubled.append(mark * 2)
print(doubled)
doubled = [mark * 2 for mark in marks]
print(doubled)[144, 90, 180, 66]
[144, 90, 180, 66]上の4行と下の1行は、まったく同じことをしています。
その4行のうち3行は本質的な仕事ではなく、雑務です — 空のリストを作り、元のデータをたどり、毎回追加する。本当の判断はmark * 2だけ。それ以外は毎回同じです。
内包表記は、この繰り返しを取り除きます。新しい機能ではありません — 今まで書けていたものを、より少ない言葉で書けるようにしただけです。その限界もそこから来ており、それがこの章の最後のテーマです:短くして分かりやすくなるものだけを短くし、何でもかんでも短くしてはいけません。
この章を終える頃には
- リスト内包表記を書き、読める
ifでフィルタリングし、if/elseで選択できる- 辞書内包表記とセット内包表記を書ける
- 内包表記が不適切なときを判断できる
- 読めない内包表記をループに戻せる
前提条件: 入れ子のデータ — 辞書のリスト.
形
[ result for item in source ]ここでresultは作りたいもの、itemは1周ごとの1つ、sourceはものが来るリスト(および他のコレクション)です。
声に出して読めば、ふつうの文章です:"`marks`の中のそれぞれの`mark`について、`mark 2`"*。
覚え方のひとつ:forの部分を先に読み、次に先頭を読む。 どこから来るか分かれば、何が作られるかは見やすくなります。
フィルタリング
末尾のifは、条件に合うものだけを残します:
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)[72, 45, 90]ここでは先頭がただのmarkです — 計算なし、そのまま採用。フィルタリングと変換は一緒にできます:
names = [" rafi ", "AHMED", " dia"]
clean = [name.strip().lower() for name in names if name.strip()]
print(clean)['rafi', 'ahmed', 'dia']条件はname.strip()だけで、比較はありません。第8章の真理値のルールを思い出してください:空の文字列は偽。 スペースだけの入力は除かれます。
末尾のif 対 先頭のif
別物です。位置が違います:
marks = [72, 33, 90]
labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)['pass', 'fail', 'pass']末尾のifはフィルタリング — 合うものだけが結果に届くので、結果は短くなりえます。
先頭のif ... elseは選択 — すべての要素が何かを生むので、長さは変わりません。第15章の1行の条件式が、内包表記の中に入ったものです。
長さが変わるか? それが、この2つを区別するいちばん簡単な質問です。
辞書とセットも同じ
同じ形、括弧が違うだけ:
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
passed = {name: mark for name, mark in marks.items() if mark >= 50}
print(passed)
doubled = {name: mark * 2 for name, mark in marks.items()}
print(doubled){'rafi': 72, 'bilal': 90}
{'rafi': 144, 'ahmed': 90, 'bilal': 180}波括弧とコロンで辞書内包表記になります。marks.items()が毎周タプルを返し、name, markがそれをアンパックします — ループとまったく同じです。
コロンなしの波括弧ならセットになります:
orders = [
{"item": "pen"},
{"item": "bag"},
{"item": "pen"},
]
items = {order["item"] for order in orders}
print(sorted(items))['bag', 'pen']第16章の「何種類か」が1行で。penは2回来ても1回だけ残ります。
数を文字列に — すでに見たもの
この行は第13章で、「説明はあとで」と約束されて登場しました:
marks = [72, 45, 90]
print(", ".join(str(m) for m in marks))
print([str(m) for m in marks])72, 45, 90
['72', '45', '90']今なら完全に読めます。str(m) for m in marksは、角括弧のない内包表記 — これはジェネレータ式と呼ばれ、リストを一度に作る代わりに、joinに1つずつ渡します。関数の唯一の引数なら括弧を省略でき、大きなデータではメモリを節約します。
書かないほうがいいとき
書けるからといって、書くべきではありません。これを見てください:
rows = [["1", "2"], ["3", "x"]]
numbers = [int(c) for row in rows for c in row if c.isdigit()]
print(numbers)[1, 2, 3]この行は正しいです。でもforが2つありifもあり、順序を覚えなければなりません:forは外側から内側へ読みます、入れ子のループを書く順序と同じです。半年後、この行を読み解くのは苦労します。
ループなら:
rows = [["1", "2"], ["3", "x"]]
numbers = []
for row in rows:
for c in row:
if c.isdigit():
numbers.append(int(c))
print(numbers)[1, 2, 3]行数は増えますが、考えることは減ります。コードの目的は短くなることではなく、分かりやすくなること — たいてい短くすれば分かりやすくなりますが、いつもではありません。
実用的なルール:forが1つとifがせいぜい1つ — 内包表記。それ以上 — ループ。 そして決して内包表記の中にprintやappendを入れないこと;内包表記は値を作るためのもので、仕事をするためのものではありません。
完全な例
views.py:
# One list of records in, three different views out
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
{"customer": "rafi", "item": "ink", "quantity": 2, "price": 120.0},
{"customer": "dia", "item": "pen", "quantity": 5, "price": 15.0},
]
totals = [order["quantity"] * order["price"] for order in orders]
print("Totals :", totals)
big = [order["item"] for order in orders if order["quantity"] * order["price"] > 100]
print("Over 100 :", big)
items = {order["item"] for order in orders}
print("Distinct :", sorted(items))
by_item = {order["item"]: order["price"] for order in orders}
print("Price list:", by_item)
print("Grand :", sum(totals))Totals : [45.0, 850.0, 240.0, 75.0]
Over 100 : ['bag', 'ink']
Distinct : ['bag', 'ink', 'pen']
Price list: {'pen': 15.0, 'bag': 850.0, 'ink': 120.0}
Grand : 1210.0第17章と同じデータですが、あそこでループだったものがここではほぼ1行です。
3つ、気づく価値があります。
totalsとsum(totals)は別々の行。 合計をsum(order["quantity"] * order["price"] for order in orders)と1行にすることもできましたが、そうすると各行の値が手元に残りません。中間の名前を持つことは、1行のトリックより価値があることがよくあります。
bigの中で同じ計算が2回出ている — 条件の中と、結果に使うはずのところ。ここでは結果が名前だけを取るので損はありません;でも両方が必要なときは、一度計算して名前を付けられるループに戻るいい理由になります。
by_itemがレコードを1つ失っている。 注文は4つ、結果は3組 — penが2回来て、第15章のルールで後勝ちで静かに置き換わりました。ここでは両方のpenが同じ価格なので害はありません;価格が違えば静かに間違います。内包表記がその問題を作るわけではありませんが、短いせいで気づく余地が減ります。
うまくいかないとき
末尾にifとelseを置いてSyntaxError [m for m in marks if m >= 40 else 0]は無効です。フィルタリング用のifは末尾に置きelseを持ちません;選択用のif ... elseは先頭に置きます:[m if m >= 40 else 0 for m in marks]。
結果がNoneだらけ 内包表記の中の何かが何も返していません — たとえば[items.append(x) for x in other]。appendはNoneを返すので(第12章)、結果はNoneのリストになります。内包表記は値を作るもの;仕事をするなら、ふつうのループを書きます。
NameError — 内包表記の変数が外で使えない [m * 2 for m in marks]のあとでmは使えません。第11章ではforループの変数はループのあとも生きていました;内包表記の変数は生きません — 中だけで生きます。
辞書内包表記の組み合わせが期待より少ない 同じキーが複数回作られ、後のものが前のものを置き換えました。len()で比較しましょう。
読めない それだけで理由になります。ループに分解しましょう — 速度の差は初心者にはほぼ関係ありませんし、次に読む人(たぶんあなた自身)が感謝します。
ステップ 4 / 6 — 予想する
理解度をチェック
末尾にifがあります。何が表示されるでしょう?
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)- A[72, 45, 90, 33]
- B[True, True, True, False]
- C[33]
- D[72, 45, 90]
今度はifが先頭にあり、elseもついています。何が表示されるでしょう?
marks = [72, 33, 90]
labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)- A['pass', 'fail', 'pass']
- B['pass', 'pass']
- C[72, 90]
- D['pass', 'fail']
内包表記の中でappendが呼ばれています。2行は何を表示するでしょう?
source = ["pen", "bag"]
target = []
result = [target.append(item) for item in source]
print(target)
print(result)- A['pen', 'bag'] [None, None]
- BA `TypeError`
- C['pen', 'bag'] ['pen', 'bag']
- D[] ['pen', 'bag']
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
clean.pyというファイルを書き、汚い名前のリストを保存しましょう — 余分なスペースがあるもの、大文字混じりのもの、そしてまったく空のものを少なくとも1つ。
内包表記を使って、次のものを作ります:
- きれいな名前のリスト — トリミング、小文字化、空白を除外
- 各名前の長さのリスト
- 5文字より長い名前のリスト
{name: length}という辞書- 最初の文字のセット
それから2つの実験:
- 1をふつうの
forループで書き直し、2つの結果が==で等しいか確認する forが2つとifがある内包表記を書き、それをループに分解する。どちらが読みやすいか?
最後の質問に「正解」はありません。それがポイントです。問うことを覚えるのが、この章の本当の学びです。