یک‌بار برای همیشه - Context manager

سلام به همه پایتون‌ دوستانِ عزیز! توی این قسمت از یکبار برای همیشه سراغِ Context managerها رفتیم. در یک‌بار برای همیشه بحثی رو مطرح می‌کنیم که خیلی در نگاه اول شاید راحت بنظر نرسه، و این‌جا سعی می‌کنیم با بیان ساده‌تر و مثال‌های خیلی خیلی ابتدایی اون مطلب رو به راحتی به مخاطب انتقال بدیم.

مقدمه

مدیریت منابع همیشه یکی از مهم‌ترین وظیفه‌های سیستم‌عامل بوده، اما بعضی اوقات پیش‌میاد به هر دلیلی منبع‌استفاده شده رو به سیستم عامل پس ندیم و اینجاست که context managerها با یک syntax خیلی دوست داشتنی به کمک ما میان! با کمک context manager ‌ها مطمئمن می‌شیم هر دری رو که باز کردیم حتما می‌بندیم!

فرض کنید توی برنامه‌مون رفتیم یک فایل رو باز کردیم به ازای اون فایل ما یک file descriptor رو مصرف کردیم. منابع همیشه توی دنیا محدود بودن و توی هر سیستم‌عامل هم این طور هست. این محدودیت باعث میشه یک سری قوانین توی سیستم عامل به کاربران یا برنامه‌ها اعمال بشه. به عنوان مثال هر پروسس تا یک تعداد محدودی از فایل‌ها رو در یک زمان حق داره باز کنه . اگر از سیستم‌عاملِ *یونیکسی استفاده می‌کنید با دستور زیر این تعداد رو می‌تونید ببینید:

توی عکس بالا این تعداد ۱۰۲۴ عدد هست، یعنی هر برنامه حداکثر می‌تونه ۱۰۲۴ فایل رو یک زمان باز کنه. حالا کافیه یک برنامه خیلی ساده بنویسید که بیشتر از این تعداد فایل رو باز کنه و اونا‌رو هم نبنده یه چیزی شبیه پایین:

اینجاست که پایتون خطای `OSError` رو به ما میده!

جدایی از اینکه امکان تغییر این محدودیت‌ها برای کاربر وجود داره، مشکل اصلی اینِ که ما کلی فایل رو باز کردیم اما هیچ‌کدوم رو سعی نبستیم. حالا برای مطمئن شدن کافیه این‌بار همه فایل‌هایی رو که باز کردیم ببندیم، با هم ببینیم:

همون‌طور که می‌بینید با بسته شدنِ منبع‌مون همه‌چی به خیر و خوشی تموم شد. اما خب توی دنیای واقعی هزار و یک اتفاق ممکن هست رخ بده. ممکنِ وسط برنامه Exceptionای رخ بده و .... . بهمین خاطر معمولاً توسعه‌دهنده‌ها دنبال یک روش بهتر برای مدیریت منابع می‌گردن که مطمئن بشن همیشه اینکار به درستی انجام میشه. توی زبان‌های برنامه نویسی دیگه ( ناگفته نمونه توی پایتون ۲ همه تا یک مدت از این روش استفاده میشد) از try-except-finally برای این‌کار استفاده میشه.

معمولاً برای کار با هر منبع ما یک سری کارهای اولیه انجام میدیم مثل اینکه به اون منبع خاص وصل بشیم (اسم این مرحله رو می‌ذاریم setup ) و بعد از این کار، کار خودمون روانجام می‌دیم(اسم این بخش رو هم می‌ذاریم do_something) و در نهایت ارتباط‌مون رو با منبع قطع می‌کنیم (اسم این بخش از کار رو می‌ذاریم teardown). با این تفاسیر اگر بخوایم گفته‌های بالا رو مدل کنیم یه چیزی شبیه پایین میشه:

