راه
۵ کیو· متوسط رو به بالامتوسطPYTHON۴ ثانیه · ۲۵۶ مگابایت

پروکسی (Proxy Pattern)

در دنیای برنامه‌نویسی شیءگرا، الگوی طراحی پروکسی (Proxy Pattern) یک روش برای کنترل دسترسی به یک شیء است. پروکسی در واقع یک نماینده یا جایگزین برای یک شیء اصلی (که به آن شیء هدف گفته می‌شود) است. این نماینده به جای این‌که مستقیماً شیء اصلی را در معرض دید قرار دهد، خودش واسطی برای ارتباط با آن می‌شود. این الگو به ما امکان می‌دهد تا قبل یا بعد از انجام عملیات روی شیء اصلی، کارهای دیگری مانند کنترل دسترسی، لاگینگ (Logging)، یا رهگیری (Tracking) انجام دهیم، بدون این‌که کد اصلی شیء هدف را تغییر دهیم.


کلاس Proxy

از شما می‌خواهیم کلاس Proxy را تکمیل کنید. این کلاس باید به‌عنوان نماینده‌ای برای هر شیء دلخواه عمل کند و دسترسی به ویژگی‌ها و متدهای آن شیء را از طریق خودش فراهم کند، در حالی که اطلاعات مربوط به این دسترسی‌ها را نیز ثبت و ارائه می‌دهد.


این متد سازنده‌ی کلاس Proxy است و وظیفه‌ی آن مقداردهی اولیه‌ی یک شیء Proxy است. این متد یک پارامتر ورودی به نام obj دریافت می‌کند که همان شیء هدف است؛ شیئی که این نمونه‌ی Proxy نماینده‌ی آن خواهد بود.

در این متد باید شیء هدف (obj) را در یک ویژگی نمونه (مثلاً self._obj) ذخیره کنید. هم‌چنین، باید ویژگی‌های دیگری که نیاز دارید را نیز در این‌جا آماده کنید.

💡 راهنمایی ۱: در متد __init__، علاوه بر ذخیره‌ی شیء اصلی (self._obj)، دو ویژگی کمکی برای رهگیری اضافه کنید: _last_accessed: یک متغیر (رشته) برای ذخیره‌ی نام آخرین عضوی که به آن دسترسی پیدا کرده‌ایم. مقدار اولیه‌ی آن را None قرار دهید. _access_counts: یک دیکشنری برای ذخیره‌ی تعداد دسترسی‌ها به هر عضو. کلیدهای آن نام اعضا (رشته) و مقادیر آن تعداد دفعات دسترسی (عدد صحیح) خواهد بود. مقدار اولیه‌ی آن باید یک دیکشنری خالی ({}) باشد. هر بار که در ادامه (در متد __getattr__) دسترسی به یک عضو معتبر در شیء هدف را تشخیص داده و آن عضو را برمی‌گردانید، باید این دو ویژگی را به‌روزرسانی کنید.

python
def __init__(self, obj):
    self._obj = obj
    self._last_accessed = None
    self._access_counts = {}

این متد نام آخرین متد یا ویژگی معتبری که با موفقیت از طریق این نمونه‌ی Proxy روی شیء هدف دسترسی یا فراخوانی شده‌است را برمی‌گرداند. خروجی این متد یک رشته است که نام آخرین عضوی است که به آن دسترسی پیدا کردیم.

اگر تا لحظه‌ی فراخوانی این متد، به هیچ عضو معتبری روی شیء هدف از طریق این پروکسی دسترسی پیدا نکرده بودیم، این متد باید یک استثناء با پیام "No attribute was accessed." پرتاب کند.

python
def last_accessed_attribute(self):
    if self._last_accessed is None:
        raise Exception("No attribute was accessed.")
    return self._last_accessed

