재사용 가능한 피크닉 목록 만들기

PythonBeginner
지금 연습하기

소개

친구들이 피크닉을 준비합니다. 두 레시피에 공통 식재료가 있고 집에 일부 재고가 있으며 상점에서는 포장 단위로 판매합니다. 이 정보를 실제로 쓸 수 있는 장보기 목록으로 바꾸는 재사용 가능한 Python 프로그램을 완성합니다.

먼저 작동하는 작은 예제를 실행한 뒤 준비된 계산 모듈을 개선하여 완전한 CSV를 만듭니다. picnic.py의 명령 옵션 처리와 CSV 저장 코드, 합성 레시피가 제공됩니다. Python 리스트, 딕셔너리, 반복문, 함수, 기본 예외와 기본 터미널 입력을 안다고 가정합니다. 새로운 파일 형식과 옵션은 여기서 설명합니다. 예상 시간에는 읽기와 일반적인 오류 대응이 포함되지만, 저자의 추정치이며 초보자 실측 시간은 아닙니다.

작은 피크닉 목록 읽기

이 단계에서는 샐러드 한 가지의 3인분 장보기 목록을 만들고 CSV 각 행을 JSON 입력과 연결합니다. WebIDE에서 Terminal 메뉴를 열고 New Terminal을 선택합니다. 실습의 모든 명령은 이 터미널에서 실행합니다. 준비 파일은 이전 실습에 의존하지 않습니다.

Python 리스트, 딕셔너리, 반복문, 함수, 기본 예외가 필요합니다. JSON 파일, CSV 저장 및 사용하는 옵션은 아래에서 설명합니다. 수량은 정수로, 그램(g), 밀리리터(ml) 또는 개수(each)를 사용하며 식재료 ID마다 단위는 하나입니다. 단위 변환이나 영양 계획은 다루지 않습니다.

작동하는 작은 예제로 시작하기

cd는 현재 디렉터리를 바꿉니다. 절대 경로는 /로 시작하고 이후의 data/menu.json 같은 상대 경로는 현재 디렉터리를 기준으로 해석됩니다. 다른 곳으로 이동한 후 경로 오류가 나면 같은 cd로 돌아오세요.

cd /home/labex/project/picnic

cat은 텍스트 파일을 출력합니다. 작은 메뉴를 읽습니다.

cat data/sample-menu.json

JSON은 객체에 {}(Python 딕셔너리로 읽힘), 리스트에 [], 문자열 키에 큰따옴표를 사용합니다. 각 레시피에는 ingredients 객체가 있습니다. "chickpea": 70은 70포장이 아니라 1인당 70 g이라는 뜻입니다. chickpea는 안정적인 ID이며 상품의 name은 표시 이름입니다. 이름이 같아도 서로 대체할 수 있는 상품은 아닐 수 있습니다.

cat data/products.json

상품 표에는 name, unit, 양의 정수 pack_size가 있습니다. 병아리콩은 한 포장에 200 g입니다. 작은 예제는 data/empty-pantry.json을 사용하며 빈 객체 {}는 재고가 없음을 의미합니다. 다른 제공 수량도 음수가 아닌 정수입니다. 재고에 ID가 없어도 재고 0이지만, 상품이 없으면 포장 용량을 모르므로 오류입니다.

제공된 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 두 포장으로 400 g을 얻고 190 g이 남습니다. 토마토는 120 g이 필요하여 250 g 한 포장을 사면 130 g이 남습니다. 이 작은 경우는 작동하지만 공통 식재료와 기존 재고를 사용하면 초기 프로그램의 두 오류가 드러납니다.

최종 CSV는 필요한 식재료 ID마다 한 행이며 열은 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이어야 합니다. 레시피별로 따로 구매하면 포장을 과다 구매할 수 있습니다. 먼저 모든 필요량을 합치고 재고를 한 번 차감한 뒤 남은 부족량을 한 번만 올림합니다.

cat calculations.py

초기 total_requirements는 매번 ID에 amount * people을 대입합니다. 두 번째 레시피에 같은 ID가 나오면 첫 번째 필요량을 덮어씁니다. 반면 totals.get(ingredient_id, 0)은 누적 합계를 읽거나 새 ID이면 0을 반환합니다. 여기에 다음 레시피의 인원수만큼 늘린 양을 더합니다.

작은 계산 모듈 교체하기

다음 명령은 들여쓰기와 함수 경계를 명확히 유지하도록 모듈 전체를 씁니다. cat > calculations.py는 텍스트를 파일로 리디렉션하여 내용을 교체합니다. <<'PYCODE'는 here-document를 시작합니다. 마지막 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여야 합니다. 두 번째 함수가 재고를 무시하므로 stock은 아직 0입니다. 이 중간 목록을 실제 장보기에 사용하면 안 됩니다.

