مدیران زمینه در پایتون چیست؟
یکی از رایج ترین کارهایی که باید در برنامه های خود انجام دهید کار با منابع خارجی است. این منابع می توانند فایل های موجود در حافظه رایانه شما یا اتصال باز به سرویس شخص ثالث در اینترنت باشند.
برای سادگی، برنامه ای را تصور کنید که یک فایل را باز می کند، چیزی روی آن می نویسد و سپس فایل را می بندد.
یکی از راه های پیاده سازی این برنامه در پایتون به صورت زیر است:
def main(): my_file = open('books.txt', 'w') my_file.write('If Tomorrow Comes by Sidney Sheldon') my_file.close() if __name__ == '__main__': main()
از آنجایی که این برنامه را با مجوزهای مناسب روی رایانه خود اجرا می کنید، فایلی به نام books.txt
ایجاد می کند و If Tomorrow Comes by Sidney Sheldon
را در آن می نویسد.
تابع open()
یکی از توابع داخلی در پایتون است. می تواند یک فایل را از یک مسیر مشخص باز کند و یک شی فایل مربوطه را برگرداند.
یک شی فایل یا شیء فایل مانند، همانطور که اغلب نامیده می شود، یک روش مفید برای کپسوله کردن متدهایی مانند read()
, write()
یا close()
است.
از متد write()
می توان برای نوشتن/ارسال شی بایت مانند به یک جریان باز مانند یک فایل استفاده کرد.
هر زمان که یک منبع خارجی را باز می کنید، باید آن را زمانی ببندید که دیگر مورد نیاز نباشد و متد close()
این کار را انجام می دهد.
این برنامه کاربردی است اما یک نقص بزرگ دارد. اگر برنامه نتواند فایل را ببندد، تا زمانی که خود برنامه بسته نشود، باز می ماند.
ببینید، هر برنامه ای که روی رایانه خود اجرا می کنید مقدار محدودی از حافظه به آن اختصاص داده می شود. همه متغیرهایی که ایجاد میکنید یا منابع خارجی که از یک برنامه باز میکنید در حافظه اختصاص داده شده به آن توسط رایانه شما باقی میمانند.
اگر برنامه ای مانند این به باز کردن فایل های جدید بدون بستن فایل های قبلی ادامه دهد، حافظه اختصاص داده شده همچنان کوچک می شود.
در یک نقطه برنامه به ناچار حافظه اش تمام می شود و به طور ناخوشایندی از کار می افتد. این مشکل به عنوان نشت حافظه نامیده می شود.
یکی از راه های جلوگیری از این اتفاق در پایتون استفاده از عبارت try...except...finally
است.
def main(): my_file = open('books.txt', 'w') try: my_file.write('If Tomorrow Comes by Sidney Sheldon') except Exception as e: print(f'writing to file failed: {e}') finally: my_file.close() if __name__ == '__main__': main()
کد داخل بلوک finally
بدون توجه به هر چیزی اجرا می شود. پس حتی اگر برنامه در عمل درست شکست بخورد، باز هم اجرا خواهد شد.
پس ، این مشکل را حل می کند، اما تصور کنید هر بار که می خواهید چیزی در یک فایل بنویسید، این خطوط کد را بنویسید.
خیلی قابل استفاده مجدد نیست شما باید خودتان را زیاد تکرار کنید و شانس پرش از بخشی از if...except...finally
نردبان نیز امکان پذیر است.
اینجاست که مدیران زمینه وارد می شوند.
مدیر زمینه در پایتون چیست؟
با توجه به واژه نامه پایتون، مدیر زمینه:
یک شی که با تعریف متدهای __enter__()
و __exit__()
محیطی را که در عبارت with
مشاهده می شود کنترل می کند.
ممکن است برای شما واضح نباشد. اجازه دهید مفهوم را با یک مثال توضیح دهم.
دستور with
در پایتون به شما امکان می دهد یک بلوک کد را در یک زمینه زمان اجرا که توسط یک شی مدیر متن تعریف شده است اجرا کنید.
هنگامی که اجرای بلوک کد به پایان رسید، شیء مدیریت متن مراقب از بین بردن منابع خارجی است که دیگر مورد نیاز نیستند.
با استفاده از عبارت with
به صورت زیر می توانید برنامه را بازنویسی کنید:
def main(): with open('books.txt', 'w') as my_file: my_file.write('If Tomorrow Comes by Sidney Sheldon') if __name__ == '__main__': main()
از آنجایی که تابع open()
با دستور with
در این مثال جفت شده است، تابع یک مدیر زمینه ایجاد می کند.
شی فایل در بافت بلوک کد فرورفته قابل دسترسی خواهد بود، به این معنی که شی فایل خارج از آن محدوده وجود ندارد.
کلمه کلیدی as
زمانی مفید است که می خواهید یک متغیر هدف را به یک شیء برگشتی اختصاص دهید. در اینجا، متغیر my_file
هدف است و شی فایل را نگه میدارد.
شما می توانید هر کاری را که می خواهید در بلوک تورفتگی کد انجام دهید و نگران بستن فایل نباشید.
زیرا هنگامی که اجرای بلوک کد به پایان رسید، مدیر زمینه فایل را به طور خودکار می بندد.
پس ، شما کل try...except...finally
با استفاده از دستور with
و یک مدیریت متن، در دو خط کد نردبان شوید.
اما چگونه این اتفاق می افتد؟ چگونه یک شی مدیر زمینه وظیفه تنظیم و بستن منابع را انجام می دهد؟
و آن متدهای __enter__()
و __exit__()
که در واژه نامه اسناد پایتون خوانده اید کجا هستند؟
خب خیلی خوشحالم که پرسیدی :-)
نحوه ایجاد یک مدیر زمینه سفارشی در پایتون
فیزیکدان نظری آمریکایی، ریچارد فاینمن، به قول معروف:
آنچه را که نمی توانم خلق کنم، نمی فهمم.
پس ، برای درک عملکردهای یک مدیر زمینه، باید خودتان یکی را ایجاد کنید و دو روش مجزا برای انجام آن وجود دارد.
اولی یک رویکرد مبتنی بر مولد و دومی یک رویکرد مبتنی بر کلاس است. در این بخش هر دو را به شما آموزش خواهم داد.
اما قبل از آن، اجازه دهید یک مثال پیچیده را برایتان بیاورم که بیش از باز کردن و بستن فایلها در پایتون است.
برنامه پایتون دیگری را تصور کنید که باید با پایگاه داده SQLite برای خواندن و نوشتن داده ها ارتباط برقرار کند.
می توانید آن برنامه را به صورت زیر بنویسید:
import sqlite3 create_table_sql_statement = 'CREATE TABLE IF NOT EXISTS books(title TEXT, author TEXT)' insert_into_table_sql_statement = "INSERT INTO books VALUES ('If Tomorrow Comes', 'Sidney Sheldon'), ('The Lincoln Lawyer', 'Michael Connelly')" select_from_table_sql_statement = 'SELECT * FROM books' def main(): database_path = ':memory:' connection = sqlite3.connect(database_path) cursor = connection.cursor() try: cursor.execute(create_table_sql_statement) connection.commit() cursor.execute(insert_into_table_sql_statement) connection.commit() cursor.execute(select_from_table_sql_statement) print(cursor.fetchall()) except Exception as e: print(f'read or write action to the database failed: {e}') finally: connection.close() if __name__ == '__main__': main() # [('If Tomorrow Comes', 'Sidney Sheldon'), ('The Lincoln Lawyer', 'Michael Connelly')]
این برنامه پایتون با پایگاه داده SQLite ارتباط برقرار می کند. سپس یک جدول جدید به نام کتاب با دو ستون TEXT
به نامهای title
و author
ایجاد میکند.
سپس این برنامه اطلاعات مربوط به سه کتاب را روی میز ذخیره می کند، آنها را از پایگاه داده بازیابی می کند و داده های بازیابی شده را روی کنسول چاپ می کند.
همانطور که از خروجی عبارت print()
مشهود است، برنامه با موفقیت داده های داده شده را از پایگاه داده ذخیره و بازیابی کرده است.
سه پرس و جوی SQL در این برنامه وجود دارد که مسئول اقدامات پایگاه داده ای است که من توضیح دادم.
create_table_sql_statement = 'CREATE TABLE IF NOT EXISTS books(title TEXT, author TEXT)' insert_into_table_sql_statement = "INSERT INTO books VALUES ('If Tomorrow Comes', 'Sidney Sheldon'), ('The Lincoln Lawyer', 'Michael Connelly')" select_from_table_sql_statement = 'SELECT * FROM books'
من این سه خط کد را در بالای فایل نگه داشته ام تا تابع main()
را پاک تر نگه دارم. بقیه برنامه پایگاه داده را راه اندازی کرده و کوئری ها را اجرا می کند.
پایتون به لطف ماژول sqlite3
که روشهای مفیدی مانند متد sqlite3.connect()
را در خود جای داده است، از پایگاههای داده SQLite پشتیبانی میکند.
این متد مسیر یک پایگاه داده را به صورت رشته ای می گیرد، سعی می کند یک اتصال برقرار کند و در صورت موفقیت، یک شی Connection
را برمی گرداند.
اگر :memory:
به جای مسیر فایل پاس کنید، برنامه یک پایگاه داده موقت روی حافظه کامپیوتر شما ایجاد می کند.
هنگامی که اتصال برقرار کردید، به یک شی Cursor
نیاز دارید. شی مکان نما لایه ای از انتزاع است که برای اجرای پرس و جوهای SQL لازم است.
متد cursor()
که در شیء Connection
کپسوله شده است، مکان نما جدیدی را به پایگاه داده متصل برمی گرداند.
در داخل یک بلوک try
، میتوانید سعی کنید هر کوئری را که میخواهید با استفاده از متدهای execute()
یا executemany()
اجرا کنید.
try: cursor.execute(create_table_sql_statement) connection.commit() cursor.execute(insert_into_table_sql_statement) connection.commit() cursor.execute(select_from_table_sql_statement) print(cursor.fetchall())
هر بار که چیزی در پایگاه داده می نویسید، باید متد connection.commit()
را فراخوانی کنید. در غیر این صورت تغییرات از بین خواهند رفت.
داده های بازگردانده شده از پایگاه داده در شی مکان cursor
باقی می مانند و می توانید با استفاده از متدهای cursor.fetchone()
یا cursor.fetchall()
به آنها دسترسی داشته باشید.
در صورت خرابی، بلوک except
فعال می شود. بلوک finally
بدون قید و شرط اجرا می شود و در پایان اتصال پایگاه داده را می بندد.
این خوب و کاربردی است، اما همانطور که قبلاً گفتم، خیلی قابل استفاده مجدد نیست و مستعد خطا است.
متأسفانه، یا در مورد ما خوشبختانه پایتون با یک مدیر زمینه داخلی برای مدیریت ارتباطات با پایگاههای داده SQLite ارائه نمیشود.
پس ، بیایید تلاش کنیم و ببینیم که آیا میتوانیم یکی از آنها را خودمان تولید کنیم.
نحوه ایجاد یک مدیر زمینه مبتنی بر کلاس در پایتون
برای نوشتن یک مدیر زمینه مبتنی بر کلاس در پایتون، باید یک کلاس خالی با سه روش خاص ایجاد کنید:
class Database: def __init__(self): pass def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): pass
واضح است که اولین مورد سازنده کلاس است که هنوز هیچ پارامتری را قبول نکرده است. مسئولیت پذیرش مسیر پایگاه داده را بر عهده خواهد داشت:
import sqlite3 class Database: def __init__(self, path: str): self.path = path def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): pass
متد __enter__()
وظیفه تنظیم منبع را بر عهده دارد. این جایی است که اتصال را برقرار می کنید و مکان نما را نمونه برداری می کنید:
import sqlite3 class Database: def __init__(self, path: str): self.path = path def __enter__(self): self.connection = sqlite3.connect(self.path) self.cursor = self.connection.cursor() return self def __exit__(self, exc_type, exc_val, exc_tb): pass
با این حال شما نمی توانید دو شی را همزمان برگردانید، پس باید نمونه خود کلاس را برگردانید.
در نهایت، متد __exit__()
وظیفه بستن منبع خارجی مورد نظر را بر عهده دارد.
import sqlite3 class Database: def __init__(self, path: str): self.path = path def __enter__(self): self.connection = sqlite3.connect(self.path) self.cursor = self.connection.cursor() return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: print(f'an error occurred: {exc_val}') self.connection.close()
شما می توانید از این مدیریت متن همراه با عبارت with
در کد خود به صورت زیر استفاده کنید:
import sqlite3 create_table_sql_statement = 'CREATE TABLE IF NOT EXISTS books(title TEXT, author TEXT)' insert_into_table_sql_statement = "INSERT INTO books VALUES ('If Tomorrow Comes', 'Sidney Sheldon'), ('The Lincoln Lawyer', 'Michael Connelly')" select_from_table_sql_statement = 'SELECT * FROM books' class Database: def __init__(self, path: str): self.path = path def __enter__(self): self.connection = sqlite3.connect(self.path) self.cursor = self.connection.cursor() return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: print(f'an error occurred: {exc_val}') def main(): with Database(':memory:') as db: db.cursor.execute(create_table_sql_statement) db.connection.commit() db.cursor.execute(insert_into_table_sql_statement) db.connection.commit() db.cursor.execute(select_from_table_sql_statement) print(db.cursor.fetchall()) if __name__ == '__main__': main() # [('If Tomorrow Comes', 'Sidney Sheldon'), ('The Lincoln Lawyer', 'Michael Connelly')]
از خروجی فراخوانی تابع print()
مشهود است، برنامه با موفقیت داده های داده شده را از پایگاه داده ذخیره و بازیابی کرده است.
بدون دستور with
، Database
فقط یک کلاس قدیمی است. با این حال، لحظه ای که with
آن روبرو می شوید، سه روش وارد عمل می شوند.
متد __init__()
اولیه ساز است و به طور یکسان با سایر روش های اولیه کلاس پایتون کار می کند. مسیر را به پایگاه داده طی می کند.
متد __enter__()
اتصال به پایگاه داده را تنظیم می کند و نمونه کلاس مدیریت متن را به متغیر هدف، در این مورد db
برمی گرداند.
این متغیر هدف اکنون هم اتصال و هم اشیاء مکان نما را محصور می کند. می توانید به ترتیب به عنوان db.connection
و db.cursor
به آنها دسترسی داشته باشید.
هنگامی که کد داخل بلوک with
به پایان رسید، متد __exit__()
اجرا می شود و اتصال فعال به پایگاه داده را می بندد.
شما می توانید هر استثنایی را که ممکن است در حین اجرا رخ دهد در متد __exit__()
مدیریت کنید. اگر استثنا وجود داشته باشد، exc_type
نوع استثنا را نگه میدارد، exc_val
مقدار استثنا را نگه میدارد، exc_tb
ردیابی را نگه میدارد.
اگر هیچ استثنایی وجود نداشته باشد، این سه متغیر دارای مقدار None
خواهند بود. من در این مقاله وارد جزئیات رسیدگی به استثنا نمی شوم زیرا بسته به آنچه شما با آن سر و کار دارید، ممکن است اشکال مختلفی به خود بگیرد.
برای اینکه این مدیر زمینه سفارشی را از هر جایی در برنامه در دسترس قرار دهید، می توانید آن را در ماژول یا حتی بسته جداگانه خود قرار دهید.
این راه حل به مراتب بهتر از try...except...finally
نردبانی که قبلاً دیدید. شما مجبور نیستید خودتان را تکرار کنید و احتمال خطای انسانی کمتر است.
نحوه ایجاد یک مدیر زمینه مبتنی بر ژنراتور در پایتون
از عنوان این بخش مشهود است، این رویکرد از یک مولد به جای یک کلاس برای پیاده سازی مدیر زمینه استفاده می کند.
از نظر نحوی، ژنراتورها تقریباً مشابه توابع عادی هستند، با این تفاوت که شما باید به جای return
در ژنراتور yield
استفاده کنید.
نوشتن یک مدیر زمینه مبتنی بر مولد به کد کمتری نیاز دارد اما مقداری از خوانایی خود را نیز از دست می دهد.
میتوانید معادل مبتنی بر مولد مدیر زمینه Database
مبتنی بر کلاس را به صورت زیر بنویسید:
import sqlite3 from contextlib import contextmanager @contextmanager def database(path: str): connection = sqlite3.connect(path) try: cursor = connection.cursor() yield {'connection': connection, 'cursor': cursor} except Exception as e: print(f'an error occurred: {e}') finally: connection.close()
به جای یک کلاس، یک تابع مولد در اینجا دارید، پس هیچ مقدار اولیه وجود ندارد. در عوض، خود تابع می تواند مسیر پایگاه داده را به عنوان پارامتر بپذیرد.
در یک بلوک try
، می توانید یک اتصال به پایگاه داده برقرار کنید، مکان نما را نمونه برداری کنید و هر دو شی را به کاربر برگردانید.
شما می توانید yield connection, cursor
برای برگرداندن دو شیء بنویسید، اما در این صورت ژنراتور آنها را به صورت یک تاپل برمی گرداند.
من ترجیح می دهم از رشته ها به جای اعداد به عنوان ابزار دسترسی استفاده کنم و به همین دلیل است که این دو شی را در یک دیکشنری با کلیدهای توصیفی قرار داده ام.
بلوک except
در صورت استثنا اجرا خواهد شد. با خیال راحت هر استراتژی رسیدگی به استثنائات را که مناسب می دانید اجرا کنید.
بلوک finally
بدون قید و شرط اجرا می شود و اتصال باز در انتهای بلوک with
بسته می شود.
از آنجایی که هیچ روش __enter__()
یا __exit__()
وجود ندارد، باید ژنراتور را با تزئین کننده @contextmanager
تزئین کنید.
این دکوراتور یک تابع کارخانه را برای with
متن بیانیه تعریف می کند، بدون نیاز به ایجاد کلاس یا متدهای __enter__()
و __exit__()
.
استفاده از این مدیر زمینه با همتای مبتنی بر کلاس آن به جز حروف بزرگ نام آن یکسان است.
import sqlite3 from contextlib import contextmanager create_table_sql_statement = 'CREATE TABLE IF NOT EXISTS books(title TEXT, author TEXT)' insert_into_table_sql_statement = "INSERT INTO books VALUES ('If Tomorrow Comes', 'Sidney Sheldon'), ('The Lincoln Lawyer', 'Michael Connelly')" select_from_table_sql_statement = 'SELECT * FROM books' @contextmanager def database(path: str): connection = sqlite3.connect(path) try: cursor = connection.cursor() yield {'connection': connection, 'cursor': cursor} except Exception as e: print(f'an error occurred: {e}') finally: connection.close() def main(): database_path = ':memory:' with database(database_path) as db: db.get('cursor').execute(create_table_sql_statement) db.get('connection').commit() db.get('cursor').execute(insert_into_table_sql_statement) db.get('connection').commit() db.get('cursor').execute(select_from_table_sql_statement) print(db.get('cursor').fetchall()) if __name__ == '__main__': main() # [('If Tomorrow Comes', 'Sidney Sheldon'), ('The Lincoln Lawyer', 'Michael Connelly')]
از آنجایی که db
به جای یک شی در این مورد یک دیکشنری است، برای دسترسی به اتصال یا شی مکان نما باید از پرانتز مربع یا متد get()
استفاده کنید.
نتیجه
مدیریت زمینه در پایتون یکی از آن موضوعاتی است که بسیاری از برنامه نویسان از آن استفاده کرده اند اما به وضوح درک نمی کنند.
امیدوارم این مقاله برخی از ابهامات شما را برطرف کرده باشد.
اگر می خواهید به من وصل شوید، من همیشه در لینکدین در دسترس هستم. با خیال راحت پیامی بفرستید و من خوشحال می شوم پاسخ دهم. همچنین، اگر فکر میکنید این مفید بود، مهارتهای مرتبط من را در پلتفرم تأیید کنید.
تا مورد بعدی، مراقب باشید و به کاوش ادامه دهید.
ارسال نظر