از اون‌‌جایی که پایتون یک زبان برنامه نویسی باحال هست؛ راه‌حلِ خیلی باحالیم برای مدیریت منابع معرفی کرده. این راه‌حل از بس باحال بوده توی خیلی از بخش‌های دیگه هم استفاده میشه و باعث شده بگیم context manager رو می‌تونید توی هر بخشی که لازم هست در ابتدا یک زیر ساختی فراهم بشه (setup) و بعد از اینکه کارمون تموم شد و اون زیرساخت بسته یا نابود بشه (teardown) استفاده بشه. با این تعریف شما می‌تونید تو خیلی از جاها علاوه بر باز و بسته کردن یک فایل این دوستمون رو به کار ببرید.

مثلاً وقتی می‌خواید بتونید توی دیتابیس امکان درج اطلاعات وجود داشته باشه پیش‌نیازش این هست که بهش وصل بشید و در پایان هم، ارتباط رو به درستی قطع کنید. مثالِ دیگه اجرای Transaction که بازم توی دیتابیس هست که سازوکاری مشابه فوق داره. توی این قسمت تاجایی که بشه سعی می‌کنیم مثال‌های مختلف رو به شما دل‌باخته‌های پایتون نشون بدیم تا ملکه‌ی ذهنتون بشه و هر کجا که به چیزی شبیه Context manager نیاز شد، سریع یه دونه تعریف کنید.

معرفی

مرسوم‌ترین شکل Context manager که(ممکنِ از قبل باهاش آشنا باشید) وقتی هست که یک فایل رو باز می‌کنیم تا محتویات اون رو بخونیم. توصیه میشه (اکیداً!) که هر موقع می‌خواید یک فایل رو باز کنید و هر کاری رو باهاش انجام بدید با with این‌کار رو انجام بدید.

نحوه استفاده از context manager خیلی ساده‌اس یک قالب کلی داره:

همون‌طور که می‌بینید با with به پایتون می‌گیم که می‌خوایم از یک Context manager استفاده کنیم، یک بخش اختیاری داره که داخل [] قرار دادیم که در واقع داریم یک اسم دیگه برای context manager اونجا می‌ذاریم ( این‌بخش که اختیاری هست موقع ساختن context manager باید پیاده‌سازی شده باشه) و تا وقتی که داخل فضای تورفته( ایندنت شده) هستیم به منبع‌ مورد نظر وصلیم و به محض خروج از فضای تور رفته اتصال ما به منبع به صورت خودکار قطع میشه .

تو قسمت Decoratorها یادتونِ گفتیم @ نماد decorator هست؟ اینجا هر کجا with دیدید یعنی داریم با context manager کار می‌کنیم. 

فرض کنید قصد خوندن یک فایل رو داریم، برای خوندش اینجوری عمل می‌کنیم:

اینکار چه مزیتی به روش قبلی داره؟ تنها مزیت‌اش اینِ که اینجا context manager قبول مسئولیت کرده که ما وقتی کارمون تموم شد( از فضای تو رفته (ایندنت شده) بیرون رفتیم) فایل رو خودش ببنده و این کار در حقیقت این کار توی تابع open تعریف شده.

اگر می‌خواستیم این کار رو خودمون انجام بدیم معادل با کد پایین می‌شد:

تعریف Context manager

برای بررسی بیشتر بریم خودمون کارِ تابع open رو انجام بدیم! برای تعریف context manager معمولاً از یک کلاس برای تعریف‌اش استفاده می‌کنیم. اما پایتون چطوری فرق این کلاس با کلاس‌های عدی رو می‌فهمه؟ اساساً برای ساخت یک context manager دو تا متد الزامی هست:

  1. ساز‌وکاری که بهش بگیم به محض ساخت context manager این کار رو برای ما انجام بده، که متد __enter__ مسئول انجام این کار هست.
  2. سازوکاری که از طریق اون موقع خروج مطمئن بشیم همه‌چی درست و تمیز بسته میشه. این کار رو متد __exit__برای ما انجام میده.

فرض کنید بخوایم کار تابع open از طریق context manager خودمون پیاده‌سازی کنیم:

اول از همه توی متد __init__ اسم فایل و مد باز کردن فایل رو می‌گیریم. بعدش توی متد __enter__ فایل مورد نظر رو باز کردیم و اون به عنوان خروجی return کردیم. هرچیزی که اینجا return بشه اون چیزی هست که کاربر می‌تونه از طریق as myfile بهش دسترسی داشته باشه.

