Python Learningاز صفر تا نوشتن پروژه از حفظ
درس ۱۳ · ساخت پروژه

ساخت پروژه ۱: مغز برنامه (کلاس Shop)

حدود ۹۰ دقیقه۶ تمرین۶ سؤال آزمون
بعد از این درس می‌توانی:
  • کلاس Shop را با همه‌ی متدها خودت بنویسی
  • هر قدم را با تست خودکار چک کنی
  • ذخیره و بارگذاری JSON را به کلاس اضافه کنی
  • در آخر کل کلاس را یک‌جا و بدون نگاه بنویسی

نقشه‌ی کار

برنامه دو نیمه دارد: مغز (کلاس Shop که داده را نگه می‌دارد و قانون‌ها را چک می‌کند) و صورت (پنجره، فرم، دکمه‌ها و جدول). مغز به پنجره هیچ ربطی ندارد، پس می‌شود آن را همین‌جا در مرورگر ساخت و با تست خودکار چک کرد. درس بعد صورت را در VS Code می‌سازی.

قدمچه می‌سازیخط‌های پروژه
۱اسکلت کلاس و سازندهخط ۸ پروژه–خط ۱۲ پروژه
۲add با چک ورودیخط ۲۸ پروژه–خط ۳۴ پروژه
۳edit و deleteخط ۳۶ پروژه–خط ۴۸ پروژه
۴sell و reportخط ۵۰ پروژه–خط ۶۳ پروژه
۵save و load با JSONخط ۱۴ پروژه–خط ۲۶ پروژه
۶همه با هم، از حفظ—
نکته

در هر قدم کد قدم قبل آماده گذاشته شده تا فقط روی قسمت جدید تمرکز کنی. save و load تا قدم ۵ خالی (pass) می‌مانند.

قدم ۱: اسکلت

تمرین

قدم ۱ — سازنده

کلاس Shop بنویس که سازنده‌اش self.products را دیکشنری خالی و self.sales را لیست خالی بگذارد و بعد self.load() را صدا بزند. save و load فعلاً pass هستند.

Ctrl + Enter
دیدن راه‌حل (اول خودت تلاش کن)
class Shop:
    def __init__(self):
        self.products = {}
        self.sales = []
        self.load()

    def save(self):
        pass  # قدم ۵

    def load(self):
        pass  # قدم ۵

قدم ۲: افزودن کالا

قانون‌ها: نام نباید خالی یا تکراری باشد؛ هر سه عدد باید بین صفر و بی‌نهایت باشند (nan و inf قبول نیست). بعد از ثبت، self.save().

تمرین

قدم ۲ — add

متد add(self, name, buy, sell, qty) را اضافه کن. خطاها ValueError باشند.

Ctrl + Enter
راهنمایی

از if not name or name in self.products: و if not all(0 <= x < float('inf') for x in (buy, sell, qty)): استفاده کن.

دیدن راه‌حل (اول خودت تلاش کن)
class Shop:
    def __init__(self):
        self.products = {}
        self.sales = []
        self.load()

    def save(self):
        pass  # قدم ۵

    def load(self):
        pass  # قدم ۵

    def add(self, name, buy, sell, qty):
        if not name or name in self.products:
            raise ValueError('نام خالی یا کالای تکراری است')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

قدم ۳: ویرایش و حذف

تمرین

قدم ۳ — edit و delete

edit مثل add است با این فرق که کالا باید وجود داشته باشد. delete(self, name) کالای موجود را با del حذف کند. هر دو در آخر save کنند.

Ctrl + Enter
دیدن راه‌حل (اول خودت تلاش کن)
class Shop:
    def __init__(self):
        self.products = {}
        self.sales = []
        self.load()

    def save(self):
        pass  # قدم ۵

    def load(self):
        pass  # قدم ۵

    def add(self, name, buy, sell, qty):
        if not name or name in self.products:
            raise ValueError('نام خالی یا کالای تکراری است')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def edit(self, name, buy, sell, qty):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def delete(self, name):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        del self.products[name]
        self.save()

قدم ۴: فروش و گزارش

sell: کالا باید باشد، تعداد بزرگ‌تر از صفر و حداکثر برابر موجودی. موجودی کم شود و رکورد {'name', 'qty', 'total'} به self.sales اضافه شود (total = تعداد × قیمت فروش). report: تاپل (جمع تعداد، جمع مبلغ).

تمرین

قدم ۴ — sell و report

دو متد sell(self, name, qty) و report(self) را اضافه کن.

Ctrl + Enter
دیدن راه‌حل (اول خودت تلاش کن)
class Shop:
    def __init__(self):
        self.products = {}
        self.sales = []
        self.load()

    def save(self):
        pass  # قدم ۵

    def load(self):
        pass  # قدم ۵

    def add(self, name, buy, sell, qty):
        if not name or name in self.products:
            raise ValueError('نام خالی یا کالای تکراری است')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def edit(self, name, buy, sell, qty):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def delete(self, name):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        del self.products[name]
        self.save()

    def sell(self, name, qty):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        if qty <= 0:
            raise ValueError('تعداد باید مثبت باشد')
        product = self.products[name]
        if qty > product['qty']:
            raise ValueError('موجودی کافی نیست')
        product['qty'] -= qty
        self.sales.append({'name': name, 'qty': qty, 'total': qty * product['sell']})
        self.save()

    def report(self):
        return sum(s['qty'] for s in self.sales), sum(s['total'] for s in self.sales)

