【Python】ランチ組み合わせ問題から学ぶ!itertoolsと集合(set)で賢くコーディング

PR表記

※アフィリエイト広告を利用しています

目次

はじめに

どうもニコイチです。プログラミングを学んでいると、「考えられる全ての組み合わせを試したい」という場面に必ず出くわします。例えば、「ECサイトで、商品の色・サイズ・オプションの全組み合わせを在庫リストとして生成したい」や「ゲーム開発で、キャラクターが装備できるアイテムの全パターンを試したい」など、その用途は様々です。

このような「総当たり」処理を、forループを何重にもネストさせて書くと、コードはたちまち複雑で読みにくいものになってしまいます。

しかし、Pythonには、こうした問題を驚くほどシンプルかつエレガントに解決できる強力な武器が揃っています。

本記事では、身近な「ランチの組み合わせ」問題を題材に、あなたのPythonコードを一段階レベルアップさせる以下の3つのテクニックを解説します。

  1. itertools.product: 面倒な「総当たり」の組み合わせを自動で生成する。
  2. 集合 (set): 重複を自動で処理し、ユニークな値を管理する。
  3. アンパック (*): リストやタプルの中身をスマートに取り出して使う。

この記事を読み終える頃には、あなたのPythonコードは一段と洗練され、組み合わせ問題を解くのが得意になっているはずです!


今回挑戦する問題:最高のランチセットを探せ!

本記事では、以下の問題を解くコードを題材に解説を進めます。

あなたはレストランのオーナーです。お客さんにもっとランチを楽しんでもらうため、「選べるランチセット」を始めました。 以下のメニューから、主食・主菜・副菜を1つずつ選ぶことができます。

  • 主食: ごはん(150円), パン(200円)
  • 主菜: 唐揚げ(400円), ハンバーグ(500円)
  • 副菜: サラダ(100円), スープ(150円)

予算800円以内で注文できるランチセットの組み合わせをすべて見つけ、その組み合わせと合計金額を出力してください。

この問題を解決するのが、今回解説する以下のコードです。

import itertools

def find_lunch_combos(budget: int):
    """
    予算内で可能なランチの組み合わせを見つけます。
    """
    # 各カテゴリのメニューを(名前, 価格)のタプルで定義
    staples = [('ごはん', 150), ('パン', 200)]
    mains = [('唐揚げ', 400), ('ハンバーグ', 500)]
    sides = [('サラダ', 100), ('スープ', 150)]

    # 1. itertools.productで全組み合わせを生成
    all_combos = itertools.product(staples, mains, sides)

    print(f"予算: {budget}円")
    print("---注文可能な組み合わせ---")

    possible_sets = []
    # 2. 集合(set)を使って、ユニークな合計金額を保持してみる
    unique_prices = set()

    for combo in all_combos:
        # comboの中身例: (('ごはん', 150), ('唐揚げ', 400), ('サラダ', 100))
        total_price = combo[0][1] + combo[1][1] + combo[2][1]

        # 予算内かチェック
        if total_price <= budget:
            combo_names = (combo[0][0], combo[1][0], combo[2][0])
            possible_sets.append((combo_names, total_price))
            unique_prices.add(total_price) # 合計金額を集合に追加

    if not possible_sets:
        print("見つかりませんでした。")
        return

    # 見つかった組み合わせを出力
    for names, price in possible_sets:
        # 3. アンパック(*)を使って名前を綺麗に出力
        print(*names, f"| 合計: {price}円")
    
    print("\n---発生する合計金額のパターン---")
    # アンパック(*)で集合の要素も出力できる
    print(*sorted(list(unique_prices)), sep="円, ", end="円\n")


if __name__ == "__main__":
    find_lunch_combos(800)

武器①:itertools.product – 面倒な「総当たり」はお任せ

コードを読み解く最初の鍵は、itertools.product です。

今回の問題では、主食(2種)×主菜(2種)×副菜(2種)で、合計

2✕2✕2=8 通りの組み合わせが考えられます。 これを愚直にforループで書くと、こうなります。

# forループ地獄...
for staple in staples:
    for main in mains:
        for side in sides:
            # combo = (staple, main, side)
            # ここで計算処理...

3重のループとなり、見た目が複雑です。もし「ドリンク」カテゴリが追加されたら4重ループになり、保守性が低下します。

