PyDA Course
مبتدئ موجّه ⏱ 60 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

مخطط الوجبات

clidata-structurescsv

يبدو تخطيط الوجبات بسيطًا على الورق ، قرّر سُعُر العَشاء السبعة، واكتب قائمة مشتريات ، لكن الحساب هو بالضبط حيث يتفكك: ثلاث وصفات تشترك في الأرز، واثنتان في الدجاج، ويبقى عمود السعرات الحرارية دون رقابة بصمت. يبني هذا المشروع واجهة CLI تدير الدفاتر فيه: قاعدة بيانات وصفات، وخطة وجبات أسبوع، وإجماليات سعرات حرارية ومغذيات يومية، وقائمة مشتريات تدمج المكونات المشتركة في ملخص موحّد واحد بدلًا من سبع قوائم متداخلة.

يفترض هذا إنهاء Python 101 ، القوائم والقواميس وقراءة الملفات والدوال. لا شيء أبعد من ذلك: لا قاعدة بيانات، ولا ويب، ولا خدمات خارجية. هذا اختياري وغير مُقيَّم؛ راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة.

🎯 ما ستفعله

  1. تخزين قاعدة بيانات وصفات صغيرة بحيث يكون لكل وصفة اسم طبقها وحصصها ومكوناتها وتغذيتها.
  2. تخطيط أسبوع بتخصيص وصفة واحدة لكل يوم وطباعة الخطة.
  3. حساب إجماليات السعرات الحرارية والمغذيات الكبرى لكل يوم من الوصفات التي اخترتها.
  4. توليد قائمة مشتريات موحّدة واحدة تدمج المكونات المشتركة بدلًا من تكرارها.
  5. تحميل قاعدة البيانات من CSV بحيث يمكنك تنميتها دون تعديل الكود.

أين تُشغّل هذا

محليًا باستخدام uv هو المسار الأساسي هنا والوحيد ذو مردود نظام ملفات فعلي: تعيش قاعدة بيانات الوصفات فعلًا على القرص (CSV)، و«أضف ملف وصفة، أعد التشغيل، قائمة مشتريات جديدة» حلقة حقيقية. تفترض الخطوات أدناه مجلدًا صغيرًا بـ uv.

GitHub Codespaces يعمل بالطريقة نفسها: افتح codespaces.new/abderrahim-lectures/python-data-analysis-course وستكون Node وPython وuv مثبّتة بالفعل في استنساخ حقيقي لمستودع الدورة.

Google Colab وKaggle Notebooks وBinder يمكنها تشغيل كل دالة، وبالنسبة لمشروع بيانات نقي كهذا فهي جيدة فعلًا ، لا أسرار ولا GPU ولا ملفات ضخمة. الشيء الوحيد الذي لا ينتقل هو «ملفات جلستك مؤقتة»، وهو ما يهم غالبًا فقط لو أردت لملف وصفاتك الشخصي البقاء. يضمّن الدفتر أدناه قاعدة بيانات مبتدئة بحيث يعمل خط الأنابيب كاملًا من الخطة → الإجماليات → قائمة المشتريات من البداية للنهاية دون أي إعداد. جرّبه هناك أولًا، ثم اذهب محليًا عندما يكون لديك وصفات خاصة بك.

Open In Colab Open In Kaggle Binder

الإعداد

كل ما تحتاجه قبل تخطيط وجبة واحدة: uv، وقاعدة بيانات وصفات صغيرة يمكنك توسيعها.

ثبّت uv وهيئ المشروع

macOS / Linux (الطرفية):

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

أغلق طرفيتك وأعد فتحها، ثم:

uv --version
mkdir meal-planner && cd meal-planner
uv init --bare

لا حزم إضافية ، هذا المشروع مكتبة قياسية نقية.

أنشئ قاعدة بيانات الوصفات

الصق هذا في recipes.py كقاعدة بيانات مبتدئة (أربع وصفات، لكل منها إجمالي سعرات حرارية ومغذيات كبرى لكل حصة، زائد قائمة مكونات واحدة بعنصر مشترك لجعل الدمج في الخطوة 4 مرئيًا):

