再利用できるピクニックリストを作る

PythonBeginner
オンラインで実践に進む

はじめに

友人たちがピクニックを計画しています。2 つのレシピで一部の食材が共通し、家には在庫があり、店ではパッケージ単位で販売されています。これらの情報を実用的な買い物リストに変える、再利用可能な Python プログラムを完成させます。

まず動作する小さな例を実行し、準備済みの計算モジュールを改良して完全な CSV を作成します。picnic.py のコマンドオプション処理と CSV 出力コード、および架空のレシピは提供済みです。Python のリスト、辞書、ループ、関数、基本的な例外、および基本的なターミナル入力を前提とします。新しい形式とオプションはここで説明します。所要時間には読解と通常の間違いへの対応を含みますが、著者の見積もりであり、初心者による実測値ではありません。

小さなピクニックリストを読む

このステップでは、サラダ 1 品の 3 人分の買い物リストを生成し、CSV の各行を JSON 入力と対応付けます。WebIDE から始め、Terminal メニューで New Terminal を選びます。このラボのコマンドはすべてそのターミナルで実行します。準備済みファイルは以前のラボに依存しません。

Python のリスト、辞書、ループ、関数、基本的な例外が必要です。JSON ファイル、CSV 出力、使用するコマンドオプションは以下で説明します。数量は整数で、グラム(g)、ミリリットル(ml)、個数(each)を使い、食材 ID ごとに単位は 1 つです。単位換算や栄養計画は扱いません。

動作する小さな例から始める

cd は現在のディレクトリを変更します。絶対パスは / で始まり、その後の data/menu.json などの相対パスは現在のディレクトリを基準に解決されます。移動後にパスのエラーが出た場合は、同じ cd でここに戻ります。

cd /home/labex/project/picnic

cat はテキストファイルを表示します。小さなメニューを読みます。

cat data/sample-menu.json

JSON はオブジェクトに {}(読み込むと Python 辞書)、リストに []、文字列キーに二重引用符を使います。各レシピには ingredients オブジェクトがあり、"chickpea": 70 は1 人あたり 70 g であり、70 パックではありません。chickpea は安定した ID、商品の name は表示名です。同名の商品でも交換可能とは限りません。

cat data/products.json

商品表には name、unit、正整数の pack_size があります。ひよこ豆は 200 g 入りです。小さな例は data/empty-pantry.json を使い、空オブジェクト {} は在庫なしを意味します。他の入力数量も非負整数です。在庫に ID がない場合も在庫ゼロですが、商品がない場合はパッケージ容量が不明なのでエラーです。

提供済みの picnic.py コードはコマンドオプションと CSV 出力を処理し、python3 で実行します。-- で始まるオプションは次の値の意味を示します。メニュー、在庫、商品表、正の人数、出力 CSV パスです。--output は作成または置換するファイルなので、ここで示す新しい名前を使ってください。元の JSON は変更しません。

python3 picnic.py --menu data/sample-menu.json --pantry data/empty-pantry.json --products data/products.json --people 3 --output sample.csv

期待されるメッセージ:

Created 2 ingredient rows in sample.csv
cat sample.csv

ヘッダーは各列の名前です。ひよこ豆は 70 * 3 = 210 g 必要です。200 g 入り 2 パックで 400 g となり、190 g 残ります。トマトは 120 g 必要で、250 g 入り 1 パックなら 130 g 残ります。この小さな例は動作しますが、共通の食材や既存在庫を使うと、初期プログラムの 2 つの不具合が現れます。

最終 CSV は必要な食材 ID ごとに 1 行で、列は ingredient_id,name,unit,required,stock,shortfall,packs,leftover です。stock は使用量だけでなく元の在庫量を表し、leftover には未使用の元在庫も含みます。行列の順序は自由ですが、全フィールドと関係が正確である必要があります。picnic.py、再利用可能な計算プログラム、data/ の全原本を残してください。同等の Python 計算方法も使えますが、インターフェースと入出力形式は維持します。

共通の食材を ID ごとに合計する

このステップでは、購入判断の前に共通食材の必要量計算を修正します。同じ WebIDE ターミナルで続けます。

cat data/menu.json

サラダは 1 人あたりひよこ豆 70 g、ラップは 40 g を使います。5 人分の合計は (70 + 40) * 5 = 550 g です。レシピごとに別々に購入すると余計なパックを買う可能性があります。まず全必要量を合計し、その後で在庫を 1 回だけ差し引き、残った不足量を 1 回だけ切り上げます。