ここで itertools.product の出番です。itertoolsは、効率的なループ処理のための便利なツール(イテレータ)がたくさん詰まったPythonの標準ライブラリです。productは、複数のリストなどから要素を1つずつ取り出したすべての組み合わせ(直積)を自動で生成してくれます。

import itertools

staples = [('ごはん', 150), ('パン', 200)]
mains = [('唐揚げ', 400), ('ハンバーグ', 500)]
sides = [('サラダ', 100), ('スープ', 150)]

# これだけで3重ループと同じ組み合わせが作られる!
all_combos = itertools.product(staples, mains, sides)

for combo in all_combos:
    print(combo)

ネストしたforループが、たった1行のitertools.productで置き換えられました。コードがシンプルになり、カテゴリが増えても引数を追加するだけで対応できる、非常に柔軟な書き方です。

武器②:集合 (set) – ユニークな値をスマートに管理

次に注目するのが集合 (set) です。 リスト (list) と似ていますが、決定的な違いが2つあります。

  1. 重複する値を持てない: 同じ値を追加しても、自動的に1つにまとめられます。
  2. 順序の概念がない: 要素がどの順番で入っているかは保証されません。

今回のコードでは、注文可能な組み合わせの「合計金額のパターン」が何種類あるかを知るためにsetを使っています。

find_lunch_combos関数の実行結果を見ると、「ごはん・唐揚げ・スープ」と「パン・唐揚げ・サラダ」は、どちらも合計金額が700円です。 unique_prices という set に合計金額を追加していくと、この 700 という値は2回 add されますが、set の特性により自動的に1つにまとめられ、最終的に {650, 700, 750} というユニークな価格の集合が出来上がります。

# 実行過程のイメージ
unique_prices = set()

unique_prices.add(650) # {650}
unique_prices.add(700) # {650, 700}
unique_prices.add(750) # {650, 700, 750}
unique_prices.add(700) # 2回目の700。でも集合の中身は変わらない! -> {650, 700, 750}
unique_prices.add(750) # 2回目の750。これも変わらない -> {650, 700, 750}

このように、「種類」や「パターン」を数え上げたいときに set は絶大な効果を発揮します。

ただし、Python3でもversionが古い場合(3.9以前)の際は作動しないおそれがございますのでご注意ください。

武器③:アンパック (*) – リストの包装を解く魔法

最後の仕上げは、結果出力で使われている**アンパック(unpack)**演算子 * です。

names 変数には ('ごはん', '唐揚げ', 'サラダ') のような、メニュー名のタプルが格納されています。これをそのまま print すると、( )' が付いてしまいます。

names = ('ごはん', '唐揚げ', 'サラダ')
print(names, "| 合計: 650円")
# 出力: ('ごはん', '唐揚げ', 'サラダ') | 合計: 650円

ここで * をタプルの前につけると、魔法が起こります。

names = ('ごはん', '唐揚げ', 'サラダ')
# * をつけると...
print(*names, "| 合計: 650円")
# 出力: ごはん 唐揚げ サラダ | 合計: 650円

* は、リストやタプルといったコンテナの”包装”を解き、中身の要素をそれぞれ個別のものとして展開する役割を持っています。

つまり、print(*names, ...) は、Pythonの内部では print('ごはん', '唐揚げ', 'サラダ', ...) と同じように解釈されているのです。これにより、スペース区切りで綺麗に出力することができます。

まとめ

今回は、身近な「ランチの組み合わせ」問題を題材に、Pythonの強力な機能を活用してスマートに解く方法を見てきました。

  • itertools.product: 多重ループになりがちな「総当たり」の組み合わせを、たった1行でエレガントに生成できる。
  • set(集合): 重複を許さず、ユニークな要素を管理するのに最適。
  • アンパック (*): リストやタプルの要素を個別に展開し、関数の引数として渡すことで、出力をはじめとする様々な処理を簡潔に記述できる。

これらのテクニックを組み合わせることで、複雑に見える問題もシンプルで読みやすいコードで解決できるのです。

おわりに

ぜひ、お手元の環境でこのコードを動かし、メニューや予算を色々変えて試してみてください。また、「アレルギーを持つ食材を避けるには?」といった追加の条件を set の差集合(-演算子)を使って実装してみるのも、良い応用練習になるでしょう。

この記事が、あなたのPythonプログラミングの楽しさと奥深さを知るきっかけとなれば幸いです!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次