# recipes.py
RECIPES = [
    {"name": "Chicken stir-fry", "servings": 2, "calories": 480, "protein": 38,
     "ingredients": ["chicken breast", "rice", "broccoli", "soy sauce"]},
    {"name": "Veggie curry", "servings": 4, "calories": 410, "protein": 16,
     "ingredients": ["chickpeas", "rice", "coconut milk", "curry powder"]},
    {"name": "Tacos", "servings": 4, "calories": 520, "protein": 24,
     "ingredients": ["ground beef", "tortillas", "lettuce", "salsa"]},
    {"name": "Tofu bowl", "servings": 2, "calories": 450, "protein": 30,
     "ingredients": ["tofu", "rice", "broccoli", "soy sauce"]},
]

شغّل:

uv run python -c "from recipes import RECIPES; print(len(RECIPES), 'recipes loaded')"

✅ قائمة التحقق

  • uv --version يطبع رقم إصدار.
  • ✅ يوجد recipes.py ويطبع uv run python -c "from recipes import RECIPES; print(len(RECIPES))" الرقم 4.
  • ✅ لاحظ أن rice وbroccoli وsoy sauce تظهر في أكثر من وصفة ، تلك هي المكونات التي يجب أن تدمجها الخطوة 4.

الخطوة 1: حمّل قاعدة بيانات الوصفات وافحصها

«البيانات» هنا تعني قائمة من القواميس، كل dict وصفة واحدة. قبل تخطيط أي شيء، تريد دالة تعرض قاعدة البيانات ، لأن كل خطوة لاحقة (الإجماليات، الدمج) ستكون خاطئة إذا اختلفت وصفتان بصمت في حقول بعضهما.

1.1 استعلم عن قاعدة البيانات بالاسم

# planner.py
from recipes import RECIPES

def find_recipe(name: str) -> dict:
    matches = [r for r in RECIPES if r["name"].lower() == name.lower()]
    if not matches:
        raise ValueError(f"No recipe named {name!r}")
    return matches[0]

def list_recipes() -> None:
    for r in RECIPES:
        print(f"{r['name']:<18} {r['calories']:>4} kcal  {r['protein']:>3} g protein")

if __name__ == "__main__":
    list_recipes()
    print()
    print(find_recipe("tacos"))

يخفض find_recipe الطرفين قبل المقارنة، بحيث يصيب find_recipe("TACOS") وfind_recipe("tacos") الـ dict نفسه ، عادة متانة صغيرة لكنها حقيقية. البحث فهم قائمة لأن أربع وصفات لا تحتاج فهرس dict؛ لو نمت قاعدة البيانات إلى الآلاف، فسيكون الإصلاح dict مفتاحه الاسم، لا فهمًا أسرع.

👟 تلميح البداية :

شغّل planner.py لسرد الوصفات الأربع كلها، ثم اتصل بـfind_recipe("tacos") وافحص الـ dict العائد حقلًا بحقل.

🎯 الناتج المتوقع :

الوصفات الأربع مطبوعة كجدول أنيق (الاسم + السعرات الحرارية + البروتين)، ثم dict باسم Tacos بكل المفاتيح الستة ، بما فيها ingredients كقائمة.

🩹 إذا لم يعمل :

إذا أُطلق ImportError: cannot import name 'RECIPES'، فالملف recipes.py لكن اسم الوحدة recipes ، تحقق أنك لم تسمِّه recipe.py. إذا رفع find_recipe("tacos") خطأ رغم وجود Tacos، فإن .lower() على الطرفين يقارن القيم الصحيحة ، أضف جملة طباعة لتأكيد الأسماء قبل أن «تصلح» النسخة العاملة بحذف خلل تحويل الحالة.

1.2 تحقّق من الاستعلام