این متد تعداد دفعاتی که یک عضو خاص (متد یا ویژگی) با نام attribute_name روی شیء هدف از طریق این نمونه Proxy به آن دسترسی پیدا کردیم را گزارش می‌دهد. این متد یک پارامتر ورودی به نام attribute_name دریافت می‌کند که نام متد یا ویژگی موردنظر است (از نوع رشته). خروجی آن یک عدد صحیح است که نشان‌دهنده‌ی تعداد دفعات دسترسی یا فراخوانی است.

اگر attribute_name ورودی، نام یک عضو معتبر در شیء هدف نباشد، یا اگر عضو معتبری باشد اما هنوز از طریق این پروکسی دسترسی یا فراخوانی نشده باشد، متد باید مقدار 0 را برگرداند.

python
def count_of_accesses(self, attribute_name):
    return self._access_counts.get(attribute_name, 0)

این متد بررسی می‌کند که آیا به یک عضو خاص (متد یا ویژگی) با نام attribute_name روی شیء هدف از طریق این نمونه‌ی Proxy حداقل یک بار دسترسی پیدا کرده‌ایم یا خیر. این متد یک پارامتر ورودی به نام attribute_name دریافت می‌کند که نام متد یا ویژگی مورد نظر است (از نوع رشته). خروجی آن یک مقدار منطقی است: True اگر عضو با موفقیت حداقل یک بار دسترسی یا فراخوانی شده باشد و False در غیر این صورت.

اگر attribute_name ورودی، نام یک عضو معتبر در شیء هدف نباشد، متد باید مقدار False را برگرداند.

python
def was_accessed(self, attribute_name):
    return self._access_counts.get(attribute_name, 0) > 0

این متد قلب کلاس Proxy است و مسئول رهگیری دسترسی به اعضای شیء هدف می‌باشد. زمانی که پایتون ویژگی خواسته شده را در خودِ کلاس Proxy پیدا نمی‌کند، این متد فراخوانی می‌شود.

الگوریتم پیشنهادی برای پیاده‌سازی __getattr__:

  1. بررسی وجود: با استفاده از تابع hasattr(self._obj, name) بررسی کنید که آیا ویژگی درخواست شده در شیء هدف (self._obj) وجود دارد یا خیر.
  2. اگر وجود نداشت: استثناء Exception("No such attribute.") را پرتاب کنید (raise).
  3. اگر وجود داشت (موفقیت): نام ویژگی (name) را در self._last_accessed ذخیره کنید. تعداد دسترسی به این نام را در دیکشنری self._access_counts اضافه کنید (اگر قبلاً بوده یکی اضافه کنید، اگر نبوده مقدار ۱ بگذارید). در نهایت با استفاده از getattr(self._obj, name) مقدار واقعی را از شیء هدف گرفته و return کنید.
python
def __getattr__(self, name):
    if not hasattr(self._obj, name):
        raise Exception("No such attribute.")
    self._last_accessed = name
    self._access_counts[name] = self._access_counts.get(name, 0) + 1
    return getattr(self._obj, name)

🎯 مثال

فرض کنید کلاس ساده‌ای به نام Radio داریم.

python
class Radio():
    def __init__(self):
        self._channel = None
        self.is_on = False
        self.volume = 0

    def get_channel(self):
        return self._channel

    def set_channel(self, value):
        self._channel = value

    def power(self):
        self.is_on = not self.is_on

    def __str__(self):
        return f"Radio(channel={self._channel}, is_on={self.is_on}, volume={self.volume})"

حالا، با استفاده از کلاس Proxy‌ای که شما پیاده‌سازی می‌کنید، می‌توانیم یک نمونه از Radio ایجاد کرده و سپس یک پروکسی برای آن بسازیم.

python
radio = Radio()
radio_proxy = Proxy(radio)

با استفاده از radio_proxy، باید بتوانیم به تمام متدها و ویژگی‌های شیء اصلی radio دسترسی داشته باشیم و همزمان کلاس Proxy این دسترسی‌ها و فراخوانی‌ها را رهگیری کند.

