عبارت with در پایتون

عبارت with در پایتون

عبارت with در پایتون معمولا یک عبارت مرموز درنظر گرفته می‌شود. اما زمانی که به پشت صحنه نگاه می‌کنید، می‌بینید که هیچ جادویی در کار نیست. عبارت with در واقع یک ویژگی جذاب و مفید است که به شما کمک می‌کند تا کد پایتونی تمیزتر و خواناتری داشته باشید.

ممکن است سوال کنید عبارت with در پایتون چه زمان هایی استفاده می‌شود؟ عبارت with زمانی استفاده می‌شود که بخواهیم با استفاده از الگوی استانداردی، به مدیریت منابع به صورت بهینه بپردازیم.

برای روشن تر شدن موضوع، به سراغ مثالی از کتابخانه open که یکی از کتابخانه های درونی پایتون است می‌رویم. کتابخانه open یکی از بهترین کتابخانه ها برای توضیح یک مثال در رابطه با عبارت with در پایتون است. در کد زیر، فایل hello.txt توسط عبارت with در پایتون با حالت w که برای write است باز شده است تا بتوانیم مقادیری را درون فایل بنویسیم.

with open('hello.txt', 'w') as f: 
    f.write('hello, world!')

به طور کلی توصیه می‌شود فایل ها را با استفاده از عبارت with در پایتون باز کنید. این کار باعث می‌شود زمانی که کار برنامه با فایل مورد نظر به اتمام می‌رسد، ارتباط برنامه با فایل به صورت خودکار بسته شود. در حقیقت زمانی که از عبارت with استفاده می‌کنید، در هسته پایتون کدهای شما به صورت زیر توسط مفسر پایتون ترجمه می‌شود.

f = open('hello.txt', 'w') 
    try:
        f.write('hello, world') 
    finally:
        f.close()

ممکن است بگویید این کد کمی طولانی است و کاملا درست است. به این نکته توجه کنید که استفاده از عبارت try … finally روشی قابل توجه برای مدیریت خطا است. برای مثال اگر برای این کار، کدی شبیه به زیر بنویسید، دچار مشکلاتی خواهید شد. (قبل از خواندن ادامه مطلب،‌ حدس بزنید چه مشکلی پیش خواهد آمد؟)

f = open('hello.txt', 'w') 
f.write('hello, world') 
f.close()

پیاده سازی فوق تضمین نمی‌کند که هنگام نوشتن در فایل، اگر با خطایی مواجه شدید پس از آن ارتباط با فایل توسط برنامه بسته شود و ممکن است برنامه با خطاهای متفاوتی روبرو شود. به همین دلیل است که در این مواقع از عبارت with در پایتون استفاده می‌کنیم. با استفاده از عبارت with در پایتون، پس از اتمام کار برنامه با منابع سیستم عامل، منابع به صورت خودکار آزاد خواهند شد.

یک استفاده جالب از عبارت with درپایتون، استفاده از آن هنگام پیاده سازی کلاس درونی threading.lock است.

some_lock = threading.Lock()

# Harmful:
some_lock.acquire()
try:
# Do something...
finally:
    some_lock.release()

    # Better:
    with some_lock:
# Do something...

همانطور که در کد بالا مشاهده می‌کنید، بجای نوشتن try … finally می‌توان از عبارت with استفاده کرد تا کد بتواند از منابع استفاده کند و سپس منابع را آزاد کند. استفاده از عبارت with در پایتون باعث خوانایی راحت کد هنگام استفاده از منابع سیستمی می‌شود. همچنین عبارت with باعث می‌شود تا از به وجود آمدن آسیب‌پذیری امنیتی جلوگیری شود به دلیل اینکه هرزمان که کار برنامه با منابع تمام شد، منابع را به صورت خودکار آزاد می‌کند.


استفاده از عبارت with در پایتون و اشیا اختصاصی

اکنون متوجه شدید که هیچ چیز جادویی در رابطه با تابع open یا کلاس threading.Lock وجود ندارد و حقیقت این است که این اشیا را از طریق عبارت with در پایتون فراخوانی می‌کنیم. شما می‌توانید همین عملکرد را به صورت شخصی‌سازی شده در کلاس ها و توابع خود، با استفاده از Context Manager ها در پایتون پیاده سازی کنید.