در نهایت کارهایی که می‌خوایم موقع خروج انجام بشه رو توی __exit__ انجام دادیم که توی این مثال یعنی فایل باز شده رو ببند.

اما یک نکته‌ای که بین این همه کد ممکن هست فراموش بشه خوانایی کدمون هست. بیایید یه مقایسه‌ای بین دو حالت استفاده معمولی از context manager و وقتی که داریم از with استفاده می‌کنیم داشته باشیم. توی بخش بالا فرض کردیم که چیزی به اسم withوجود نداره و در بخش پایین همون کار رو با with انجام دادیم:

پس الان شاید برای شما واضح‌تر شده باشه که چرا خیلی از ماژول‌های پایتون این قابلیت رو به‌خودشون اضافه کردن.

متدِ __exit__ ورودیی داره که برای مدیریت خطا‌هایی که ممکن هست رخ بده استفاده می‌شه. مثلاً بر حسب نوع خطایی که رخ داده، کار مخصوص به اون رو انجام بدیم. که ما فعلاً برای سادگی مثالمون ازش استفاده نکردیم.

نکته: متغیرهایی که داخل with استفاده میشن بعد از with هم قابل استفاده هستند، یعنی متغییر محلی نیستند! 

یک مثال خیلی مرسوم دیگه می‌تونه وصل شدن به دیتابیس باشه. برای سادگی کار دیتابیس SQLite رو مورد استفاده قرار می‌دیم و قصدمون ساخت یک جدول برای ذخیره‌ی داده‌هامون و درج توی اون هست. این کار رو به صورت عادی اینجوری انجام میشه:

حالا با context manager اولش کلاس پایین رو تعریف می‌کنیم:

و در ادامه‌ی زندگی، هر بار که خواستیم با دیتابیس کار کنیم ، اینجوری (باکلاس) بهش وصل می‌شیم:

در حقیقت دیگه از تکرار مکررات به بهترین نحوه ممکن جلوگیری کردیم!

خاطر‌تون هست توی بخش Decorator یک timer تعریف کردیم که زمان اجرا شدنِ یک فرآیند رو برای ما اندازه‌گیری می‌کرد (؟) این‌بار همین کار رو با Context manager می‌خوایم انجام بدیم:

خروجی کد بالا میشه:

اما همه‌ی مثال‌های بالا یک ایراد اساسی دارند و اون این هست که ما به هیچ‌عنوان خطا‌هایی که ممکن هست رخ رو بده رو مد نظر قرار ندادیم!

متد __exit__ سه تا پارامتر ورودی می‌گیره برای مدیریت خطا‌ها به کار می‌رن:

  • پارامتر اول exctype که نوع خطاری رخ داده شده رو مشخص می‌کنه.
  • پارامتر دوم مقدار خطای هست که صورت گرفته و اسمش رو value می‌ذاریم.
  • پارامتر سوم traceback خطای رخ داده شده است.

حالا به عنوان مثال می‌خوایم متدِ __exit__ رو برای کلاسِ Timer تغییر بدیم تا بتونه خطاها رو دریافت کنه:

حالا برای تستِ context manager بالا، خودمون قصدا یک خطا رو ایجاد می‌کنیم تا خروجی هر متغییر رو ببینیم:

انتظار میره خروجی پایین رو ببنید:

اگر براتون سوالِ که چرا پایتون Exception رو raise نکرده همه چی به return True آخرِ متد __exit__ بر می‌گرده. در حقیقت متد __exit__ یا باید False یا True رو برگردونه.

  • با False بودن داریم به پایتون می‌گیم که Exception رو به کاربر برگردون (اگر چیزی رو return نکنیم به صورت پیش‌فرض False در نظر گرفته میشه).
  • اما موقعی که True باشه به این معنی هست که خطا رو نادیده بگیر (suppress). و نهایتاً حواستون باشه هر چیزی بعد از raise بیاد اجرا نمیشه یعنی به raise که رسیدیم دیگه متد __exit__ اجرا شده!

