از اکسپشن ها فرار نکنید - نگاهی به ساختار اکسپشن‌ها در پایتون

مقدمهکد ها بسیار آسیب پذیر و شکننده هستند!قطعا دلچسب خواهد بود اگر همیشه بتوانیم نتایج دلخواه را از برنامه ها و اسکریپت‌هایی که نوشته‌ایم بگیریم، اما همیشه اینطور نیست. عملا نرم افزار می تواند در مقابل ورودی‌های نامناسب و محیط‌های نامناسب کاملا آسیب پذیر و شکننده باشد. برنامه‌ها سعی می‌کنند خطا‌ها و اشکالات پیش آمده را به نحوی به کاربر اطلاع دهند و برنامه نویس می بایستی تک تک آنها را کنترل کند. ولی خب خیلی‌ها این زحمت را به خودشان نمی‌دهند.در این مقاله قصد داریم تا به اکسپشن‌ها در زبان پایتون بپردازیم و نحوه صحیح استفاده از آن‌ها را درک کنیم. مفاهیمی‌ که می‌آموزیم:اکسپشن چیستچگونه یک اکسپشن raise کنیمنحوه برخورد با اکسپشن‌هاساختار اکسپشن‌هانوشتن یک اکسپشن جدیدبا اکسپشن‌ها آشنا شویدخب؛ در ابتدا بهتر است با ماهیت اکسپشن‌ها آشنا شویم. اکسپشن‌ها درواقع یک آبجکت ( Object ) هستند. پایتون چندین کلاس اکسپشن آماده دارد که شاخصه مشترک آن‌ها ارث بری از یک کلاس داخلی پایتون به نام BaseException است.عاملی که اکسپشن‌ها را مخصوص می‌کند این است که وقتی یکی از آن‌ها رخ می‌دهد کل اجرای برنامه متوقف می‌شود و صرفا قسمتی که برنامه نویس مشخص کرده اجرا می‌شود و یا به سادگی برنامه به اتمام می‌رسد. حالا چگونه یکی از آن‌ها را ببینیم؟ راحتترین روش اینه که یه کار احمقانه بکنیم :))مثلا یکی از اکسپشن‌هایی که مطمئنا همه با آن آشنا هستیم Syntax Error هست وقتی یک جای کد را اشتباه می‌نویسیم ( در این قسمت از پایتون ۳ استفاده شده است، اگر از پایتون ۲!!! استفاده می‌کنید خط پایین کار میکند ).Syntax Errorاین یکی از اکسپشن‌های خاص است، چرا که نمی‌توانید آن‌را کنترل کنید و حتما باید ایراد کد را درست کنید. حالا چند نمونه از اکسپشن‌هایی که می‌توان آن‌ها را کنترل کرد را ببینیم.چند نمونه اکسپشنهمیشه اکسپشن‌ها به معنی ورودی اشتباه نیستند، مثلا اکسپشن ZeroDivisionError را در نظر بگیرید‌ ( وقتی رخ می‌دهد که بخواهید یک عدد را بر ۰ تقسیم کنید )، ممکن است کاربر عدد ۰ را بخاطر صفر بودن موجودی حساب خود وارد کند یا بخواهد برای اینکه بتواند نرم افزاری را دانلود کند در صفحه donation مقدار صفر را وارد کند.اگر به تصویر بالا دقت کرده باشید، متوجه خواهید شد که همه اکسپشن های داخلی پایتون به Error ختم می‌شوند. در پایتون exception و error به‌جای هم استفاده می‌شوند، اما یک تفاوت بسیار کوچک دارند. error ها همگی از کلاس Exception ارث بری می‌کنند که آن نیز از کلاسی که قبلا مطرح شد یعنی BaseException ارث بری می‌کند.یک اکسپشن نشان دهید !وقت آن رسیده که یک اکسپشن تولید کنیم ( برای تولید اکسپشن از اصطلاح Raise استفاده می‌شود ).def factorial(number): if not isinstance(number, int): raise TypeError(f&quotCannot calculate factorial for {type(number)}&quot) if number < 0: raise ValueError(&quotThe number must be >= 0&quot) if number == 0 or number == 1: return 1 else: return number * factorial(number - 1)در این تابع ساده که وظیفه محاسبه فاکتوریل یک عدد را دارد، اگر number عدد صحیح نباشد اکسپشن از نوع TypeError ایجاد می‌شود. در if بعدی اگر عدد کوچکتر از صفر باشد ValueError رخ می‌دهد و در نهایت مقدار فاکتوریل محاسبه می‌شود.قبلا هم ذکر شد که اکسپشن ها آبجکت هستند، در هنگام ایجاد یک اکسپشن جدید می‌توانیم آرگیومنت‌هایی را به کلاس اکسپشن‌ بدهیم. مثلا رشته "The number must be >=0" یک آرگیومنت برای کلاس ValueError می‌باشد. این آرگیومنت‌ها می‌توانند هنگام مدیریت اکسپشن‌ها اطلاعات مفیدی به کاربر بدهند. نحوه استفاده از این آرگیومنت‌ها را در بخش های بعدی خواهیم دید. این قطعه کد را در فایلی ذخیره کنید سپس در ترمینال خود دستور زیر را وارد کنید.$ python[3] -i factorial.py # نام فایل factorial.pyاین دستور فایل factorial.py را داخل interpreter پایتون import میکند و می‌توانید از تابع factorial استفاده کنید.استفاده از تابع factorialاکسپشن دقیقا چه کاری انجام می‌دهد؟!وقتی یک اکسپشن رخ می‌دهد، خط‌های بعد از وقوع اکسپشن دیگر اجرا نمی‌شود و اگر برنامه نویس اکسپشن به‌وجود آمده را مدیریت نکند برنامه با یک پیغام خطا متوقف می‌شود. برای درک بهتر موضوع دو تابع زیر را در نظر بگیرید.# exception-path.py def exception_sample(): print(&quotHere is an exception&quot) raise Exception(&quotThis will happen all the time&quot) print(&quotHi&quot) print(&quotNo response ?&quot) def executor(): print(&quotExecutor started&quot) exception_sample() print(&quotExecutor ended&quot)$ python3 -i exception-path.pyتابع اول را اجرا کردیم. همانطور که می‌بینید پرینت اول اجرا شده ولی سپس اکسپشن ایجاد شده است و خطوط بعدی رها می‌شوند.حالا تابع دوم را اجرا کردیم. پرینت اول تابع اجرا شده سپس پرینت تابع فراخوانی شده نیز اجرا می‌شود ولی به محض رخدادن اکسپشن روند برنامه ( هردو تابع ) متوقف می‌شود و برنامه خارج می‌شود. این ویژگی جالب اکسپشن ها است که مهم نیست در کجا رخداده باشند، برنامه در همان نقطه متوقف نمی‌شود، بلکه تا ابتدایی ترین مرحله بالا می‌آید تا به برنامه نویس فرصت دهد تا در محل مناسب وقوع اکسپشن را مدیریت کند. به تصویر بالا دقت کنید. زیر Traceback مسیر کامل پیموده شده توسط اکسپشن نشان داده شده است. از ماژول مورد نظر تا تابع اولیه سپس تابع دوم ، ... تا شماره خط رخ دادن اکسپشن.یک اکسپشن شکار کنید!وقت آن رسیده است که اکسپشن‌ها را شکار کنیم D: . برای اینکه بتوانیم اکسپشن ها‌ را مدیریت کنیم آن قسمت از کد که احتمال رخدادن اکسپشن در آن قسمت وجود دارد را باید داخل بلوک try بنویسیم و بعد از آن می بایستی عملیاتی که می‌خواهید هنگام بوجود آمدن اکسپشن اجرا شود را داخل بخش except پیاده سازی کنیم. ساختار کلی بلوک try except را در زیر ببینید.try: do_something() except [Exception]: do_something_else()تابع executor از قسمت قبل را به یاد دارید. به اینصورت آن را تغییر می‌دهیم.def executor(): print(&quotExecutor started&quot) try: exception_sample() except Exception: print(&quotCaught an exception&quot) print(&quotExecutor ended&quot) خروجی اجرای این تابع:اکسپشن مدیریت شده است.در این تابع خط اول اجرا شده سپس تابع exception_sample داخل یک بلوک try اجرا می‌شود و هنگام مشاهده اکسپشن کد‌های داخل بلوک except اجرا می‌شود. به کلاس مقابل except دقت کنید؛ در این بلوک except تمام اکسپشن ها ( بجز دو مورد خاص که بعدا بحث می‌شوند ) مدیریت می‌شوند و برای تمامی کلاس ها یک رفتار پیاده سازی شده است. گاهی نیاز است که برای کلاس‌های مختلف رفتار‌های متفاوتی معین شود مثلا اگر کاربر بجای عدد، یک رشته به عنوان آرگیومنت به تابع داد ارور شماره ۱ برای کاربر چاپ شود و اگر عدد منفی داد ارور شماره ۲. این کار به آسانی قابل اجرا است، فقط کافی است بجای Exception نام کلاس مورد نظر را بنویسید؛ مثلا IndexError. ساختار کد شما به شکل زیر خواهد بود.try: print(my_list[10]) except IndexError: print(&quotThe provided index is not available&quot)همچنین می‌توانید چند کلاس مختلف پشت سر هم مدیریت کنید.try: do() except Class1: do_for_class1() except Class2: do_for_class2() except Class3: do_for_class3()قبلا اشاره شد که اکسپشن‌ها می‌توانند مقادیری را به عنوان argument دریافت کنند. در هنگام مدیریت اکسپشن‌ها دسترسی به این مقادیر می‌تواند کمک بزرگی به کاربر برای اشکال یابی بکند. کلیدواژه as این امکان را به ما می‌دهد که اکسپشن را به عنوان یک متغیر ذخیره و به اطلاعات آن دسترسی پیدا کنیم. این قابلیت زمانی بهتر دیده می‌شود که ما اکسپشن های خودمان را تعریف کنیم. فعلا به این حد بسنده می‌کنیم تا در ادامه واضح تر و جزئی تر بحث کنیم.try: raise ValueError(&quotThis is an argument&quot) except ValueError as e: print(&quotThe exception arguments were&quot, e.args)بصورت قراردادی آبجکت اکسپشن‌ها را با e نمایش می‌دهند. اگرچه شما می‌توانید هر اسمی که مدنظرتان است استفاده کنید.آشنایی با Finally و Elseدر مبحث اکسپشن‌ها دو کلیدواژه finally و else بسیار با اهمیت هستند. در این بخش بصورت عملی با هرکدام آشنا خواهیم شد.کلیدواژه finally به برنامه نویس این امکان را می‌دهد که در هرصورتی ( چه اکسپشن رخ دهد، چه رخ ندهد ) بتواند یک کار خاصی را انجام دهد. فرض کنید شما مسئول برنامه نویسی یک دستگاه خودپرداز هستید، کاربر می‌خواهد از حساب خود پول برداشت کند. حال ممکن است کاربر به اندازه درخواستش پول در حساب داشته باشد یا خیر، ما می خواهیم بعد از دریافت پول یا مشاهده کسری موجودی، کارت از دستگاه خارج شود. اینجاست که finally به کمک ما می آید. عملیات برداشت را داخل بلوک try انجام دهید. اکسپشن کمبود موجودی را داخل بلوک except مدیریت کنید و درنهایت در بخش finally دستور خروج کارت را صادر کنید.def withdraw(amount): try: pay_money(amount) except LowBalance: print(f&quotYou don't have {amount}&quot) finally: take_out_card()در این قسمت تابع withdraw وظیفه پرداخت پول را برعهده دارد. ابتدا تابع pay_money فراخوانی می‌شود ( فرض کنید این تابع قرار است موجودی را چک کند و اگر موجودی به اندازه کافی بود پول پرداخت شود. در غیر اینصورت اکسپشن LowBalance را ایجاد کند ). در صورت رخ دادن اکسپشن مورد نظر پیغام خطا چاپ می‌شود و در نهایت بعد از دریافت پول یا مشاهده پیفام خطا، در بخش finally کارت خارج می‌شود.نکته: اکسپشن LowBalance جز اکسپشن های داخلی پایتون نیست و خودمان تعریف کرده ایم. در ادامه روش تعریف اکسپشن‌های جدید را خواهیم دید.کلیدواژه else را مطمئنا در شرط‌ها ( if ) دیده اید، اینجا نیز کارکردی تقریبا مشابه دارد. else در واقع به برنامه نویس اجازه می‌دهد اگر هیچ اکسپشنی رخ نداد کاری را انجام دهد. به تفاوت else و finally دقت کنید. اولی فقط در صورتی که هیچ اکسپشنی رخ ندهد اجرا می‌شود اما دومی در هر صورت. حالتی را فرض کنید که در یک قطعه کد try, except هم else داریم و هم finally، در این صورت اگر اکسپشنی رخ ندهد، هم محتوای داخل else اجرا خواهد شد هم محتوای داخل finally. مثال زیر روش استفاده از else را نشان می‌دهد.try: do() except Exception: handle_exception() else: no_exception()حالا که کلیات مدیریت اکسپشن‌ها را یادگرفتیم، بیایید با یک مثال این مطالب را تثبیت کنیم.# structure.py import random some_exceptions = [ValueError, TypeError, IndexError, None] try: choice = random.choice(some_exceptions) print(&quotraising {}&quot.format(choice)) if choice: raise choice(&quotAn error&quot) except ValueError: print(&quotCaught a ValueError&quot) except TypeError: print(&quotCaught a TypeError&quot) except Exception as e: print(&quotCaught some other error: %s&quot % ( e.__class__.__name__)) else: print(&quotThis code called if there is no exception&quot) finally: print(&quotThis cleanup code is always called&quot)قطعه کد بالا در هر مرحله اجرا از لیست some_exceptions یک اکسپشن یا None را انتخاب می‌کند. سپس با توجه به اینکه مقدار انتخاب شده از نوع اکسپشن می‌باشد یا نه، سعی می‌کند آن را مدیریت کند. چندبار این قطعه کد را اجرا کنید. به ساختار کد هم توجه کنید. همیشه باید اول try بعد except سپس else و در نهایت finally تعریف شود.# If IndexError raising <class 'IndexError'> Caught some other error: IndexError This cleanup code is always called # If ValueError raising <class 'ValueError'> Caught a ValueError This cleanup code is always called # If TypeError raising <class 'TypeError'> Caught a TypeError This cleanup code is always called # If None raising None This code called if there is no exception This cleanup code is always calledتا اینجای کار توانستیم اکسپشن‌ها را مدیریت کنیم و بدون خطر خاصی به ادامه برنامه بپردازیم. ولی همیشه اینطور نیست، گاهی یک حرکت مخرب از سمت کاربر ممکن است برنامه را به سمت اشتباهی ببرد ( حتی با وجود try, except ). در این صورت می‌خواهیم بعد از اینکه اکسپشن هندل شد دوباره همان اکسپشن ایجاد شود و با نمایش پیغام خطا برنامه متوقف شود. این حالت را در نظر بگیرید که برنامه شما برای اجرا شدن نیاز به یک پایگاه داده دارد ( اگر با فریمورک‌هایی مثل جنگو آشنا هستید این سناریو برایتان روشن است ). حال شما برنامه را آغاز می‌کنید ولی مثلا فایل پایگاه داده موجود نیست یا پایگاه داده روی پورت 5432 ( پورت پیشفرض پستگرس ) فعال نیست. در اینصورت مدیریت اکسپشن تاثیری در روند کار نخواهد داشت، چرا که تا وقتی پایگاه داده متصل نباشد کلیه فرایند‌ها به مشکل برخواهند خورد. پس تصمیم می‌گیریم پس از catch کردن اکسپشن چند نمونه راهنما برای کاربر چاپ کنیم و در نهایت با فعال کردن همان اکسپشن برنامه را خاتمه بدهیم.try: connect_to_database() except DataBaseNotFound: print(&quotMake sure to configure your setting file or check if database exists&quot) raiseساختار کلی کد کاملا مشابه هرآنچه تا الان دیدیم است. تنها چیز متفاوت وجود raise می باشد. کلیدواژه raise بدون نوع کلاس، اکسپشنی که رخ داده است را دوباره فعال می‌کند. اگر کد بالا برایتان واضح نیست به مثال پایین دقت کنید.my_list = [1, 2, 3, 4] try: print(my_list[10]) except IndexError: print(&quot Be careful !!! &quot) raiseخروجی کد بالا به شکل زیر است.سلسه مراتب در اکسپشن‌هادر بخش‌ها قبل در مورد اکسپشن‌ها و مدیریت آن‌ها صحبت کردیم. حال بیایید عمیق‌تر وارد این مبحث شویم. قبلا گفته شد تمامی اکسپشن‌ها از یک کلاس به نام Exception ارث بری می‌کنند که آن کلاس نیز از BaseException ارث بری می‌کند. همچنین گفته شد اکسپشن‌هایی که در انتهای نام خود واژه Error را دارند از Exception ارث بری کرده اند. دو کلاس اکسپشن در زبان پایتون وجود دارند که مستقیما از BaseException ارث بری کرده اند. SystemExit و KeyboardInterrupt. به ترتیب به توضیح هرکدام می‌پردازیم.اکسپشن SystemExit زمانی رخ می‌دهد که برنامه به صورت کاملا طبیعی خارج شود مثلا زمانی که دستور sys.exit یک جایی از کد اجرا شود‌ (مثلا کاربر از منو روی دکمه ضربدر برای خروج را کلیک می‌کند). هدف از طراحی این اکسپشن این بوده است که به ما اجازه بدهد قبل از خروج از برنامه اگر نیاز داریم کاری را انجام دهیم. مثلا بتوانیم قبل از خروج فایل هایی که توسط برنامه باز کرده ایم را ببندیم یا اگر فایلی ذخیره نشده آن را ذخیره کنیم. مورد بعدی KeyboardInterrupt می باشد که در برنامه های خط فرمان همگی با آن سر و کار داشتیم. وقتی یک برنامه پایتون را اجرا می‌کنید ولی قبل از اتمام برنامه دو کلید کنترلی Ctrl+C را می‌زنید، این اکسپشن raise می‌شود و برنامه متوقف می‌شود. همانند SystemExit این اکسپشن نیز قابل کنترل می باشد تا اگر می‌خواهیم قبل از بستن برنامه فعالیت نهایی برنامه را اجرا کنیم.با توجه به توضیحات داده شده می‌توان کلاس دیاگرام زیر را برای اکسپشن ها در پایتون رسم کرد.ساختار سلسه مراتبی اکسپشن هادر قسمت مدیریت اکسپشن ها بیان شد که except Exception بجز دو مورد بقیه اکسپشن ها را catch می‌کند. این دو مورد همان SystemExit و KeyboardInterrupt هستند. احتمالا دلیلش را فهمیده اید. دقیقا؛ این دو مورد مستقیما از BaseException ارث بری می‌کنند. لذا از نوع Exception نیستند. متفاوت بودن ساختار این دو مورد با بقیه اکسپشن ها این امکان را به ما می‌دهد که با except Exception خطاهای احتمالی برنامه را مدیریت کنیم و مستقیما با این دو نوع اکسپشن ارتباط برقرار نکنیم.حالا که با ساختار اکسپشن‌ها آشنا شدیم بهتر است موارد زیر را ببینیم.except ValueError -> فقط برای این نوع اکسپشن except (IndexError, TypeError): -> برای این دونوع اکسپشن except Exception -> برای همه اکسپشن ها بجز دو مورد استثنایی except -> همه اکسپشن هااکسپشن های جدید!حالا بیایید یک اکسپشن جدید بنویسیم. برای نوشتن یک اکسپشن جدید کافی است فقط یک کلاس بسازید و از Exception ارث بری کنید. به همین راحتی:class SimpleException(Exception): pass raise SimpleException(&quotMy Simple Exception&quot)می‌توانیم اکسپشن ها را گسترش بدهیم.class WeirdOddNumber(Exception): def __init__(self, number): super().__init__(f&quotI don't like {number}&quot) self.number = number def explain(self): print(f&quotI don't like odd numbers and {self.number} is odd&quot)جالب شد ! D:به تصویر زیر دقت کنید. پیاده سازی WeirdOddNumber و یک تابع که فقط اعداد زوج را چاپ می‌کند موجود می‌باشد. در انتها نیز سعی کرده ایم ۱۰ و ۱۳ را چاپ کنیم.اکسپشن های شخصی سازی شدهخروجی کد بالاعدد ۱۰ بدون مشکل چاپ شد. وقتی عدد ۱۳ را به print_even_numbers دادیم، تابع متوجه شد که این عدد فرد است پس اکسپشنی که تعریف کرده بودیم را raise کرد. خوشبختانه ما WeirdOddNumber را کنترل کرده بودیم. پس در بلوک except آرگیومنت آن یعنی ( I don't like this 13 ) چاپ شد، سپس متد explain فراخوانی شد.سخن نهاییاز اکسپشن‌ها فرار نکنید؛ ممکن است با خود بگویید که همه حالات را ‌می‌توان با شروط مختلف بررسی کرد و از بروز اکسپشن‌ها جلوگیری کرد. اما مثلا ممکن است برای دسترسی به یکی از عناصر لیست، در ابتدا بررسی کنید که index داده شده حتما کمتر از طول لیست باشد اما یادمان نباشد که از وارد کردن اعداد منفی فراتر از حد لیست جلوگیری کنیم ( همانطور که می‌دانید در پایتون امکان دسترسی به لیست با index منفی وجود دارد ). این حالت جز یکی از ساده ترین حالات ممکن است. اما همیشه مسئله آنقدر ساده نیست. جدا از کنترل روند برنامه، استفاده از اکسپشن ها باعث افزایش خوانایی کد و درک بهتر آن می‌شود.برای نوشتن این مقاله از فصل ۴ کتاب Python3 Object Oriented Programming نوشته Dusty Phillips استفاده شده است.امیدوارم که این مطلب برایتان مفید واقع شده باشد. با سپاس.