재고를 사용한 뒤 포장 단위로 구매하기

이 단계에서는 구매 계산을 완성하고 집에 있는 재고가 반영된 목록을 읽습니다. WebIDE 터미널에서 계속합니다.

cat data/pantry.json

5인분 병아리콩 필요량은 550 g, 재고는 150 g이므로 400 g이 부족합니다. 200 g 두 포장이 정확히 채웁니다. 토마토는 200 g이 필요하지만 250 g이 있으므로 부족량과 구매량은 0이고 50 g이 남습니다. 랩은 5개가 필요하고 재고가 2개여서 3개가 부족합니다. 6개 한 포장을 사면 최종 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만 계산하면 재고가 필요량을 초과할 때 쓰지 않은 재고를 누락합니다.

완성된 두 함수로 모듈을 교체합니다. here-document 사용법은 이전 단계와 같습니다.

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

실제로 사용할 목록입니다. 병아리콩 두 포장, 토마토는 구매하지 않고, 랩 한 포장을 구매합니다. 구매량 0인 행을 남겨 “재고로 충분함”과 “실수로 빠진 식재료”를 구분하게 합니다. 이 메뉴에 필요한 ID만 행으로 작성하고 무관한 재고 항목은 출력하지 않아도 됩니다.

프로그램을 재사용하고 입력 오류에서 복구하기

이 단계에서는 picnic.py의 제공된 파일 저장 코드를 이해하고 다른 메뉴에 계산을 재사용하며 일반적인 오류에서 복구합니다. WebIDE 터미널에서 계속합니다. 변수나 이전 셸 세션은 필요하지 않으며 각 명령이 입력 경로를 명시적으로 전달합니다.

cat picnic.py

argparse는 이름 있는 옵션을 읽고 --help를 제공합니다. type=int는 정수가 아닌 인원수를 거부하고 다음 검사는 0과 음수를 거부합니다. load_json은 open(..., encoding="utf-8")와 json.load로 완전한 구조화 객체를 읽습니다. with 블록이 끝나면 파일이 닫힙니다. 계산 모듈은 행 딕셔너리의 리스트를 반환합니다.

모든 입력 처리와 계산을 끝낸 후 출력 파일을 엽니다. 상품 검사는 필요한 ID와 상품 ID를 비교하고 포장 정보가 없으면 ValueError를 발생시킵니다. csv.DictWriter는 열 이름에 따라 헤더와 행을 씁니다. newline=""은 CSV 모듈에 줄 끝 처리를 맡기고 UTF-8은 이름을 보존합니다. 쉼표로 문자열을 합치는 방식으로 CSV 저장을 대신하지 마세요. 나중에 이름에 쉼표가 있어도 한 필드로 유지되어야 합니다.

제공된 picnic.py 코드는 파일, JSON/값, 누락된 키 오류를 잡아 표준 오류에 설명을 출력하고 0이 아닌 상태로 종료합니다. 디스크 쓰기 중단까지 보호한다고 보장하지는 않지만, 잘못된 인원수나 상품 누락은 기존 출력을 그대로 보존해야 합니다.

예상된 실패를 읽고 복구하기

다음 두 명령은 의도적으로 실패합니다. 오류는 예상된 결과이고 터미널은 계속 사용할 수 있습니다. 첫 번째는 0명을 요청하며 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

두 번째는 메뉴 파일명의 오타입니다. 빈 목록이 성공하는 것이 아니라 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를 보존해야 합니다. 포장 정보를 모르는 것은 재고 0이나 구매 0과 다릅니다.

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

토마토 구매량은 0이고 50 g이 남습니다. 랩 부족량은 6으로 정확히 한 포장이며 잔량은 없습니다. 병아리콩 행은 없어야 합니다. five.csv, sample.csv, 프로그램, 원본 JSON이 남아 있어야 합니다. 다른 메뉴와 새 출력명을 사용하면 두 계획을 보관할 수 있습니다. 검사는 실제 출력을 읽고 임시 입력에서 변경하지 않은 프로그램을 실행합니다. 작업 중인 목록을 생성하거나 수정하지 않습니다.

요약

1인분 레시피 필요량을 식재료 ID별로 합산하고 재고를 한 번 차감한 뒤 남은 부족량만 포장 수로 올림했습니다. 실제 CSV를 만들고 의미 있는 구매량 0 행을 유지했으며 다른 메뉴에 프로그램을 재사용했습니다. 잘못된 입력에서도 정상 결과를 교체하지 않고 복구했습니다.