۲. کلاس Shop: ساختن، ذخیره و بارگذاری

خط ۸ تا ۲۶

کلاس Shop مغز برنامه است و هیچ ربطی به ظاهر پنجره ندارد. دو داده نگه می‌دارد: دیکشنری کالاها و لیست فروش‌ها. در این بخش سه متد اول را می‌بینیم: سازنده که شیء را آماده می‌کند، save که همه‌چیز را در فایل می‌نویسد و load که آن را برمی‌گرداند.

کد این بخش

shop_manager.py, lines 8–26
8class Shop:9    def __init__(self):10        self.products = {}11        self.sales = []12        self.load()13 14    def save(self):15        with open(FILE, 'w', encoding='utf-8') as f:16            json.dump({'products': self.products, 'sales': self.sales}, f, ensure_ascii=False, indent=2)17 18    def load(self):19        if os.path.exists(FILE):20            try:21                with open(FILE, 'r', encoding='utf-8') as f:22                    data = json.load(f)23                self.products = data.get('products', {})24                self.sales = data.get('sales', [])25            except (OSError, json.JSONDecodeError, AttributeError):26                self.products, self.sales = {}, []

روی شماره‌ی هر خط بزن تا به توضیحش بروی؛ روی «خط …» در توضیح بزن تا همان خط در کد روشن شود.

توضیح خط به خط

خط ۸class Shop:
تعریف کلاس Shop. کلاس قالبی است که داده‌ها و رفتارهای مربوط به هم را کنار هم نگه می‌دارد. این خط هنوز چیزی نمی‌سازد؛ شیء واقعی در خط ۶۵ با Shop() ساخته می‌شود.
خط ۹def __init__(self):
متد سازنده. پایتون هر بار که Shop() صدا زده شود این متد را خودکار اجرا می‌کند. self خودِ شیئی است که در حال ساخته شدن است و با آن ویژگی‌ها را روی شیء می‌گذاریم.
خط ۱۰self.products = {}
دیکشنری خالی کالاها. کلید هر عضو نام کالاست و مقدارش یک دیکشنری کوچک مثل {'buy': 1000, 'sell': 1500, 'qty': 3} (قیمت خرید، قیمت فروش، موجودی).
خط ۱۱self.sales = []
لیست خالی فروش‌ها. هر فروش یک دیکشنری مثل {'name': 'مداد', 'qty': 2, 'total': 3000} است که در خط ۵۹ اضافه می‌شود.
خط ۱۲self.load()
بلافاصله داده‌های ذخیره‌شده را از فایل می‌خواند. ترتیب مهم است: اول مقدار پیش‌فرض خالی ساخته شده (خط ۱۰ و خط ۱۱)، پس اگر فایلی نباشد، شیء با همان مقدارهای خالی کار می‌کند.
خط ۱۳
خط خالی بین دو متد.
خط ۱۴def save(self):
متد save همه‌ی داده‌ی شیء را در فایل می‌نویسد. ورودی‌ای جز self ندارد چون همه‌چیز را از خود شیء برمی‌دارد.
خط ۱۵with open(FILE, 'w', encoding='utf-8') as f:
فایل را در حالت نوشتن ('w') باز می‌کند. حالت 'w' اگر فایل باشد محتوایش را پاک می‌کند و از نو می‌نویسد، اگر نباشد می‌سازد. with تضمین می‌کند فایل در پایان بسته شود، حتی اگر وسط کار خطا رخ دهد. encoding='utf-8' برای درست ذخیره شدن حروف فارسی لازم است. f نام متغیر فایل باز است.
خط ۱۶json.dump({'products': self.products, 'sales': self.sales}, f, ensure_ascii=False, indent=2)
یک دیکشنری با دو کلید products و sales می‌سازد و با json.dump در فایل f می‌نویسد. ensure_ascii=False باعث می‌شود فارسی همان‌طور خوانا ذخیره شود و به شکل \u0645\u062f در نیاید. indent=2 هر سطح را دو فاصله تو می‌برد تا فایل برای آدم هم خوانا باشد.
خط ۱۷
خط خالی.
خط ۱۸def load(self):
متد load داده‌ها را از فایل می‌خواند و روی شیء می‌گذارد. فقط از سازنده (خط ۱۲) صدا زده می‌شود.
خط ۱۹if os.path.exists(FILE):
اول بررسی می‌کند فایل وجود دارد. بار اولی که برنامه اجرا می‌شود فایلی نیست و کل این بلوک رد می‌شود.
خط ۲۰try:
شروع بلوک try. هر خطایی که در خطوط زیرش رخ دهد، به‌جای بستن برنامه به بخش except (خط ۲۵) می‌رود.
خط ۲۱with open(FILE, 'r', encoding='utf-8') as f:
فایل را در حالت خواندن ('r') و با همان کدگذاری utf-8 باز می‌کند.
خط ۲۲data = json.load(f)
json.load متن فایل را می‌خواند و به ساختار پایتون (اینجا یک دیکشنری) تبدیل می‌کند و در data می‌گذارد.
خط ۲۳self.products = data.get('products', {})
مقدار کلید products را برمی‌دارد. get اگر کلید نباشد به‌جای خطای KeyError مقدار دوم، یعنی {}، را برمی‌گرداند.
خط ۲۴self.sales = data.get('sales', [])
همین کار برای sales، با مقدار پیش‌فرض لیست خالی [].
خط ۲۵except (OSError, json.JSONDecodeError, AttributeError):
سه نوع خطا را می‌گیرد: OSError وقتی فایل باز نشود (مثلاً دسترسی نباشد)، json.JSONDecodeError وقتی محتوای فایل JSON معتبر نباشد، و AttributeError وقتی فایل JSON معتبر باشد ولی دیکشنری نباشد (مثلاً یک لیست)، چون لیست متد get ندارد. پرانتز یعنی «هر کدام از این‌ها».
خط ۲۶self.products, self.sales = {}, []
در صورت خطا، هر دو را با یک انتساب چندگانه خالی می‌کند تا برنامه از صفر شروع کند. سمت راست یک تاپل دوتایی است که به ترتیب در دو متغیر سمت چپ ریخته می‌شود.