مثال دوم context manager مربوط به SQLite رو تغییر میدیم به نحوی که مدیریت خطا را انجام بده. توی جدولی که تعریف کردیم، ایمیل کاربر رو به عنوان یک چیز unique در نظر گرفتیم درنتیجه اگه کاربر قصد داشته باشیم یک کاربر جدید رو با ایمیل قبلی اضافه کنیم، به ما خطا میده. بریم برای مدیریت خطا متد __exit__ رو برای DBHelper تغییر بدیم که یک پیغام مناسب توی همچین مواقعی به کاربر نشون بده:

حالا می‌ریم که کد پایین رو اجرا کنیم:

که با خروجی پایین مواجهه می‌شیم:

خوشبختی با contextlib

چیزایی که تا الان یاد گرفتیم شامل معرفی context manager، نحوه ساخت یک context manager و مدیریت خطا در context managerها بود. از اونجایی که این مفهوم خیلی کاربردهای زیادی داره، در نتیجه یک ماژول برای تسهیل در روند استفاده از Context managerها هم وجود داره که اسمش contextlib هست. در ادامه می‌ریم که یک مرور روی توابع پرکاربردِ این ماژول داشته باشیم.

تعریفِ ساده‌تر

یکی از چیزای باحالِ ماژول contextlib، داشتن یک decorator برای ساده‌تر کردن ساخت context manager هست. این decorator باید به همراه یک generator استفاده بشه. اگه یادتون باشه گفتیم generator تابعی هست که به جای return از yield استفاده می‌کنه.

نحوه استفاده از این decorator اینجوری هست که هرچیزی که قبل از yield باشه رو به عنوان __enter__ در نظر می‌گیره و هرچیزی بعدش باشه __exit__ می‌شناسه(آموزش Decorator لینک، آموزش Generator لینک).

  • مثال: با استفاده از decorator ماژولِ contextlib تابعی بنویسیم که محتویات فایل رو بخونه:

مثال دوم dbhelper و نحوه کنترل خطا در این بخش:

همون‌طور که می‌بینید کنترل و مدیریت خطا‌ها با روش‌های قدیمی انجام میشه. فقط اینجا اگر بخواهید خطای رخ داده شده مثل وقتی که توی کلاس context manager به کاربر انتقال داده شد منتقل بشه اون خطی که کامنت شده `raise` رو باید بنویسید.

کلاسِ ContextManager

ماژول contextlib یک کلاس هم داره با اسم ContextDecorator که می‌تونید اون رو به کلاس خودتون به ارث بدید که مزیت‌اش این هست که context manager ای که با این کلاس ساخته بشه به عنوان decorator هم می‌تونه استفاده بشه!

تابع closing

این تابع خودش یک context manager هست که یک ورودی می‌گیره و موقعی که خروج به صورت خودکار اگر اون ورودی تابع close داشته باشه اون رو صدا می‌زنه، مثلاً:

تابع suppress

این یکی خیلی چیز باحالی هست، فرض کنید می‌خواهید یک فایل رو حذف کنید اما مطمئن نیستید که وجود داره یا نه، چطور این کار رو می‌کنیم؟

اما با کمک suppress خیلی خواناتر و راحت‌تر میشه این کار رو انجام داد:

در حقیقت suppress به عنوان ورودی می‌تونه چندین Exception بگیره و اگر هیچ‌کدوم از اونا رخ نده کار مورد نظر رو انجام میده، اگر خطا رخ بده همه‌چی رو بیخیالش میشه.

جمع‌بندی

هدف ما از این قسمت آشنایی با context manager بود، این دوست خوبمون هم امکان مدیریت منابع بهمون می‌ده و هم می‌تونه باعث خواناتر شدن کد بشه. امیدوارم از این قسمت لذت برده باشد و همراه خوبی برای آینده‌تون باشه. کدهای استفاده شده در این آموزش رو از طریق این لینک می‌تونید به دسترسی داشته باشید.

از اینجا کجا بریم

  • معرفی کامل ماژول contextlib
https://docs.python.org/3/library/contextlib.html
https://pymotw.com/3/contextlib/
  • معرفی context manager
http://arnavk.com/posts/python-context-managers/
https://alysivji.github.io/managing-resources-with-context-managers-pythonic.html