✅ قائمة التحقق

  • ✅ يطبع list_recipes() الوصفات الأربع كلها بالسعرات الحرارية والبروتين.
  • ✅ يرفع find_recipe ValueError واضحًا لاسم غير موجود، ويُرجع الـ dict الصحيح بغض النظر عن حالة الأحرف.
  • ✅ لكل dict وصفة المفاتيح نفسها ، يمكنك كتابة فحص من سطر واحد أن تشترك الأربع في مجموعة مفاتيح متطابقة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يُرجع find_recipe مرجعًا إلى الـ dict الفعلي في RECIPES، لا نسخة. إذا غيّر كود لاحق ما أرجعه، فما الذي يفسد بصمت ، وهل هناك حجة لإرجاع نسخة؟
  • أربع وصفات تبرر مسحًا خطيًا. عند أي حجم قاعدة بيانات يتوقف «إعادة تسمية dict مفتاحه الاسم» عن كونه اختياريًا ، وماذا يخبرك ذلك عن متى تمد يدك لتغيير بنية مقابل متى تكون القوة الغاشمة صادقة وخالية؟

الخطوة 2: ابنِ الخطة الأسبوعية

الخطة أبسط نموذج ممكن للأسبوع: dict واحد يربط كل يوم باسم وصفة. كل شيء لاحق ، الإجماليات، قائمة المشتريات ، يأخذ هذه الخطة كمُدخل وحيد. إبقاء بيانات الخطة منفصلة عن دوال الإجماليات هو ما يجعل كل خطوة قابلة للاختبار بشكل مستقل.

2.1 خصّص وصفة لكل يوم

# planner.py (continued)

DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]

def build_plan(assignments: dict[str, str]) -> dict[str, dict]:
    plan = {}
    for day, name in assignments.items():
        if day not in DAYS:
            raise ValueError(f"{day!r} is not a day of the week")
        plan[day] = find_recipe(name)
    return plan

if __name__ == "__main__":
    week = build_plan(
        {"Mon": "Chicken stir-fry", "Tue": "Tacos", "Wed": "Veggie curry",
         "Thu": "Tofu bowl", "Fri": "Chicken stir-fry", "Sat": "Tacos", "Sun": "Veggie curry"}
    )
    for day, recipe in week.items():
        print(f"{day}: {recipe['name']}")

يتحقق build_plan من أمرين قبل التخزين: أن اليوم واحد من الأسماء السبعة المعروفة (خطأ مطبعي مثل "monday" يُلتقط بصخب)، وأن كل وصفة تُحَل عبر find_recipe (بحيث يفشل مخطط يشير إلى وصفة محذوفة في وقت التخطيط، لا في وقت الإجماليات بعد ثلاث خطوات). ربط اليوم → dict الوصفة الكامل، لا اليوم → سلسلة، هو القرار الذي يتيح للخطوة 3 قراءة التغذية من الخطة دون بحث ثانٍ.

👟 تلميح البداية :

أبقِ الخطة في dict[str, str] عادي لمدة دقيقة قبل الانتقال إلى dict[str, dict] ، شغّل الحلقة، ثم اجعل التبديل وأعد التشغيل؛ استشعر الفرق فيما ستتمكن الخطوة 3 من الوصول إليه.

🎯 الناتج المتوقع :

سبعة أسطر، من Mon: Chicken stir-fry حتى Sun: Veggie curry، بترتيب الأيام، دون KeyError وكل الأسماء مطابقة لقاعدة البيانات.

🩹 إذا لم يعمل :

إذا حدث ValueError: 'monday' is not a day of the week، فمفاتيحك ليست سلاسل DAYS بالضبط ، الفحص صارم عن قصد، لذا إما أصلح المفتاح أو (أفضل) دع الفحص يواصل حمايتك. إذا رفع find_recipe خطأ No recipe named 'Taco'، فخططك يشير إلى اسم لا تملكه قاعدة البيانات ، فشل وقت التخطيط هو الإصلاح، لا الخلل.

2.2 تحقّق من الخطة

