Skip to content
This page has been auto-translated and may contain errors.View in English

ラムダと内包表記

これら3つの機能には共通点があります。本来なら複数行を要するアイデアを、1つの読みやすい式で表現できるのです。うまく使えば、コードはより短く、より明確になります。下手に使えば、読めなくなります。この章では、それぞれをいつ使うべきか、そしていつ使うのをやめるべきかを扱います。

ラムダ、内包表記、zip は、よくあるパターンを式に圧縮する3つのツールです。必須ではありませんが、Pythonコード全体を通して登場するため、認識し、流暢に書けるようになる価値があります。指針となる原則は、単に短くするためではなく、意図をより明確にするために使うことです。

ラムダ式は実行時に無名の関数オブジェクトを生成します。内包表記は外側のフレームに for ループを持たずにコレクションを構築する最適化されたバイトコードにコンパイルされます。ジェネレータは遅延評価です。シーケンス全体を実体化せずに必要に応じて値を生成します。zip はタプルのイテレータを返し、入力のイテラブルを遅延的に消費します。これら3つはすべて、変換を命令的なループではなく式として表現するというテーマを共有しています。

ラムダ関数

ラムダは、名前のない、単一式の関数です。lambda キーワードで作成します。その本当の有用性は、名前付き関数を先に定義することなく、必要な場所にインラインで書けることです。これが sorted() で役立つ理由です。

ラムダは無名の単一式関数です。複数の引数を取れますが、本体は文ではなく単一の式でなければなりません。主な用途は、完全な def だと冗長な間接化を加えてしまう場面で、インラインの key= やコールバック引数として使うことです。もっと複雑なものには def を使ってください。

lambda args: expression はコードオブジェクトにコンパイルされ、関数オブジェクトを生成します。def と同じですが、名前がない(トレースバックでは <lambda> として表示)、文を含められない、docstringやアノテーションをサポートしないという点が異なります。ラムダはクロージャに参加します。自由変数は外側のスコープからキャプチャされます。よくある落とし穴は、ループ内の lambda i: ii を値ではなく参照でキャプチャすることです。作成時に値をバインドするには lambda i=i: i を使います。

python
double = lambda x: x * 2
double(5)   # 10

これは以下と等価です:

python
def double(x):
    return x * 2

ほとんどの場合、def を使ってください。ラムダには本当の利点が1つあります。名前を付けずに、必要な場所にインラインで書けることです。これが sorted()map()filter() で役立つ理由です:

python
players = [("さくら", 87), ("ひろし", 74), ("ゆうき", 92)]

sorted(players, key=lambda p: p[1])              # スコアでソート(昇順)
sorted(players, key=lambda p: p[1], reverse=True)  # スコアでソート(降順)

ラムダがなければ、key= 引数のためだけに名前付き関数を定義しなければなりません。ラムダは意図をローカルに、目に見える形で保ちます。

ラムダは複数の引数を取ることができます:

python
add = lambda a, b: a + b
add(3, 4)   # 7

ラムダを使うとき: 1か所だけで使う単純な式の場合のみ。複雑になっている、または再利用が必要な場合は、ちゃんとした def を書いてください。複数の演算子にまたがる、または条件分岐が必要なラムダは、通常 def に切り替えるべきサインです。

リスト内包表記

Pythonで最も一般的な変換は、シーケンスを取って、各要素に対して何かを行い、新しいリストを取得することです。リスト内包表記は、これを1行の読みやすい形で行います: [expression for item in iterable]if でフィルタを追加することもできます。

リスト内包表記は、ループで構築するパターンの簡潔な代替です。最適化されたバイトコードにコンパイルされ、.append() を使った同等の for ループより一般的に高速です。構造は [expression for item in iterable if condition] で、if 句はオプションです。

リスト内包表記は専用のバイトコードである LIST_APPEND ループにコンパイルされ、Pythonレベルのループで繰り返される list.append() 呼び出しより高速です。Python 3では新しいスコープを作成するため(Python 2とは異なり)、ループ変数は外に漏れません。ネストした内包表記は左から右、上から下に実行されます: [expr for x in a for y in b]x が外側のループとなるネストした for ループと等価です。