یادداشت

اگر فایل خراب باشد، برنامه بی‌صدا با داده‌ی خالی باز می‌شود و با اولین ذخیره (خط ۱۵) فایل خراب کامل بازنویسی می‌شود. در نسخه‌ی قبلی AttributeError در خط ۲۵ نبود و اگر محتوای فایل مثلاً [1, 2] بود، برنامه هنگام باز شدن کرش می‌کرد.

ارتباط با بقیه‌ی کد

همچنین ببینید

Shop
در خط ۶۵ یک نمونه از آن با نام shop ساخته می‌شود و همه‌ی توابع رابط کاربری از همان استفاده می‌کنند.
self.products
در add خط ۳۳، edit خط ۴۱، delete خط ۴۷، sell خط ۵۵ و در جدول خط ۱۱۶ خوانده یا تغییر داده می‌شود.
self.sales
در sell خط ۵۹ عضو جدید می‌گیرد و در report خط ۶۳ جمع زده می‌شود.
save()
بعد از هر تغییر صدا زده می‌شود: خط ۳۴، خط ۴۲، خط ۴۸ و خط ۶۰. خطای فایلش (OSError) در خط ۹۰ گرفته می‌شود.
load()
فقط یک بار، در سازنده خط ۱۲.

سؤال‌های تمرینی

اول خودت جواب بده، بعد جواب را باز کن.

  1. بار اولی که برنامه اجرا می‌شود و فایل داده وجود ندارد چه اتفاقی می‌افتد؟

    نمایش جواب

    شرط خط ۱۹ نادرست است، پس load کاری نمی‌کند و شیء همان {} و [] خالیِ خط ۱۰ و خط ۱۱ را نگه می‌دارد.

  2. with در خط ۱۵ چه چیزی می‌دهد که f = open(...) ساده نمی‌دهد؟

    نمایش جواب

    فایل در پایان بلوک خودکار بسته می‌شود، حتی اگر json.dump خطا بدهد.

  3. فرق data.get('products', {}) با data['products'] چیست؟

    نمایش جواب

    اگر کلید نباشد، get مقدار {} را برمی‌گرداند ولی data['products'] خطای KeyError می‌دهد.

  4. محتوای فایل داده [1, 2] است. وقتی برنامه باز می‌شود چه می‌شود؟

    نمایش جواب

    json.load یک لیست برمی‌گرداند، data.get خطای AttributeError می‌دهد، خط ۲۵ آن را می‌گیرد و خط ۲۶ داده‌ها را خالی می‌کند.