✅ قائمة التحقق

  • ✅ يُرجع build_plan dict بسبعة مفاتيح بالضبط، واحد لكل يوم، كل قيمة dict وصفة مكتمل.
  • ✅ يرفع اسم يوم غير معروف ValueError بدلًا من إنشاء يوم شبح بصمت.
  • ✅ يمكن لاسم الوصفة نفسه أن يظهر في عدة أيام ، الخطة لا تتطلب سبعة أطباق متميزة.
  • ✅ تعديل الخطة للإشارة إلى وصفة مفقودة يفشل وقت البناء، لا وقت الإجماليات.

🤔 سؤال (أسئلة) سقراطي(ة)

  • تخزن الخطة dictات الوصفات الكاملة، بحيث يمكن للخطة وRECIPES التباعد: إذا عدّلت سعرات وصفة ما، فما زالت الخطة القديمة المبنية قبل التعديل تحمل الـ dict القديم ، والخطط الجديدة تأخذ الجديد. هل تلك ميزة أم خلل لمخطط وجبات، وأي سلوك ستريده إذا تغيرت الوصفات أسبوعيًا؟
  • اخترنا أسماء الأيام كمفاتيح الخطة. ما الذي ينكسر إذا أراد شخص خطةً لأيام الأسبوع فقط ، وكيف سيتغير build_plan ليقبل «أي مُكرَّر من (الخانة، الوصفة)»؟

الخطوة 3: اجمع تغذية اليوم

سعرات كل وصفة ومغذياتها الكبرى لكل حصة؛ والخطة وجبات، والوجبة الواحدة عادة «الوصفة كلها» أو عدد حصص محدد. هذه الخطوة حيث يصبح تخطيط الوجبات مفيدًا: لا «اخترت وصفات لطيفة» بل «يجيء أسبوعي إلى 2180 سعرًا حراريًا يوميًا».

3.1 اجمع تغذية كل وجبة عبر اليوم

# planner.py (continued)

def day_totals(recipes: list[dict]) -> dict:
    total = {"calories": 0, "protein": 0, "meals": 0}
    for r in recipes:
        total["calories"] += r["calories"]
        total["protein"] += r["protein"]
        total["meals"] += 1
    return total

def week_totals(plan: dict[str, dict]) -> dict:
    return {day: day_totals([r]) for day, r in plan.items()}

if __name__ == "__main__":
    week = build_plan(
        {"Mon": "Chicken stir-fry", "Tue": "Tacos", "Wed": "Veggie curry",
         "Thu": "Tofu bowl", "Fri": "Chicken stir-fry", "Sat": "Tacos", "Sun": "Veggie curry"}
    )
    totals = week_totals(week)
    for day, t in totals.items():
        print(f"{day}: {t['calories']:>4} kcal, {t['protein']:>2} g protein")
    daily_mean = sum(t["calories"] for t in totals.values()) / len(totals)
    print(f"daily mean: {daily_mean:.0f} kcal")

التراكم dict عادي تضيف إليه ، نمط «الإجمالي الدُرفلي»، مسطور حتى يظهر الشكل (أول مرة تكتبه، ترى أن += إلى إدخال dict قانوني، وأنه يجب عليك تهيئة المفتاح إلى 0 أولًا). week_totals مجرد day_totals مطبقة على قائمة من وصفة واحدة لكل يوم ، وضع [r] في صندوق طريقة تبدو محرجة بعض الشيء لكنها صادقة لقول «هذا اليوم يستخدم وصفة واحدة بالضبط»، وتعني أن day_totals يمكنها لاحقًا قبول عدة وصفات (غداء وأيضًا عشاء حقيقيين) دون أي تغيير.

👟 تلميح البداية :

أضف day_totals(["a", "b"]) مع dictي وصفة قبل بناء أسبوعك ، أكّد أن الدالة تجمعهما صحيحًا وحدها، ثم اربطها في week_totals.

🎯 الناتج المتوقع :

