مقدمه
تا اینجا از دوره، با اصول ساخت کلاس، ارثبری، ترکیب و اصول SOLID آشنا شدهاید. اما یک برنامهی واقعی، به سه قابلیت حیاتی دیگر نیز نیاز دارد که اغلب در آموزشهای مقدماتی نادیده گرفته میشوند: مدیریت صحیح خطاها، ذخیرهسازی و انتقال اشیاء، و بررسی اشیاء در زمان اجرا. این سه ابزار، تفاوت میان یک کد «که کار میکند» و یک کد «حرفهای و قابل نگهداری» را مشخص میکنند.
چرا این مباحث را یاد بگیریم؟
- برای نوشتن کدی که خطاها را به طور شفاف مدیریت میکند و به شما امکان میدهد تا به سرعت مشکل را پیدا کنید.
- برای ذخیرهسازی دادههای برنامه در فایل یا پایگاه داده و همچنین ارسال آنها از طریق شبکه.
- برای نوشتن ابزارهای دیباگ، کتابخانهها و فریمورکهایی که میتوانند ساختار اشیاء را در زمان اجرا کشف کنند.
- برای درک بهتر عملکرد فریمورکهای محبوبی مانند جنگو (Django) که به شدت از این مفاهیم استفاده میکنند.
بخش اول: استثناها (Exceptions) – مدیریت هوشمندانهی خطاها
استثناهای آماده در پایتون چه هستند؟
پایتون استثناهای داخلی زیادی دارد، مانند ValueError، TypeError و KeyError. اینها برای خطاهای عمومی زبان مناسب هستند. اما در یک پروژهی واقعی، با خطاهایی روبرو میشوید که مختص دامنهی کاری شما هستند. برای نمونه: «موجودی حساب کافی نیست»، «کارت اعتباری منقضی شده است»، یا «کاربر دسترسی لازم را ندارد».
اگر برای همهی این موارد از یک استثنای عمومی مانند ValueError استفاده کنید، اطلاعات مفیدی را از دست میدهید. زیرا وقتی یک خطا رخ میدهد، نمیتوانید به راحتی تشخیص دهید که دقیقاً چه نوع خطایی رخ داده است و بر اساس آن، رفتار متفاوتی از خود نشان دهید.
چرا باید استثنای سفارشی بسازیم؟
با تعریف استثنای سفارشی، این مشکل را حل میکنید. شما خطاهای خودتان را میسازید که دقیقاً نشاندهندهی مشکلات دنیای واقعی برنامهتان هستند. این کار سه مزیت بزرگ دارد:
- شفافیت: نام استثنا خودش توضیح میدهد که چه خطایی رخ داده است (مثلاً
InsufficientFundsError). - قابلیت کنترل: میتوانید در بخشهای مختلف برنامه، خطاهای خاص را به صورت جداگانه مدیریت کنید.
- حمل داده: میتوانید اطلاعات مفید مربوط به خطا (مانند مبلغ مورد نیاز و موجودی) را به همراه استثنا منتقل کنید.
ساختار استثنای سفارشی
برای ساختن یک استثنای سفارشی، کافی است یک کلاس جدید تعریف کنید که از کلاس پایهی Exception یا یکی از زیرکلاسهای آن، ارثببرد. (مبحث ارثبری را در فصل چهارم به طور کامل بررسی کردیم).
# تعریف یک استثنای پایه برای تمام خطاهای مربوط به پرداخت
class PaymentError(Exception):
pass
# تعریف یک استثنای خاص برای خطای «موجودی ناکافی»
class InsufficientFundsError(PaymentError):
def __init__(self, needed, available):
self.needed = needed
self.available = available
# پیام خطایی که به نمایش داده میشود
super().__init__(f"Required: {needed}, Available: {available}")
نکتهی کلیدی: با ارثبری از Exception، استثنای شما تمام قابلیتهای یک استثنای استاندارد را خواهد داشت. همچنین، شما میتوانید دادههای اضافی (مانند needed و available) را به آن اضافه کنید تا در بخش مدیریت خطا، از آنها استفاده کنید.
استفاده از استثنای سفارشی
def process_payment(amount, balance):
if amount > balance:
# پرتاب کردن استثنای سفارشی
raise InsufficientFundsError(amount, balance)
print("Payment successful!")
# استفاده از تابع و مدیریت خطا
try:
process_payment(1000, 300)
except InsufficientFundsError as e:
# دسترسی به دادههای اضافی استثنا
print(f"Error: {e}")
print(f"Needed: {e.needed}, Available: {e.available}")
خروجی کد بالا به این صورت خواهد بود:
Error: Required: 1000, Available: 300
Needed: 1000, Available: 300
طراحی سلسلهمراتب استثنا (Exception Hierarchy)
قدرت واقعی استثناهای سفارشی، زمانی آشکار میشود که یک سلسلهمراتب برای آنها طراحی کنید. این کار به شما اجازه میدهد تا خطاها را در سطوح مختلف (از کلی به جزئی) بگیرید.
# استثنای پایه برای کل پروژه
class ShopError(Exception):
pass
# زیرشاخهی خطاهای پرداخت
class PaymentError(ShopError):
pass
class InsufficientFundsError(PaymentError):
pass
class CardExpiredError(PaymentError):
pass
# زیرشاخهی خطاهای موجودی
class InventoryError(ShopError):
pass
class OutOfStockError(InventoryError):
pass
حالا میتوانید در کد خود، خطاها را در هر سطحی که نیاز دارید، بگیرید:
# سطح ۱: گرفتن یک خطای خاص
try:
# عملی که ممکن است OutOfStockError ایجاد کند
raise OutOfStockError("Keyboard is out of stock")
except OutOfStockError as e:
print(f"Item is out of stock: {e}")
# سطح ۲: گرفتن تمام خطاهای مربوط به موجودی
try:
# عملی که ممکن است هر نوع InventoryError ایجاد کند
raise OutOfStockError("Keyboard is out of stock")
except InventoryError as e:
print(f"Inventory problem: {e}")
# سطح ۳: گرفتن هر خطایی از کل فروشگاه
try:
# هر عملیات فروشگاهی
raise OutOfStockError("Keyboard is out of stock")
except ShopError as e:
print(f"Shop error: {e}")
این انعطافپذیری، به شما امکان میدهد تا منطق مدیریت خطا را دقیقاً به همان اندازهای که نیاز دارید، جزئی یا کلی طراحی کنید.
قاعدهی طلایی: برای هر ماژول یا کتابخانهای که مینویسید، یک استثنای پایه (مانند ShopError) تعریف کنید و تمام استثناهای دیگر را از آن مشتق کنید. این کار به کاربران کتابخانهی شما اجازه میدهد تا با یک except ShopError تمام خطاهای شما را بگیرند.
مدیریت منابع با try/finally و with
برای مدیریت منابعی مانند فایلها، اتصالات شبکه یا قفلها، باید مطمئن شوید که پس از استفاده، به درستی بسته یا آزاد میشوند. روش سنتی، استفاده از try/finally است:
f = open("data.txt", "w")
try:
f.write("Hello, World!")
finally:
f.close() # حتی اگر خطایی رخ دهد، فایل بسته میشود
اما روش تمیزتر و پایتونیتر، استفاده از مدیریت زمینه (Context Manager) با کلمهی کلیدی with است که در فصل پنجم با آن آشنا شدید:
with open("data.txt", "w") as f:
f.write("Hello, World!")
# فایل به طور خودکار و حتی در صورت بروز خطا، بسته میشود
زنجیرهای کردن استثناها با raise ... from ...
گاهی لازم است یک استثنا را بگیرید و به جای آن، استثنای دیگری را پرتاب کنید (مثلاً برای پنهان کردن جزئیات فنی سطح پایین). برای حفظ علت اصلی خطا، از raise ... from ... استفاده میکنیم:
class ConfigError(Exception):
pass
def load_config(file_path):
try:
with open(file_path) as f:
return f.read()
except FileNotFoundError as e:
# یک استثنای جدید با سطح بالاتر پرتاب میکنیم، اما علت اصلی را حفظ میکنیم
raise ConfigError(f"Config file not found: {file_path}") from e
try:
config = load_config("config.ini")
except ConfigError as e:
print(f"Error: {e}")
# خطای اصلی (FileNotFoundError) در e.__cause__ قابل دسترسی است
print(f"Original cause: {e.__cause__}")
در traceback (گزارش خطا)، هر دو خطا نمایش داده میشوند و رابطهی علت و معلولی آنها مشخص است.
بخش دوم: سریالسازی (Serialization) – ذخیره و انتقال اشیاء
سریالسازی به فرآیند تبدیل یک شیء در حافظه (با ساختار پیچیده) به قالبی (مانند رشته یا بایت) گفته میشود که بتوان آن را روی دیسک ذخیره کرد یا از طریق شبکه ارسال نمود. به فرآیند معکوس، Deserialization (بازسازی) میگویند.
سریالسازی به فرمت JSON
JSON یک فرمت متنی، سبک و استاندارد است که تقریباً همهی زبانهای برنامهنویسی از آن پشتیبانی میکنند. اما JSON به طور پیشفرض فقط با انواع سادهی داده (دیکشنری، لیست، رشته، عدد، بولین و null) کار میکند.
برای سریال کردن یک شیء سفارشی، باید آن را به یک دیکشنری تبدیل کنیم. یک روش استاندارد، اضافه کردن متدهای to_dict و from_dict به کلاس است:
import json
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def to_dict(self):
"""تبدیل شیء به دیکشنری برای سریالسازی"""
return {"name": self.name, "age": self.age}
@classmethod
def from_dict(cls, data):
"""بازسازی شیء از دیکشنری (یک Factory Method)"""
return cls(data["name"], data["age"])
user = User("Ali", 30)
# سریالسازی: تبدیل شیء به رشتهی JSON
json_string = json.dumps(user.to_dict(), ensure_ascii=False)
print(json_string) # {"name": "Ali", "age": 30}
# بازسازی: ساخت شیء از رشتهی JSON
restored_user = User.from_dict(json.loads(json_string))
print(restored_user.name) # Ali
سریالسازی با pickle – قدرتمند اما خطرناک
ماژول pickle یک شیء پایتونی را به یک دنبالهی بایت (باینری) تبدیل میکند. این روش بسیار قدرتمند است و میتواند تقریباً هر شیء پایتونی (حتی توابع و کلاسها) را سریال کند.
import pickle
user = User("Sara", 25)
# سریالسازی به بایت
byte_data = pickle.dumps(user)
# بازسازی شیء از بایت
restored_user = pickle.loads(byte_data)
print(restored_user.name) # Sara
نکتهی بسیار مهم امنیتی: pickle هنگام بازسازی (loads)، میتواند کد دلخواه اجرا کند. این بدان معناست که اگر یک فایل pickle مخرب را باز کنید، برنامهنویس آن میتواند هر دستوری (حتی پاک کردن فایلها) را روی سیستم شما اجرا کند. به همین دلیل، هرگز دادهی pickle را که از یک منبع نامطمئن (کاربر، شبکه، اینترنت) دریافت کردهاید، باز نکنید.
قاعدهی طلایی امنیتی: از
pickleفقط برای دادههای داخلی و قابل اعتماد خودتان استفاده کنید. برای تبادل داده با دنیای خارج، از JSON یا فرمتهای امن مشابه استفاده کنید.
بخش سوم: Introspection (دروننگری) – بررسی اشیاء در زمان اجرا
Introspection یا دروننگری، یعنی توانایی یک برنامه برای بررسی نوع، ساختار و ویژگیهای اشیاء در حین اجرا. این قابلیت، پایه و اساس بسیاری از کتابخانهها و فریمورکهای پیشرفتهی پایتون (مانند جنگو و Pytest) است.
بررسی نوع: type() در برابر isinstance()
سادهترین عمل دروننگری، پرسیدن نوع یک شیء است. پایتون دو تابع برای این کار دارد که تفاوت مهمی با هم دارند:
class Animal: pass
class Dog(Animal): pass
d = Dog()
print(type(d) == Dog) # True
print(type(d) == Animal) # False (چون type فقط نوع دقیق را میدهد)
print(isinstance(d, Animal)) # True (چون isinstance به سلسلهمراتب احترام میگذارد)
تفاوت کلیدی: type() فقط نوع دقیق را بررسی میکند، اما isinstance سلسلهمراتب ارثبری را نیز در نظر میگیرد و بررسی میکند که آیا شیء، نمونهای از آن کلاس یا زیرکلاسهای آن است.
قاعده: در برنامهنویسی شیءگرا، تقریباً همیشه از isinstance استفاده کنید. استفاده از type() برای بررسی نوع، چندریختی (Polymorphism) را از بین میبرد و کد شما را در برابر اضافه شدن زیرکلاسهای جدید در آینده، شکننده میکند. (مبحث چندریختی را در فصل چهارم کامل بررسی کردیم).
برای بررسی رابطهی کلاسها نیز از issubclass استفاده میشود:
print(issubclass(Dog, Animal)) # True
دسترسی به دادهها و ویژگیها: __dict__
هر شیء در پایتون، یک دیکشنری به نام __dict__ دارد که تمام attributeهای (ویژگیهای) نمونه را در خود نگهداری میکند. این برای دیباگ و مشاهدهی دادههای واقعی یک شیء بسیار مفید است.
class Product:
category = "General" # متغیر کلاس
def __init__(self, name):
self.name = name # متغیر نمونه
p = Product("Keyboard")
print(p.__dict__) # {'name': 'Keyboard'} (فقط متغیرهای نمونه)
کار با attributeهای پویا: hasattr، getattr و setattr
این توابع به شما اجازه میدهند تا با استفاده از رشتهها، به attributeهای یک شیء دسترسی داشته باشید. این کار زمانی مفید است که نام attribute در زمان نوشتن کد مشخص نیست (مثلاً از یک فایل پیکربندی خوانده میشود).
user = User("Ali", 30)
# وجود attribute را بررسی کنید
if hasattr(user, "name"):
# مقدار آن را دریافت کنید (با مقدار پیشفرض برای حالت عدم وجود)
name = getattr(user, "name", "Unknown")
print(name)
# مقدار یک attribute را به صورت پویا تنظیم کنید
setattr(user, "age", 31)
print(user.age) # 31
این قابلیت، سنگ بنای بسیاری از فریمورکها برای انجام کارهایی مانند خواندن پویای تنظیمات است.
یک مثال ملموستر: فرض کنید میخواهید از بین چند شیء، یکی را بهصورت تصادفی «رئیس» کنید و بعداً بررسی کنید هر شیء رئیس هست یا نه. چون این ویژگی قرار نیست روی همهی شیءها وجود داشته باشد (یک attribute اکتسابی است، نه ذاتی)، دقیقاً همینجا باید از hasattr کمک بگیریم:
import random
class Human:
def __init__(self, name):
self.name = name
humans = [Human("Ali"), Human("Alex"), Human("John"), Human("Marta")]
boss = random.choice(humans)
boss.is_boss = True # ویژگی اکتسابی و موقت، فقط روی همین یک shi
for human in humans:
if hasattr(human, "is_boss"):
print(f"{human.name} is boss")
else:
print(f"{human.name} is not boss")
هیچکدام از این افراد از بدو تولد ویژگی is_boss ندارند — این ویژگی فقط بعد از انتخاب تصادفی و فقط روی همان یک شیء اضافه شد. بدون hasattr، دسترسی مستقیم به human.is_boss برای کسانی که این ویژگی را ندارند، یک AttributeError پرتاب میکند.
خلاصه
در این فصل با سه ابزار قدرتمند برای حرفهایتر کردن کد خود آشنا شدید:
- استثناهای سفارشی برای مدیریت خطاهای شفاف، ساختارمند و قابل کنترل.
- سریالسازی با JSON (برای تبادل امن داده) و
pickle(برای ذخیرهسازی داخلی، با رعایت دقیق نکات امنیتی). - دروننگری (Introspection) برای بررسی پویای اشیاء، که پایهی بسیاری از کتابخانههای پیشرفته است.
| موضوع | ابزارهای کلیدی | کاربرد اصلی |
|---|---|---|
| استثناها | ارثبری از Exception، raise from | مدیریت خطاهای دامنهی خاص و حفظ علت اصلی |
| سریالسازی | json، pickle | ذخیرهسازی و انتقال داده |
| دروننگری | isinstance، __dict__، getattr | بررسی و کشف ساختار اشیاء در زمان اجرا |
نکتهی امنیتی طلایی: هرگز دادهی pickle را از منابع نامطمئن دریافت نکنید. و در طراحی استثناها، با ایجاد یک سلسلهمراتب، مسیر مدیریت خطا را برای همیشه هموار کنید. Introspection ابزاری قدرتمند است، اما استفادهی افراطی از آن میتواند کد را برای دیگران غیرقابلفهم کند.
پرسشهای مصاحبه
۱. چرا باید از استثنای سفارشی استفاده کنیم؟
برای ایجاد خطاهای معنادار در دامنهی کسبوکار خود. این کار به شما اجازه میدهد خطاها را با دقت بیشتری مدیریت کنید، دادههای مرتبط را به همراه خطا منتقل کنید، و با ایجاد سلسلهمراتب، سطح انتزاع مناسبی برای مدیریت خطا داشته باشید.
۲. تفاوت raise ... from ... و یک raise ساده چیست؟raise ... from e زنجیرهای از علتها را در traceback حفظ میکند و نشان میدهد که خطای سطح بالا، به دلیل خطای سطح پایینتری رخ داده است. این کار برای دیباگ بسیار مفید است.
۳. چرا pickle برای دادههای نامطمئن خطرناک است؟
زیرا فرآیند deserialization در pickle میتواند باعث اجرای کد دلخواه شود. یک فایل pickle مخرب، عملاً یک برنامه است که میتواند هر کاری روی سیستم شما انجام دهد. از آن فقط برای دادههای قابل اعتماد خودتان استفاده کنید.
در فصل بعد: با در دست داشتن تمام این ابزارها، به سراغ الگوهای طراحی (Design Patterns) میرویم. این الگوها، راهحلهای آزمودهشدهای برای مسائل تکراری در طراحی نرمافزار هستند که طی دههها توسط مهندسان خبره کشف و تدوین شدهاند.