آشنایی با Context Manager در پایتون

Context Manager در پایتون چیست؟ یک پروتکل (مجموعه‌ای از قوانین) است که اشیا برنامه باید از آن پیروی کنند تا اشیا بتوانند از عبارت with پشتیبانی کنند.

اگر بخواهیم عمیق‌تر به هسته پایتون بنگریم، برای پیاده سازی این قابلیت باید توابع جادویی __enter__ و __exit__ پیاده سازی شوند تا عملکردی مشابه با context manager ایجاد کنیم. زبان پایتون به صورت خودکار، دو تابع فوق را هنگام مواجهه با چرخه مدیریت منابع سیستم فراخوانی می‌کند. (به توابعی نظیر __enter__ و __exit__ در پایتون توابع جادویی یا Magic Method گفته می‌شود، هرچند من با این اسم موافق نیستم زیرا هیچ چیز جادویی وجود ندارد!)

بیایید به این موضوع در عمل نگاه کنیم. در زیر یک پیاده سازی ساده از لایه انتزاعی تابع open پایتون نوشته‌ایم.

class ManagedFile:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        self.file = open(self.name, 'w')
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()

کلاس ManagedFile از پروتکل context manager پیروی می‌کند و به همین دلیل از عبارت with پشتیبانی می‌کند. مشابه با همان کاری که در مثال تابع open انجام دادیم را می‌توانیم با کلاسی که به تازگی توسعه داده ایم، انجام دهیم.

>>> with ManagedFile('hello.txt') as f:
    f.write('hello, world!')
    f.write('bye now')

زبان برنامه نویسی پایتون زمانی تابع جادویی __enter__ را فراخوانی می‌کند که برنامه شروع به اجرای کدهای درون قسمت with می‌کند و برنامه نیاز به در اختیار گرفتن منابع دارد. هنگامی که اجرای برنامه در قسمت with به پایان می‌رسد، پایتون تابع جادویی __exit__ را فراخوانی می‌کند تا منابعی که در اختیار گرفته شده بودند، آزاد شوند.

نوشتن یک کلاس از جنس context manager تنها راه برای پشتیبانی از عبارت with در پایتون نیست. همانطور که مشاهده کردید کلاس ManagedFile تعداد خط کدهای زیادی داشت و در زبان برنامه نویسی پایتون همیشه به دنبال ساده ترین و بهترین راه حل هستیم.

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

برای مثال، در قطعه کد زیر با استفاده از یک decorator پایتون به اسم contextmanager می‌توان امکان پشتیبانی از عبارت with در پایتون را به وجود آورد. در اینجا مثال قبلی کلاس ManagedFile را به صورت تابعی بازنویسی می‌کنیم تا ببینیم این تکنیک چگونه کار می‌کند.

from contextlib import contextmanager

@contextmanager
def managed_file(name): 
    try:
        f = open(name, 'w')
        yield f 
    finally:
        f.close()
        
>>> with managed_file('hello.txt') as f:
    f.write('hello, world!')
    f.write('bye now')

در این مثال، تابع managed_file یک generator است که ابتدا منابع را در اختیار می‌گیرد و پس از آن با استفاده از yeild امکان استفاده برای منابع را برای درخواست کننده فعال می‌کند. زمانی که اجرای عبارت with در پایتون به اتمام برسد، generator شروع به تمیزکاری و آزاد کردن منابع می‌کند تا اختیار استفاده از منابع به سیستم عامل برگردد.

پیاده سازی context manager به دو صورت class-based و generator-based در عمل کاملا مشابه هستند. بهتر است شما با هر روشی که راحت هستید، همان روش را انتخاب کنید.