سبعة أسطر من إجماليات بأسلوب Mon: 480 kcal, 38 g protein، ثم سطر daily mean: ... kcal واحد ، بخططنا، بالضبط (480+520+410+450+480+520+410)/7 = 467 kcal كمتوسط.

🩹 إذا لم يعمل :

إذا جُمعت السعرات الحرارية بصمت إلى صفر، فـ += يكتب إلى مفتاح لم يُهيأ قط ، يجب أن يظهر كل مفتاح أولًا كـ total = {"calories": 0, ...} قبل حلقة الحلقة. إذا طبع المتوسط اليومي بذيل .6666, فهذا سلوك عائم صحيح ، حوِّل إلى int أو قرّب صراحة عند رغبتك في عرض 467.

3.2 تحقّق من الإجماليات

✅ قائمة التحقق

  • day_totals على dictي وصفة يُرجع المجموع الحسابي للوصفتين ، تحقق باليد مع dictين صغيرين مختلقين.
  • ✅ يغطي week_totals كل يوم في الخطة، لا أكثر ولا أقل.
  • ✅ المتوسط اليومي يطابق متوسطك المحسوب باليد للقيم السبعة لكل يوم.
  • ✅ تشغيل day_totals([]) يُرجع التراكم الصفري كله، لا خطأً.

🤔 سؤال (أسئلة) سقراطي(ة)

  • تُعامل تغذية كل حصة كـ«مغذيات للطبق الكامل». إذا كانت للوصفة servings: 4 وأنت تأكل ربعها فقط، فماذا يبالغ في الإبلاغ أداةُنا بمعامل 4 ، وما الضرب الوحيد الذي يصلحه؟
  • تأخذ day_totals قائمة وصفات. ما التغيير على نموذج بيانات الخطة الذي يتيح ليوم واحد أن يحمل فطورًا وغداءً وعشاءً، وما أقل إعادة كتابة لـ week_totals تدعمها؟

الخطوة 4: ادمج المكونات في قائمة مشتريات واحدة

التخطيط والإجماليات دفاتر محاسبية؛ قائمة المشتريات هي المردود. قائمة ساذجة «السوتيه يحتاج أرزًا، والتاكوس يحتاج أرزًا، والكاري يحتاج أرزًا» تسلّمك ثلاثة أكياس أرز. دمج المكونات المشتركة بالاسم ، جمع عدد الوصفات التي تحتاجها ، يحوّل ذلك إلى إدخال صادق واحد.

4.1 عدّ ذِكر المكونات عبر الأسبوع

# planner.py (continued)

def grocery_list(week: dict[str, dict]) -> dict[str, int]:
    needed = {}
    for recipe in week.values():
        for ingredient in recipe["ingredients"]:
            needed[ingredient] = needed.get(ingredient, 0) + 1
    return dict(sorted(needed.items()))

if __name__ == "__main__":
    week = build_plan(
        {"Mon": "Chicken stir-fry", "Tue": "Tacos", "Wed": "Veggie curry",
         "Thu": "Tofu bowl", "Fri": "Chicken stir-fry", "Sat": "Tacos", "Sun": "Veggie curry"}
    )
    for ingredient, count in grocery_list(week).items():
        print(f"{ingredient:<14} x{count}")

needed.get(ingredient, 0) + 1 هو اصطلاح «عدّ التكرارات بـ dict» القياسي ، .get(key, 0) يُرجع الإجمالي الجاري حتى الآن، افتراضيًا 0 عند أول رؤية، ثم يضاف واحد. لاحظ أن rice يجب أن يظهر الآن x3 (سوتيه، كاري، وعاء توفو): الدمج هو الميزة كلها، وتمريرة فرز أخيرة تجعل القائمة قابلة للمسح السريع بغض النظر عن ترتيب الخطة.

👟 تلميح البداية :

شغّل الدمج، ثم عدّ rice باليد عبر الخطة وتأكد أن عدّك اليدوي يطابق x3 الذي طبعته الدالة ، ذلك التعادل هو لحظة «إنها تعمل».