長い書き方:

python
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
    squares.append(n ** 2)

リスト内包表記:

python
squares = [n ** 2 for n in numbers]

構造は常に同じです: [expression for item in iterable]

python
scores    = [87, 42, 96, 55, 71]
scaled    = [s * 1.1 for s in scores]       # 10%のボーナスを適用
as_grades = [f"{s}/100" for s in scores]    # それぞれをフォーマット

条件付きフィルタリング

if 句を追加すると、テストに合格する要素だけを含めることができます。結果は、条件が True の要素だけを含む新しいリストです。

内包表記の if 句はフィルタであり、if/else ではありません。各要素に対して1回実行され、条件が真の要素だけを含めます。条件付き変換(条件に基づいて値を別の値にマップ)には、メイン式の中で三項式を使います。

if フィルタは、出力の条件式とは別物です。[x for x in data if x > 0] はフィルタです。[x if x > 0 else 0 for x in data] はマップ(ゼロにクランプ)です。両方を組み合わせることもできます: [x * 2 for x in data if x > 0]。複数の if 句は暗黙の and で連鎖します。

python
numbers  = [1, 2, 3, 4, 5, 6, 7, 8]
evens    = [n for n in numbers if n % 2 == 0]    # [2, 4, 6, 8]
odds     = [n for n in numbers if n % 2 != 0]    # [1, 3, 5, 7]
python
scores   = [87, 42, 96, 55, 71, 38]
passing  = [s for s in scores if s >= 60]    # [87, 96, 71]
failing  = [s for s in scores if s < 60]     # [42, 55, 38]

ネストした内包表記

内包表記をネストすることで、リストのリストを単一のリストに平坦化できます。左から右に読みます: 各行について、その行の各要素について、その要素を含めます。

ネストした内包表記は左から右に実行されます。最初の for 句が外側のループ、2番目が内側です。2次元構造ではなく、単一の平坦な結果を生成します。一目で内包表記が読みにくい場合は、ループを明示的に書いてください。

ネストした内包表記は、最初の for が最も外側となるネストしたループとして実行されます。各ループ変数のスコープは、後続の句で使用できます。デカルト積には、itertools.product の方が明確な場合がよくあります。読みやすさの重要なルール: 内包表記の解釈に1秒以上かかるなら、明示的なループ形式の方が良いドキュメントになります。

python
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat   = [item for row in matrix for item in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

左から右に読みます: matrixの各rowについて、rowの各itemについて、itemを含める。

ネストした内包表記はすぐに混乱してしまうことがあります。解釈に少しでも時間がかかるなら、ループを明示的に書いてください。

辞書内包表記

辞書内包表記は、リスト内包表記と同じ考え方を使って、1つの式で辞書を構築します: {key: value for item in iterable}。リスト内包表記と同じように if でフィルタを追加できます。

辞書内包表記は、キーと値のペアを生成する任意のイテラブルから新しい辞書を作成します。構文は {key_expr: val_expr for item in iterable if condition} です。ループからの重複するキーは、黙って最後の値を使います。既存の辞書に対する .items() が、辞書内包表記で最も一般的に使われるイテラブルです。

辞書内包表記は、リスト内包表記の LIST_APPEND に類似した、専用の MAP_ADD バイトコードにコンパイルされます。Python 3では新しいスコープを作成します。キー式はハッシュ可能な値を生成する必要があります。キー式が重複を生成すると、後の値が黙って勝ちます。順序付きマージのセマンティクスには、| 演算子(Python 3.9+)の方が内包表記より明確です。

python
names  = ["さくら", "ひろし", "ゆうき"]
scores = [87, 74, 92]

score_map = {name: score for name, score in zip(names, scores)}
# {"さくら": 87, "ひろし": 74, "ゆうき": 92}

フィルタ付き:

python
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"さくら": 87, "ゆうき": 92}
python
words     = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}

セット内包表記

セット内包表記は、波括弧を使い、コロンなしで1つの式でセットを構築します。結果がセットなので、重複は自動的に削除されます。

セット内包表記は {expression for item in iterable} を使い、set を生成します。自動的に重複を排除します。順序が重要でなく、変換から一意のコレクションを構築する必要があるときに使います。