python
radio_proxy.set_channel(95)
radio_proxy.power()
radio_proxy.set_channel(100)
print(radio_proxy.is_on)                                     # True
print(radio_proxy.last_accessed_attribute())                 # is_on
print(radio_proxy.count_of_accesses('set_channel'))          # 2
print(radio_proxy.was_accessed('power'))                     # True
print(radio_proxy.count_of_accesses('non_existent_method'))  # 0
print(radio_proxy.was_accessed('volume'))                    # False

خروجی:

text
True
is_on
2
True
0
False

💡 چالش‌های حل تمرین

⚠️ چگونه به ویژگی‌های موجود در شیء هدف دسترسی پیدا کنم و آن‌ها را رهگیری کنم و تعداد دفعات دسترسی را بشمارم؟

برای رهگیری دسترسی به ویژگی‌ها و متدهایی که به‌صورت مستقیم در کلاس Proxy تعریف نشده‌اند، باید متد __getattr__(self, name) را در کلاس Proxy پیاده‌سازی (override) کنید. این متد زمانی فراخوانی می‌شود که پایتون ویژگی خواسته شده را در خودِ کلاس Proxy پیدا نمی‌کند.

الگوریتم پیشنهادی برای پیاده‌سازی __getattr__:

  1. بررسی وجود: با استفاده از تابع hasattr(self._obj, name) بررسی کنید که آیا ویژگی درخواست شده در شیء هدف (self._obj) وجود دارد یا خیر.
  2. اگر وجود نداشت: استثناء Exception("No such attribute.") را پرتاب کنید (raise).
  3. اگر وجود داشت (موفقیت): نام ویژگی (name) را در self._last_accessed ذخیره کنید. تعداد دسترسی به این نام را در دیکشنری self._access_counts اضافه کنید (اگر قبلاً بوده یکی اضافه کنید، اگر نبوده مقدار ۱ بگذارید). در نهایت با استفاده از getattr(self._obj, name) مقدار واقعی را از شیء هدف گرفته و return کنید.

✅ این تمرین را در ویرایشگر خود پیاده‌سازی کنید. به یاد داشته باشید که کلاس Proxy باید تمام ویژگی‌ها و متدهای شیء هدف را رهگیری کند.

نمونه‌ها

ورودی نمونه ۱
radio = Radio()
radio_proxy = Proxy(radio)
radio_proxy.set_channel(95)
radio_proxy.power()
radio_proxy.set_channel(100)
print(radio_proxy.is_on)
print(radio_proxy.last_accessed_attribute())
print(radio_proxy.count_of_accesses('set_channel'))
print(radio_proxy.was_accessed('power'))
print(radio_proxy.count_of_accesses('non_existent_method'))
print(radio_proxy.was_accessed('volume'))
خروجی نمونه ۱
True
is_on
2
True
0
False
ورودی نمونه ۲
p = Proxy(Radio())
try:
    p.last_accessed_attribute()
except Exception as error:
    print(error)
خروجی نمونه ۲
No attribute was accessed.
ورودی نمونه ۳
p = Proxy(Radio())
p.volume
try:
    p.nope
except Exception as error:
    print(error)
p.last_accessed_attribute()
p.count_of_accesses("nope")
p.was_accessed("nope")
خروجی نمونه ۳
0
No such attribute.
'volume'
0
False
ورودی نمونه ۴
p = Proxy(Radio())
p.volume
p.volume
p.count_of_accesses("volume")
p.was_accessed("volume")
p.last_accessed_attribute()
خروجی نمونه ۴
0
0
2
True
'volume'
ورودی نمونه ۵
radio = Radio()
p = Proxy(radio)
p.set_channel(7)
p.get_channel()
radio.get_channel()
p.count_of_accesses("get_channel")
خروجی نمونه ۵
7
7
1

برای نوشتن پاسخ و دریافت امتیاز، ابتدا وارد حساب شوید.

ورود