🎯 الناتج المتوقع :

قائمة مرتبة حيث يظهر rice x3 وbroccoli x2 وsoy sauce x2 مرة واحدة لكلٍّ ، لا مكررًا لكل وصفة ، وعناصر ذات استخدام واحد مثل tortillas x1 ما زالت حاضرة.

🩹 إذا لم يعمل :

إذا ظهر rice x1 ثلاث مرات لأن dict لا يمكن أن يملك مفاتيح مكررة، فأنت تُلحق بقائمة بدلًا من العدّ في dict ، الدمج هو الـ dict. إذا كان العدّ خاطئًا لكن المفاتيح فريدة، تحقق أن +1 داخل إسناد needed[ingredient] = ... وأن الافتراضي .get(..., 0) هجاؤه 0، لا None (الذي سينهار على None + 1).

4.2 تحقّق من الدمج

✅ قائمة التحقق

  • ✅ المكونات المشتركة (rice وbroccoli وsoy sauce) تظهر مرة واحدة بالضبط، بعدّادات تطابق عدّك اليدوي للخطة.
  • ✅ كل مكون تستخدمه أي وصفة يظهر في القائمة النهائية ، لا شيء محذوف.
  • ✅ المخرجات مرتبة أبجديًا بغض النظر عن الترتيب الذي تظهر به الوصفات في الخطة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يعدّ هذا الدمج عدد الوصفات التي تحتاج أرزًا، لا كمية الأرز التي يجب أن يخزنها متجر البقالة (ذلك يعتمد على الحصص والأجزاء لكل شخص). ما الذي ستحتاجه دالة grocery_stock(week, portion_per_person) ، لكل مكون ، لا تستطيع قائمة name -> count الحالية الإجابة عنه ، وما الشكل الذي يجب أن تكون عليه البيانات؟
  • مكوّنان هما نفس عنصر البقالة بأسماء مختلفة («chicken breast» مقابل «chicken thighs») والدمج يعاملهما منفصلين بسعادة. هل يجب على الأداة دمجهما؟ ما أبسط تغيير بيانات (تلميح: اسم معياري لكل مكون) يتيح لها ، وما المشكلة التي يُدخلها ذلك في مكان آخر؟

الخطوة 5: حمّل الوصفات من CSV

كانت أربع وصفات مشفرة في recipes.py جيدة للتعلم؛ المخطط الحقيقي يكبر. تستبدل الخطوة 5 حرف RECIPES بـ CSV على القرص ، المهارة نفسها «اقرأ CSV»، لكن مطبقة لتجعل قاعدة البيانات ملفًا يمكنك تعديله دون لمس الكود.

5.1 اقرأ قاعدة البيانات من CSV

أنشئ recipes.csv:

name,servings,calories,protein,ingredients
Chicken stir-fry,2,480,38,"chicken breast, rice, broccoli, soy sauce"
Veggie curry,4,410,16,"chickpeas, rice, coconut milk, curry powder"
Tacos,4,520,24,"ground beef, tortillas, lettuce, salsa"
Tofu bowl,2,450,30,"tofu, rice, broccoli, soy sauce"
# csv_loader.py
import csv
from recipes_data import RECIPES  # same dict structure, now built by load_recipes_csv

def load_recipes_csv(path: str) -> list[dict]:
    recipes = []
    with open(path, newline="") as f:
        for row in csv.DictReader(f):
            recipes.append({
                "name": row["name"],
                "servings": int(row["servings"]),
                "calories": int(row["calories"]),
                "protein": int(row["protein"]),
                "ingredients": [i.strip() for i in row["ingredients"].split(",")],
            })
    return recipes