قدم ۵: ذخیره و بارگذاری

حالا save و load واقعی را بنویس. اینجا FILE = 'shop_data.json' است؛ در VS Code از خط __file__ (درس ۱۰) استفاده می‌کنی. load باید با فایل خراب (JSON نامعتبر یا غیر دیکشنری) بی‌صدا با داده‌ی خالی شروع کند.

تمرین

قدم ۵ — save و load

save و load را کامل کن. تست، یک شیء دوم می‌سازد و چک می‌کند داده‌ها از فایل خوانده شوند.

Ctrl + Enter
دیدن راه‌حل (اول خودت تلاش کن)
import json
import os

FILE = 'shop_data.json'  # در VS Code: مسیر کنار برنامه با __file__

class Shop:
    def __init__(self):
        self.products = {}
        self.sales = []
        self.load()

    def save(self):
        with open(FILE, 'w', encoding='utf-8') as f:
            json.dump({'products': self.products, 'sales': self.sales}, f, ensure_ascii=False, indent=2)

    def load(self):
        if os.path.exists(FILE):
            try:
                with open(FILE, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                self.products = data.get('products', {})
                self.sales = data.get('sales', [])
            except (OSError, json.JSONDecodeError, AttributeError):
                self.products, self.sales = {}, []

    def add(self, name, buy, sell, qty):
        if not name or name in self.products:
            raise ValueError('نام خالی یا کالای تکراری است')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def edit(self, name, buy, sell, qty):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def delete(self, name):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        del self.products[name]
        self.save()

    def sell(self, name, qty):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        if qty <= 0:
            raise ValueError('تعداد باید مثبت باشد')
        product = self.products[name]
        if qty > product['qty']:
            raise ValueError('موجودی کافی نیست')
        product['qty'] -= qty
        self.sales.append({'name': name, 'qty': qty, 'total': qty * product['sell']})
        self.save()

    def report(self):
        return sum(s['qty'] for s in self.sales), sum(s['total'] for s in self.sales)

قدم ۶: همه با هم، از حفظ

کادر زیر خالی است. بدون نگاه کردن به قدم‌های قبل، کل کلاس را (با importها و FILE) بنویس. اگر گیر کردی، فقط همان تکه را نگاه کن و دوباره از اول بنویس. این تمرین را چند روز پشت سر هم تکرار کن تا بدون اشتباه و سریع شود.

تمرین

کل کلاس Shop از حفظ

import json، import os، FILE = 'shop_data.json' و کلاس کامل Shop با ۸ متد.

Ctrl + Enter
دیدن راه‌حل (اول خودت تلاش کن)
import json
import os

FILE = 'shop_data.json'  # در VS Code: مسیر کنار برنامه با __file__

class Shop:
    def __init__(self):
        self.products = {}
        self.sales = []
        self.load()

    def save(self):
        with open(FILE, 'w', encoding='utf-8') as f:
            json.dump({'products': self.products, 'sales': self.sales}, f, ensure_ascii=False, indent=2)

    def load(self):
        if os.path.exists(FILE):
            try:
                with open(FILE, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                self.products = data.get('products', {})
                self.sales = data.get('sales', [])
            except (OSError, json.JSONDecodeError, AttributeError):
                self.products, self.sales = {}, []

    def add(self, name, buy, sell, qty):
        if not name or name in self.products:
            raise ValueError('نام خالی یا کالای تکراری است')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def edit(self, name, buy, sell, qty):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        if not all(0 <= x < float('inf') for x in (buy, sell, qty)):
            raise ValueError('عدد منفی یا نامعتبر مجاز نیست')
        self.products[name] = {'buy': buy, 'sell': sell, 'qty': qty}
        self.save()

    def delete(self, name):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        del self.products[name]
        self.save()

    def sell(self, name, qty):
        if name not in self.products:
            raise ValueError('کالا پیدا نشد')
        if qty <= 0:
            raise ValueError('تعداد باید مثبت باشد')
        product = self.products[name]
        if qty > product['qty']:
            raise ValueError('موجودی کافی نیست')
        product['qty'] -= qty
        self.sales.append({'name': name, 'qty': qty, 'total': qty * product['sell']})
        self.save()

    def report(self):
        return sum(s['qty'] for s in self.sales), sum(s['total'] for s in self.sales)
در پروژه‌ی فروشگاه

مقایسه کن: کلاس کامل پروژه در بخش ۲ تا ۴ مستندات آمده است.

آزمون این درس

برای گذراندن این درس حداقل ۸۰٪ جواب درست لازم است. هر چند بار خواستی می‌توانی دوباره امتحان بدهی.

۱. چرا کلاس Shop را می‌شود در مرورگر تست کرد ولی پنجره را نه؟
۲. کدام چک در add هست ولی در edit نیست؟
۳. در sell، total چطور حساب می‌شود؟
۴. report() برای فروش‌های qty=2,total=40 و qty=1,total=15 چه برمی‌گرداند؟
۵. چرا load در سازنده بعد از ساختن {} و [] صدا زده می‌شود؟
۶. اگر save را در sell فراموش کنی چه می‌شود؟