برای درک بهتر پیاده سازی context manager در پایتون بهتر است ابتدا درک عمیقی از decorator ها و generator ها در پایتون به دست آورید. یک بار دیگر برای تاکید می‌گویم، این که از کدام روش پیاده سازی استفاده کنید کاملا به اینکه شما و تیمتان با کدام روش راحت تر است، بستگی دارد. همیشه راهی را انتخاب کنید که به خوانایی کدهایتان افزوده شود.


نوشتن API های زیبا توسط Context Manager در پایتون

Context manager ها در پایتون کاملا انعطاف پذیر هستند. اگر از عبارت with در پایتون به صورت خلاقانه ای استفاده کنید می‌توانید API های زیبایی برای ماژول ها و کلاس های خود طراحی کنید.

برای مثال، اگر منابعی که می‌خواهیم استفاده کنیم مجموعه از نوشته ها باشند که توسط یک برنامه گزارش گیری تولید شده اند و مجموعه ای از تو رفتگی ها (indent) در آن وجود دارد، چطور این کار را انجام می‌دهیم؟

بیشتر توضیح می‌دهم، اگر بخواهید برنامه ای بنویسید که گزارشی به صورت متن تولید می‌کند و این گزارش در هر سطح شامل یک سری تو رفتگی‌ها است، چطور این کار را انجام می‌دهید؟ چطور می‌توانستیم یک لایه انتزاعی پیاده سازی کنیم تا این کار را به راحتی انجام دهیم؟

with Indenter() as indent: 
    indent.print('hi!') 
    with indent:
        indent.print('hello') 
        with indent:
            indent.print('bonjour') 
    indent.print('hey')

کد بالا شبیه زبان های ‌domain-specific برای ایجاد تورفتگی در متن‌ها است. به کد بالا دقت کنید، متوجه می‌شوید چندین بار وارد context manager های یکسان شده و از آن خارج شده ایم تا تورفتگی ها را اعمال کنیم. اجرای کد بالا باید نتیجه ای مشابه زیر را در کنسول برای شما چاپ کند.

hi!
    hello
        bonjour
hey

بنابراین، برگردیم به اصل موضوع، چطور یک context manager می‌سازید تا عملکرد بالا را بتوانید ایجاد کنید؟

به هرحال، این می‌تواند تمرین خوبی باشد تا به درک عمیقی از context manager ها در پایتون برسید. قبل از اینکه پیاده سازی من را چک کنید، مقداری زمان بگذارید و سعی کنید یک پیاده سازی انتزاعی به عنوان تمرین برای کد بالا انجام دهید.

اگر آماده هستید که پیاده سازی من را ببینید، این روشی است که من برای پیاده سازی یک context manager به صورت class-based انجام می‌دهم تا بتوانم قابلیت ایجاد تورفتگی را در کدهایم را به صورت یک API ایجاد کنم.

class Indenter:
    def __init__(self):
        self.level = 0

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.level -= 1

    def print(self, text):
        print(' ' * self.level + text)

زیاد بد نیست، درست است؟ اکنون من می‌توانم از شی Indenter به عنوان یک API درونی در کد‌هایم استفاده کنم. امیدوارم با خواندن این مطلب، درک بهتری از context manager ها و عبارت with در برنامه های پایتونی بدست آورید. این یک ویژگی جذاب در پایتون است که با آن می‌توانید منابع سیستم را پایتونیک تر مدیریت کنید. همانطور که دیدید هیچ چیز مرموزی در رابطه با عبارت with در پایتون وجود نداشت.


نکات کلیدی عبارت with در پایتون

  • استفاده از عبارت with باعث می‌شود بتوانید راحت تر خطاهای برنامه را مدیریت کنید زیرا با این کار عملیات encapsulation را بر روی عبارت استاندارد try…finally انجام می‌دهید.
  • بیشترین استفاده از عبارت with زمانی است که می‌خواهید منابعی را توسط برنامه پایتونی در اختیار بگیرید و پس از انجام کار، منابع را به صورت خودکار آزاد کنید.
  • استفاده از عبارت with باعث جلوگیری از نشت منابع سیستم‌عامل خواهد شد و همچنین خوانایی بهتری به کدهای شما اضافه می‌کند.
محمد بابازاده
متخصص DevOps و توسعه دهنده Python