تحويلان يجعلان هذه الخلية مختلفة عن «اقرأ ملفًا فقط»: أعمدة الأرقام تُصب في قوالب int(...) (قارئ CSV يُرجع سلاسل بالتصميم ، نسيان هذا ينتج '480' بدلًا من 480 وخطأً صامتًا في حساب الخطوة 3)، وسلسلة المكونات الواحدة تصبح قائمة بالتقسيم على الفواصل وتجريد المسافات. Dict الوصفة الآن بالشكل الذي تتوقعه الخطوات السابقة بالضبط ، المُحمّل بديل إسقاطي جاهز لـ RECIPES المشفر، وهي نقطة الحفاظ على مخطط مستقر كلها.

👟 تلميح البداية :

اكتب الـ CSV، وشغّل load_recipes_csv، ثم استبدل from recipes import RECIPES في planner.py بـ import csv_loader as recipes وأعد تشغيل list_recipes ، مخرجات متطابقة، مصدر حقيقة جديد.

🎯 الناتج المتوقع :

يُرجع load_recipes_csv("recipes.csv") أربعة dict متطابقة مع المشفرة، ويطبع list_recipes() في planner.py الجدول نفسه كما قبل ، الآن منسوبًا إلى الملف.

🩹 إذا لم يعمل :

إذا طُبع الإجمالي مثل 480 38 لكن الماسح يُظهر سلاسل، فقوالب int(...) تُخطت ، كل عمود رقمي يحتاجها. إذا خرجت المكونات كسلسلة واحدة طويلة، فلم يُطبق split(",") أو لا تقتبس صفوف CSV عمود المكونات (فواصل غير مقتبسة تقسّم الصف، بحيث يكسر csv الصف عند كل فاصلة). إذا حدث KeyError: 'name', فسطر الرأس لديك باسم عمود أول مختلف عما تتوقعه row["name"] ، اطبع row من DictReader لرؤية المفاتيح الحقيقية.

5.2 تحقّق من مُحمّل الـ CSV

✅ قائمة التحقق

  • ✅ يُرجع load_recipes_csv وصفات حقولها الرقمية int، لا str.
  • ingredients في كل dict قائمة حقيقية، دون مسافات أمامية/خلفية شاردة.
  • ✅ حذف صف CSV يزيل تلك الوصفة من list_recipes() في التشغيل التالي ، تعديل ملف، لا تعديل كود.
  • ✅ إضافة صف جديد بنفس الأعمدة الخمسة يُحمَّل دون لمس أي Python.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يمنحك CSV قاعدة بيانات يمكنك تعديلها باليد. ما الذي لا يمنحك إياه قاعدة بيانات حقيقية (أو حتى ملف JSON) ، فكر في الاقتباس والتهريب وماذا يحدث عندما يحتوي المكوّن نفسه فاصلة؟
  • استخدم csv.DictReader سطر الرأس للمفاتيح. إذا تغير الرأس إلى dish بدلًا من name، فسينكسر كل row["name"]. هل هذه المقاومة جيدة (أمانة مخطط) أم سيئة (اقتران هش)؟ وما الذي سيجعل المُحمّل يفشل بصخب بدلًا من الفشل بشكل خاطئ؟