cat calculations.py

初期の total_requirements は、その都度 ID に amount * people を代入します。2 つ目のレシピに同じ ID があると、1 つ目の必要量を上書きします。totals.get(ingredient_id, 0) はそれまでの合計を取得し、新しい ID ならゼロを返します。そこに次のレシピの人数分の量を加えます。

小さな計算モジュールを置き換える

次のコマンドは、インデントと関数の境界を明確にするためモジュール全体を書き込みます。cat > calculations.py はテキストをファイルへリダイレクトし、内容を置き換えます。<<'PYCODE' はヒアドキュメントの開始です。最後の PYCODE 行まで全部を貼り付けます。区切り文字を引用符で囲むと、シェルが Python テキストを解釈しません。置き換えるのは calculations.py だけで、JSON と CSV は変更しません。入力を間違えたらブロック全体を再実行できます。Python のインデントには意味があるため、4 スペース単位を保ってください。

cat > calculations.py <<'PYCODE'
"""The two calculation functions you will improve."""


def total_requirements(menu, people):
    totals = {}
    for recipe in menu["recipes"]:
        for ingredient_id, amount in recipe["ingredients"].items():
            totals[ingredient_id] = totals.get(ingredient_id, 0) + amount * people
    return totals


def shopping_rows(totals, pantry, products):
    rows = []
    for ingredient_id, required in totals.items():
        product = products[ingredient_id]
        size = product["pack_size"]
        packs = (required + size - 1) // size
        rows.append({"ingredient_id": ingredient_id,
                     "name": product["name"], "unit": product["unit"],
                     "required": required, "stock": 0,
                     "shortfall": required, "packs": packs,
                     "leftover": packs * size - required})
    return rows
PYCODE

現時点では集計だけが修正されています。主要メニューの 5 人分を生成します。

python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/products.json --people 5 --output five.csv
cat five.csv

ひよこ豆の required は 550、トマトは 200、ラップは 5 になります。2 つ目の関数が在庫を無視しているため、stock はまだゼロです。この途中段階のリストは買い物には使えません。

在庫を使ってからパッケージ単位で購入する

このステップでは購入計算を完成させ、家にある在庫を考慮したリストを読みます。WebIDE ターミナルで続けます。

cat data/pantry.json

5 人分のひよこ豆は 550 g 必要で、在庫が 150 g あるため不足量は 400 g です。200 g 入り 2 パックでちょうど補えます。トマトは 200 g 必要ですが 250 g あるので、不足量も購入数もゼロで、50 g 残ります。ラップは必要数 5、在庫 2、不足数 3 なので、6 個入りを 1 パック買い、最後に 3 個残ります。

max(0, required - stock) を使い、過剰在庫による負の購入量を防ぎます。非負整数の不足量 s と正の容量 p に対し、(s + p - 1) // p はパック数を切り上げます。// は切り捨て整数除算です。200 g 入りなら不足量 0 は 0 パック、200 は 1 パック、201 は 2 パックです。浮動小数点演算なしで境界を正しく扱えます。

最終残量は stock + packs * pack_size - required です。packs * pack_size - shortfall だけでは、在庫が必要量を上回る場合の未使用在庫を取りこぼします。

完成した 2 つの関数でモジュールを置き換えます。ヒアドキュメントの操作は前のステップと同じです。

cat > calculations.py <<'PYCODE'
"""Calculate requirements before deciding what to buy."""


def total_requirements(menu, people):
    totals = {}
    for recipe in menu["recipes"]:
        for ingredient_id, amount in recipe["ingredients"].items():
            totals[ingredient_id] = totals.get(ingredient_id, 0) + amount * people
    return totals


def shopping_rows(totals, pantry, products):
    rows = []
    for ingredient_id, required in totals.items():
        product = products[ingredient_id]
        stock = pantry.get(ingredient_id, 0)
        shortfall = max(0, required - stock)
        size = product["pack_size"]
        packs = (shortfall + size - 1) // size
        rows.append({"ingredient_id": ingredient_id,
                     "name": product["name"], "unit": product["unit"],
                     "required": required, "stock": stock,
                     "shortfall": shortfall, "packs": packs,
                     "leftover": stock + packs * size - required})
    return rows