セット内包表記は SET_ADD バイトコードにコンパイルされます。結果は順序のないセットです。式からの重複する値は黙ってマージされます。セット内包表記はリストや辞書の内包表記ほど一般的ではありませんが、1つの式で重複排除された変換を生成するきれいな方法です。

python
words   = ["apple", "banana", "cherry", "apple"]
unique  = {w.lower() for w in words}    # {"apple", "banana", "cherry"}

一意の値が欲しく、順序を気にしないときに、セット内包表記を使ってください。

ジェネレータ式

ジェネレータは、角括弧の代わりに丸括弧を使うリスト内包表記のように見えます。重要な違いは、リスト内包表記はリスト全体を一度にメモリに構築することです。ジェネレータは、必要なときだけ、1つずつ値を生成します。大きなシーケンスの場合、これははるかに少ないメモリで済みます。

ジェネレータ式はイテレータを生成し、コレクションではありません。値を遅延的に計算します。次の値は要求されたときだけ生成されます。これが最も価値があるのは、結果が sum()max()any() のような関数によってすぐに消費されるときで、完全なリストを先に構築する意味がないときです。

ジェネレータ式はコードオブジェクトにコンパイルされ、ジェネレータオブジェクトを返します。値は __next__ を介して遅延的に生成されるため、メモリ使用量は入力サイズに関係なく O(1) です。イテレータプロトコルに参加し、連鎖できます。イテラブルを受け取る関数に直接渡される場合、外側の括弧を省略できます。ジェネレータは枯渇後は再利用できません。複数回反復する必要がある場合は、リストに実体化してください。

python
squares_gen = (n ** 2 for n in range(1000000))
python
total = sum(n ** 2 for n in range(1000000))   # sum() がジェネレータを消費する

sum()max()min()any() などの関数に直接ジェネレータを渡すときは、追加の括弧を省略できます:

python
total = sum(n ** 2 for n in range(1000))   # 括弧は2組ではなく1組

ほとんどの日常的なコードでは、リスト内包表記で十分です。大きなデータセットを処理する場合や、すべてをメモリに保持するのが無駄になるようなストリーミングデータを扱う場合に、ジェネレータを使ってください。

zip()

zip() は2つ以上のシーケンスから要素をペアにして、並列にループできるようにします。最短のシーケンスで停止します。2つのリストが互いに対応している場合に、インデックスの管理を避けるきれいな方法です。

zip() は遅延イテレータのタプルを返し、入力イテラブルを同期して消費します。最短の入力で停止します。より長いシーケンスは黙って切り捨てられます。長さが異なる可能性のあるシーケンスには、itertools.zip_longest() が指定された値で短い方を埋めます。

zip()zip オブジェクト、つまり各入力イテレータに対して同時に next() を呼び出す遅延イテレータを返します。いずれかのイテレータが StopIteration を発生させると停止します。すべての入力は遅延的に消費されます。zip() 自体は入力サイズに関係なく O(1) のメモリを割り当てます。zip(*iterable) は標準的な転置操作です。* は外側のイテラブルを個別の引数に展開します。

python
names  = ["さくら", "ひろし", "ゆうき"]
scores = [87, 74, 92]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# さくら: 87
# ひろし: 74
# ゆうき: 92

zip() は最短のシーケンスで停止します。シーケンスの長さが異なる可能性がある場合は、itertools.zip_longest() を埋め値とともに使ってください。

ペアのzipされたリストを2つの個別のリストに戻すには、zip(*pairs) を使います:

python
pairs  = [("さくら", 87), ("ひろし", 74), ("ゆうき", 92)]
names, scores = zip(*pairs)
# names = ("さくら", "ひろし", "ゆうき")
# scores = (87, 74, 92)

* はここで何をしているのか?

*pairs はリストを個別の引数に展開します: zip(*pairs)zip(("さくら", 87), ("ひろし", 74), ("ゆうき", 92)) になります。* 演算子については 関数 の章で扱います。

zip() は、インデックスを手動で管理することなく、複数のシーケンスを並列に反復するきれいな方法でもあります:

python
before = [10, 20, 30]
after  = [15, 18, 35]

for b, a in zip(before, after):
    change = a - b
    print(f"{b} -> {a} ({'+' if change >= 0 else ''}{change})")

map() と filter()

map()filter() は、内包表記と同じことをする古い関数型スタイルのツールです。古いコードで目にすることがあるので、その意味を知っておく価値があります。新しいコードでは内包表記を優先してください。ほとんどのPython開発者にとってより読みやすいです。

map(func, iterable) は、各要素に func を適用する遅延イテレータを返します。filter(func, iterable) は、func が真の要素の遅延イテレータを返します。両方とも内包表記より古いものです。新しいコードでは内包表記を優先してください。必要なことを行う名前付き関数がすでにある場合は、map() を使ってください。

map()filter() は、Python 3ではリストではなく遅延イテレータを返します。map(f, it)(f(x) for x in it) と等価です。filter(pred, it)(x for x in it if pred(x)) と等価です。名前付き関数の場合、list(map(int, strings)) は「int を strings にマップする」と読めるので慣用的です。同等の内包表記 [int(s) for s in strings] も等しく有効です。

python
numbers = [1, 2, 3, 4, 5]

list(map(lambda x: x ** 2, numbers))         # [1, 4, 9, 16, 25]
list(filter(lambda x: x % 2 == 0, numbers))  # [2, 4]

内包表記を優先してください。ほとんどのPython開発者にとってより読みやすいです。すでに存在する名前付き関数があるときは map() を使ってください:

python
strings = ["1", "2", "3"]
numbers = list(map(int, strings))   # [1, 2, 3](ここでは内包表記よりきれい)

実践

プレイヤーリストを合格点でフィルタし、sorted とラムダでスコア順にランク付けし、列挙された順位とともに出力します:

python
players = [
    {"name": "さくら", "score": 87},
    {"name": "ひろし", "score": 42},
    {"name": "ゆうき", "score": 96},
    {"name": "たけし", "score": 55},
]

passing   = [p for p in players if p["score"] >= 60]
ranked    = sorted(passing, key=lambda p: p["score"], reverse=True)
score_map = {p["name"]: p["score"] for p in ranked}

for i, (name, score) in enumerate(score_map.items(), start=1):
    print(f"{i}. {name}: {score}")

ユーザーリストをアクティブな管理者でフィルタし、idから名前へのルックアップ辞書を構築し、ソートされた名前をそれぞれ1パスで収集します:

python
raw_users = [
    {"id": 1, "name": "さくら", "role": "admin", "active": True},
    {"id": 2, "name": "ひろし", "role": "user",  "active": False},
    {"id": 3, "name": "ゆうき", "role": "admin", "active": True},
    {"id": 4, "name": "たけし", "role": "user",  "active": True},
]

active_admins = [u for u in raw_users if u["active"] and u["role"] == "admin"]
id_map        = {u["id"]: u["name"] for u in raw_users}
names         = sorted(u["name"] for u in raw_users if u["active"])

print(f"Active admins: {[u['name'] for u in active_admins]}")
print(f"All active: {names}")

zip を使って特徴量名と重要度スコアをペアにし、辞書内包表記を構築し、ラムダでソートし、2番目の内包表記で値を正規化します:

python
feature_names = ["age", "income", "score", "tenure"]
importances   = [0.12, 0.34, 0.28, 0.26]

feat_dict = {f: i for f, i in zip(feature_names, importances)}
top_feats = sorted(feat_dict.items(), key=lambda x: x[1], reverse=True)[:2]

print("Top 2 features:")
for name, score in top_feats:
    print(f"  {name}: {score:.2f}")

# 合計が1.0になるように正規化(ここでは値はすでに1の合計だが、パターンとして示す)
total      = sum(feat_dict.values())
normalised = {k: round(v / total, 4) for k, v in feat_dict.items()}
print(f"Normalised: {normalised}")

zip は中間のタプルを構築せずに2つのリストをペアにします。辞書内包表記は1つの式でマッピングを構築します。ソートラムダは名前付きのキー関数を避けます。正規化の内包表記は元の辞書を変更することなく値を変換します。