⚠️ مآزق شائعة

  • نسيان قوالب int() من الـ CSV. يُرجع csv سلاسل؛ calories: "480" المجموع مع protein: "38" قد لا يرفع حتى خطأً ، يمكنه أن يدمج أو يجري حساب سلاسل بهدوء ، بينما يعيش "480" * 4 لاحق في عالم يكون فيه كل شيء نصًا. اصبب دائمًا الحقول الرقمية في قوالب وقت التحميل، في مكان واحد، وثق بكل شيء في المنبع.
  • معاملة dict الوصفة كسلطته الخاصة. إرجاع find_recipe مرجعًا حيًا يعني أن تغييرًا عرضيًا واحدًا يفسد قاعدة البيانات كلها لكل خطوة لاحقة. إما أرجع نسخًا أو ، بشكل أصرع ، اجعل الوصفات مقروءة فقط عبر MappingProxyType وصمّم بحيث لا تحتاج الخطة الكتابة أبدًا.
  • عنصر واحد لكل وصفة في قائمة المشتريات. ميزة «الدمج باسم المكوّن» هي بالضبط ما يفصل مخططًا عن مذكرة لاصقة؛ خطة تنتج ثلاثة أسطر rice لم تنجز. إذا أظهر ناتج الدمج مكررات، فأنت تعدّ في قائمة، لا في dict (انظر إصلاح الخطوة 4).
  • حساب كل حصة مُتجاهل. الوصفة servings: 4؛ والخطة تعامل الوجبات كوصفات كاملة. قرارات مثل «آكل نصف الكاري» تحتاج عدّ حصص صريحًا لكل إدخال خطة ، ابنِ الخانة في نموذج الخطة قبل أن تحتاجها، أو اقبل إجماليات الطبق الكامل كالافتراضي الموثق.
  • انزياح اسم المكوّن. "rice" في أربع وصفات بهجة في الدمج؛ "rice " و"Rice" و"basmati rice" ثلاثة عناصر منفصلة. الإصلاح الحقيقي قائمة مكونات معيارية تشير إليها كل وصفة (مفتاح أجنبي، حتى في CSV)، وخطوة تطبيع وقت التحميل (تجريد + تحويل لأحرف صغيرة) كالنسخة الرخيصة.

ما بنيته للتو

مخطط وجبات يعمل: قاعدة بيانات من أربع وصفات، وخطة سبعة أيام، وإجماليات سعرات حرارية وبروتين يومية، وقائمة مشتريات مدمجة حيث يعني rice x3 سطر تسوق واحدًا صادقًا. المهارة القابلة للنقل هي خط الأنابيب «خطّط ← اشتق ← أبلغ» كله: تُبقي مصدر حقيقة صغيرًا واحدًا (الوصفات)، وتتخذ قرارًا (الأسبوع)، وتحسب كل مخرج (الإجماليات، قائمة المشتريات) من الاثنين ، لا شيء قديم، لا شيء مُصان باليد. نفس الشكل يدير إعداد الوجبات والميزانيات والجداول وأي «مُدخلات قابلة للتكرار، تقرير مشتق» مهمة تقابلك.

شغّل نسخة أكمل دون أي إعداد محلي

examples/meal-planner/ في مستودع الدورة يحزم قواعد البيانات ودفترًا يشغّل البناء → الإجماليات → قائمة المشتريات خلية بخلية. استنسخه، أو افتح المستودع كله في GitHub Codespace، وشغّل خط الأنابيب في نافذة متصفح.

إلى أين تذهب من هنا

  • أضف وعيًا بالحصص: إدخال خطة مثل ("Veggie curry", 2) يقيس المكونات ويضاعف التغذية أو ينصفها ، الضرب الواحد من الخطوة 3.3 مجسَّدًا.
  • تتبّع كلفة التسوق: أعطِ كل مكوّن سعرًا لكل وحدة، واطبع إجمالي الأسبوع ، اشتقاق ثانٍ معلق على البيانات نفسها.
  • أضف ميزة أسبوع عشوائي: plan --random يختار سبع وصفات (بلا تكرار، أو متجانسة أيام الأسبوع عن قصد) ويطبع الخطة المشتقة فورًا.
  • انقل قاعدة البيانات إلى JSON بدلًا من CSV ، json.load يُبقي الأرقام مكتوبة في قوالب مجانًا ويتعامل مع المكونات الحاملة للفواصل دون صداع اقتباس، وهو تبديل خمس دقائق الآن بعد أن يملك المُحمّل مخططًا مستقرًا.

شارك مشروعك مع الصف

بنيت شيئًا فخورًا به ، خطة أسبوع، أو قائمة مشتريات، أو قاعدة بيانات وصفات تطبخ منها فعلًا؟ examples/student-projects/ معرض لمشاريع طلاب آخرين قدَّموها، وملف README الخاص به يرشدك من البداية إلى النهاية لإضافة مشروعك عبر pull request: عمل fork والتفريع والتثبيت وفتح الـ PR. لا يُفترَض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة Python خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.