PYCODE
python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/products.json --people 5 --output five.csv
cat five.csv

期待される内容です。同等の実装では順序が異なってもかまいません。

ingredient_id,name,unit,required,stock,shortfall,packs,leftover
chickpea,Chickpeas,g,550,150,400,2,0
tomato,Tomatoes,g,200,250,0,0,50
wrap,Wraps,each,5,2,3,1,3

ひよこ豆 2 パック、トマトは購入なし、ラップ 1 パックという実用的なリストになりました。購入ゼロの行も残し、「在庫で足りる」と「食材の出力漏れ」を区別できるようにします。このメニューで必要な ID だけに行が必要で、関係ない在庫品は不要です。

プログラムを再利用し入力エラーから復旧する

このステップでは picnic.py の提供済みファイル出力コードを理解し、別メニューに計算を再利用して通常の間違いから復旧します。WebIDE ターミナルで続けます。変数や以前のシェルセッションは不要で、各コマンドが入力パスを明示的に渡します。

cat picnic.py

argparse は名前付きオプションを読み、--help を提供します。type=int は整数でない人数を拒否し、次のチェックがゼロや負数を拒否します。load_json は open(..., encoding="utf-8") と json.load で構造化オブジェクト全体を読みます。with ブロックが終わるとファイルが閉じます。計算モジュールは各行の辞書を並べたリストを返します。

すべての入力処理と計算を終えてから出力ファイルを開きます。商品チェックは必要な ID と商品 ID を比較し、包装情報がなければ ValueError を発生させます。csv.DictWriter は列名に従ってヘッダーと各行を書きます。newline="" は改行処理を CSV モジュールに任せ、UTF-8 は名前を保持します。CSV 出力をカンマでの文字列連結に置き換えないでください。将来、名前にカンマが含まれても 1 フィールドとして扱う必要があります。

提供済みの picnic.py コードはファイル、JSON/値、存在しないキーのエラーを捕捉し、標準エラー出力に説明を出して非ゼロで終了します。ディスク書き込み中断への保護は保証しませんが、不正な人数や商品欠落では既存出力を変更してはいけません。

想定どおりの失敗を読み、復旧する

次の 2 コマンドは意図的に失敗します。エラーは想定どおりで、ターミナルは引き続き使用できます。最初は人数ゼロを要求し、five.csv を置き換える前に people must be a positive integer を表示するはずです。

python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/products.json --people 0 --output five.csv

2 つ目はメニューファイル名の誤記です。成功した空リストではなく、Cannot make list: とファイルがない旨のメッセージになります。data/ の名前を確認し、正しいパスで再実行して直します。

python3 picnic.py --menu data/menus.json --pantry data/pantry.json --products data/products.json --people 5 --output five.csv
ls data

missing-products.json は意図的に不完全な商品表で、メニューに必要な商品が欠けています。次のコマンドは missing product: を表示し、既存の five.csv を保持する必要があります。包装情報が不明であることは、在庫ゼロや購入ゼロとは異なります。

python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/missing-products.json --people 5 --output five.csv
cat five.csv

正しい 5 人分リストは前のステップと同じ内容のままです。原入力を変更せず、完全な商品ファイルを選んで復旧します。

別の買い物リストを作る

cat data/other-menu.json

このメニューはトマトとラップだけです。4 人分にはトマト 200 g とラップ 8 個が必要です。同じプログラムで元の在庫を使い、前回の食材を引き継がないようにします。

python3 picnic.py --menu data/other-menu.json --pantry data/pantry.json --products data/products.json --people 4 --output other.csv
cat other.csv

トマトは購入ゼロ、残量 50 g です。ラップは不足 6 個でちょうど 1 パック、残量ゼロです。ひよこ豆の行はありません。five.csv、sample.csv、プログラム、原 JSON は保持されます。別メニューと新しい出力名で 2 つの計画を残せます。チェックは実際の出力を読み、変更していないプログラムを一時的な入力で実行します。作業中のリストを作成したり修復したりはしません。

まとめ

1 人分の必要量を食材 ID ごとに合計し、在庫を 1 回だけ差し引き、残りの不足量だけをパッケージ単位で切り上げました。実際の CSV を作り、意味のある購入ゼロの行を保持し、別メニューにプログラムを再利用しました。また、不正入力から復旧しても正しい結果を置き換えずに済みました。