تا اینجا یک ورودی نامعتبر می‌توانست برنامه را متوقف کند و خروج از برنامه هم داده‌های فهرست را از بین می‌برد. این شب دو مسئله را حل می‌کند: خطای قابل انتظار را به پیام روشن تبدیل می‌کنیم و داده را در فایل JSON نگه می‌داریم. پیش‌نیاز، تابع و دیکشنری شب سوم است. در پایان می‌توانید سرور تازه‌ای را بدون حذف رکوردهای قبلی به فهرست محلی اضافه کنید.

خطا را در محل مناسب بگیرید#

اجرای int("abc") مقدار قابل استفاده تولید نمی‌کند؛ استثنای ValueError رخ می‌دهد. استثنا راه پایتون برای اعلام شکست یک عملیات است. اگر آن را مدیریت نکنیم، اجرای عادی متوقف می‌شود و اطلاعات خطا نمایش داده می‌شود. برای ورودی کاربر، این شکست قابل پیش‌بینی است و می‌توانیم به‌جای توقف ناگهانی، پیام مناسبی بدهیم.

بلوک try عملیات احتمالاً ناموفق را در بر می‌گیرد و except ValueError فقط همان نوع خطا را می‌گیرد. else متصل به try زمانی اجرا می‌شود که هیچ استثنایی از بلوک try بیرون نیامده باشد. این else را با شاخهٔ جایگزین یک if اشتباه نگیرید؛ اینجا شرط تازه‌ای مطرح نیست، بلکه موفقیت اجرای عملیات مطرح است.

در پوشهٔ ~/python-backend/night-04 فایل safe_config.py را بسازید. بازهٔ یک تا 65535 قرارداد درگاه مقصد در این تمرین است. پورت صفر در بعضی کاربردهای شبکه معنای ویژه دارد، ولی برای نشانی سرویس ما پذیرفته نمی‌شود. پس حتی پس از تبدیل موفق هم باید بازه را بررسی کنیم.

service = input("Service name: ")
try:
    port = int(input("Service port: "))
except ValueError:
    print("ERROR: Port must be a whole number")
else:
    if not 1 <= port <= 65535:
        print("ERROR: Port must be between 1 and 65535")
    else:
        print(f"Service: {service}")
        print(f"URL: http://localhost:{port}")
python3 safe_config.py

ورودی 8081 باید گزارش معتبر بسازد؛ eight-thousand باید پیام خطای عدد و 70000 باید پیام خطای بازه بدهد. این سه حالت عمداً متفاوت‌اند: شکست تبدیل، مقدار عددی نامعتبر و موفقیت کامل. هنگام بازبینی مطمئن شوید پس از خطای تبدیل هیچ محاسبه‌ای با متغیر port انجام نمی‌شود، چون ممکن است هنوز مقدار نگرفته باشد.

از except: بدون نام خطا پرهیز کنید. چنین بلوکی خطاهای نامرتبط و حتی برخی وقفه‌های کاربر را هم می‌پوشاند و ممکن است اشکال برنامه را شبیه ورودی نامعتبر نشان دهد. هرچه محدودهٔ try کوچک‌تر و نوع خطا مشخص‌تر باشد، تشخیص علت آسان‌تر می‌شود. عملیات بعدی را فقط زمانی داخل آن بگذارید که واقعاً می‌خواهید خطای همان عملیات را هم مدیریت کنید.

تمرین safe_storage.py را با دو تبدیل float() در یک try بنویسید. پس از موفقیت، کل باید مثبت، مصرف غیرمنفی و حداکثر برابر کل باشد. برای کامل بودن بررسی، با math.isfinite() مقدارهای نامتناهی و NaN را هم رد کنید؛ float() برخی متن‌های غیرمعمول را به چنین مقدارهایی تبدیل می‌کند. سپس درصد را از مقدار اصلی دسته‌بندی و فقط برای نمایش گرد کنید: سالم زیر 75، هشدار از 75 تا کمتر از 90 و بحرانی از 90 به بالا.

JSON متن است، دیکشنری شیء داخل برنامه#

JSON کوتاه‌شدهٔ JavaScript Object Notation و قالب متنی تبادل داده است. برای استفاده از آن لازم نیست JavaScript بدانید. ماژول استاندارد json دیکشنری را به object، فهرست را به array، رشته را به string و عدد را به number تبدیل می‌کند. True و False در فایل به true و false و None به null تبدیل می‌شوند. این تفاوت املایی را خود ماژول مدیریت می‌کند.

json.dump() داده را در فایل می‌نویسد و json.load() آن را از فایل می‌خواند. دو تابع dumps() و loads() کار مشابه را با رشته انجام می‌دهند؛ در این شب به نسخهٔ فایل نیاز داریم. هنگام بازکردن فایل، encoding="utf-8" را مشخص می‌کنیم تا متن فارسی هم به شیوهٔ معین ذخیره شود. ensure_ascii=False خواندن متن فارسی داخل فایل را راحت‌تر می‌کند.

بلوک with فایل را پس از خروج از بدنه می‌بندد، حتی اگر خطایی رخ دهد. حالت r برای خواندن و w برای نوشتن است. بازکردن فایل موجود با w محتوای قبلی را کوتاه و جایگزین می‌کند؛ بنابراین پیش از اجرای نمونه روی فایل مهم، از آن نسخهٔ پشتیبان بگیرید. در پوشهٔ تازهٔ آزمایشگاهی، فایل json_inventory.py را با کد زیر بسازید.

import json

servers = [
    {"hostname": "pve01", "address": "192.168.10.11",
     "role": "hypervisor", "online": True},
    {"hostname": "nexus01", "address": "192.168.10.20",
     "role": "registry", "online": False},
]
with open("servers.json", "w", encoding="utf-8") as file:
    json.dump(servers, file, indent=2, ensure_ascii=False)

with open("servers.json", "r", encoding="utf-8") as file:
    loaded_servers = json.load(file)

for server in loaded_servers:
    print(f'{server["hostname"]} | {server["address"]} | {server["role"]}')
print(f"Total servers: {len(loaded_servers)}")
python3 json_inventory.py
cat servers.json

دو رکورد و تعداد دو انتظار می‌رود. در فایل، false با حروف کوچک دیده می‌شود و تورفتگی دو فاصله‌ای از indent=2 آمده است. این گزینه فقط خوانایی متن را تغییر می‌دهد و ساختار منطقی داده همان است. فایل نسبت به پوشهٔ جاری ترمینال ساخته می‌شود؛ اجرای برنامه از پوشه‌ای دیگر می‌تواند فایل متفاوتی بسازد. هنگام پیدا نکردن داده، نخست پوشهٔ جاری را بررسی کنید.

تمرین افزودن بدون فراموش‌کردن گذشته#

فایل add_server.py ابتدا باید servers.json را بخواند. فقط اگر FileNotFoundError رخ داد، با فهرست خالی شروع کنید. سپس نام میزبان، آدرس و نقش را بگیرید، دیکشنری تازه را اضافه کنید و کل فهرست به‌روز را بنویسید. اگر از ابتدا فهرست خالی بسازید یا فایل را پیش از خواندن با w باز کنید، رکوردهای قبلی از دست می‌روند.

try:
    with open("servers.json", "r", encoding="utf-8") as file:
        servers = json.load(file)
except FileNotFoundError:
    servers = []

بار اول pve01 و بار دوم nexus01 را وارد کنید؛ پس از اجرای دوم باید هر دو در فایل باشند. همچنین فایل JSON خراب را در یک نسخهٔ آزمایشی امتحان کنید. json.JSONDecodeError با نبود فایل فرق دارد: پیام روشن بدهید و عملیات نوشتن را متوقف کنید تا دادهٔ خراب برای بررسی باقی بماند. تبدیل خودکار هر خطای خواندن به فهرست خالی می‌تواند به حذف بی‌صدای داده منجر شود.

موفقیت خواندن JSON فقط قالب متن را تأیید می‌کند؛ تضمین نمی‌کند نتیجه حتماً فهرستی از رکوردهای مناسب است. برای تمرین تکمیلی، نوع نتیجه و وجود کلیدهای لازم را هم بررسی کنید. این روش فایل ساده برای یک اجرای محلی مناسب است؛ نوشتن هم‌زمان چند برنامه، قطع ناگهانی هنگام ذخیره و بازیابی داده هنوز حل نشده‌اند. در بخش SQLite، ذخیره‌سازی را به سازوکاری با تراکنش می‌سپاریم.

برای خودآزمایی بگویید else پس از try چه زمانی اجرا می‌شود، تفاوت نبود فایل با JSON خراب چیست و چرا with به باز و بسته‌کردن دستی ترجیح دارد. پاسخ باید به رفتار واقعی مربوط باشد: خطای شناخته‌شده را مهار می‌کنیم، اما دادهٔ قبلی و نشانهٔ اشکال غیرمنتظره را بی‌دلیل از بین نمی‌بریم.

برای بررسی ذخیره‌سازی، فقط به پیام «ذخیره شد» اکتفا نکنید. برنامه را ببندید، دوباره باز کنید و دادهٔ قبلی را بخوانید؛ این آزمایش نشان می‌دهد رکورد واقعاً از فایل برگشته است. همچنین یک نسخهٔ آزمایشی را عمداً با JSON نامعتبر پر کنید و مطمئن شوید برنامه پیش از نوشتن متوقف می‌شود. معیار موفقیت مدیریت خطا فقط نبود پیام طولانی نیست؛ حفظ داده و روشن بودن علت شکست هم بخشی از رفتار درست است.

منابع#