فصل چهاردهم: مدیریت حافظه و بهینه‌سازی

Please login to bookmark Close

مقدمه

در این فصل، به زیر پوست پایتون سفر می‌کنیم. خواهیم دید که اشیاء در حافظه چگونه نگهداری و آزاد می‌شوند. یاد می‌گیریم که __slots__ چگونه مصرف حافظه را کاهش می‌دهد. با weakref آشنا می‌شویم که از نشت حافظه جلوگیری می‌کند. و در نهایت، روش‌های درست کپی و سریال‌سازی اشیاء را بررسی می‌کنیم.

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

چرا مدیریت حافظه را یاد بگیریم؟

بسیاری از برنامه‌نویسان کدی می‌نویسند که در ابتدا کار می‌کند. اما وقتی همان کد در مقیاس بزرگ اجرا می‌شود، با مشکلات جدی روبرو می‌شود. مصرف حافظه به شدت افزایش می‌یابد. اشیاء به درستی آزاد نمی‌شوند. و برنامه به مرور زمان کند و پرمصرف می‌شود.

یادگیری مدیریت حافظه به شما کمک می‌کند تا از این مشکلات جلوگیری کنید. کدی بنویسید که در محیط production دوام بیاورد. Memory Leak را شناسایی و رفع کنید. و بین خوانایی کد و کارایی آن، به طور آگاهانه تصمیم بگیرید.

مدیریت حافظه در پایتون

اشیاء، Heap و Reference

در پایتون، هر شیء در بخش خاصی از حافظه به نام Heap ساخته می‌شود. Heap فضایی است که اشیاء در طول عمر برنامه در آن جاگذاری می‌شوند. نکته‌ی کلیدی این است که متغیرها خود شیء را در خود نگه نمی‌دارند. در عوض، متغیرها فقط یک Reference (اشاره‌گر) به آن شیء روی Heap هستند.

این مدل، پیامدهای مهمی دارد:

a = [1, 2, 3]      # یک لیست در Heap ساخته می‌شود و 'a' به آن اشاره می‌کند
b = a              # 'b' نیز به همان لیست اشاره می‌کند. هیچ کپی ساخته نمی‌شود!
b.append(4)
print(a)           # [1, 2, 3, 4] — چون 'a' و 'b' به یک شیء اشاره دارند
print(a is b)      # True

این رفتار که «اشتراک Reference» نام دارد، منبع بسیاری از باگ‌هاست. وقتی یک متغیر را به متغیر دیگر نسبت می‌دهیم، یک شیء جدید ساخته نمی‌شود. فقط یک Reference جدید به شیء موجود ایجاد می‌شود. برای ساخت یک کپی واقعی، باید از ابزارهایی مانند copy استفاده کرد که در ادامه به آنها خواهیم پرداخت.

Mutable و Immutable درون self

تمایز بین اشیاء Mutable (مانند list، dict، set) و Immutable (int، str، tuple) زمانی که داده‌ای را درون self نگه می‌دارید، اهمیت ویژه‌ای پیدا می‌کند. یک دام کلاسیک و بسیار رایج، استفاده از مقدار پیش‌فرض Mutable در متد __init__ است:

class ShoppingCart:
    def __init__(self, items=[]):      # تله! لیست پیش‌فرض بین همه‌ی نمونه‌ها مشترک می‌شود
        self.items = items

c1 = ShoppingCart()
c2 = ShoppingCart()
c1.items.append("Book")
print(c2.items)      # ['Book'] — فاجعه! c2 نیز این تغییر را دید

چرا این اتفاق می‌افتد؟ چون لیست پیش‌فرض، فقط یک بار و در زمان تعریف تابع ساخته می‌شود. سپس همان یک لیست، بین تمام فراخوانی‌های تابع که از مقدار پیش‌فرض استفاده می‌کنند، به اشتراک گذاشته می‌شود. راه درست، استفاده از None به عنوان مقدار پیش‌فرض است:

class ShoppingCart:
    def __init__(self, items=None):
        self.items = items if items is not None else []   # هر بار یک لیست تازه ساخته می‌شود

c1 = ShoppingCart()
c2 = ShoppingCart()
c1.items.append("Book")
print(c2.items)      # [] — درست است

قاعده‌ی طلایی: هرگز از یک شیء Mutable (list، dict، set و غیره) به عنوان مقدار پیش‌فرض برای پارامترهای تابع یا متد استفاده نکنید. همیشه از None استفاده کنید و درون تابع، شیء مورد نیاز را بسازید.

Reference Counting و Garbage Collector

پایتون برای مدیریت حافظه و آزادسازی اشیایی که دیگر مورد استفاده نیستند، از دو مکانیزم اصلی استفاده می‌کند. درک این دو مکانیزم برای نوشتن کدهای بهینه ضروری است.

Reference Counting

هر شیء در پایتون، یک شمارنده دارد که تعداد Referenceهایی را که به آن شیء اشاره می‌کنند، نشان می‌دهد. وقتی این شمارنده به صفر برسد، یعنی هیچ متغیر یا ساختار دیگری به آن شیء اشاره نمی‌کند، پایتون بلافاصله حافظه‌ی آن شیء را آزاد می‌کند.

import sys
a = [1, 2, 3]
print(sys.getrefcount(a))   # تعداد Referenceها (یک عدد اضافی به دلیل فراخوانی خود getrefcount)
b = a
print(sys.getrefcount(a))   # یک عدد افزایش یافته است
del b
print(sys.getrefcount(a))   # یک عدد کاهش یافته است

این مکانیزم بسیار سریع و کارآمد است و حافظه را به محض بی‌استفاده شدن آزاد می‌کند.

Garbage Collector (GC)

Reference Counting یک نقطه‌ضعف اساسی دارد: Cyclic Reference (ارجاع حلقوی). اگر دو شیء به یکدیگر اشاره کنند، شمارنده‌ی Reference هر یک هرگز به صفر نمی‌رسد، حتی اگر هیچ متغیر خارجی دیگری به آنها اشاره نکند. در این حالت، آن دو شیء غیرقابل دسترس می‌شوند اما حافظه‌شان آزاد نمی‌شود.

class Node:
    def __init__(self):
        self.partner = None

a = Node()
b = Node()
a.partner = b      # a به b اشاره می‌کند
b.partner = a      # b به a اشاره می‌کند — یک حلقه ساخته شد!
del a
del b
# شمارنده‌ی Reference هیچ‌کدام به صفر نمی‌رسد، اما هیچ کس دیگری به آنها دسترسی ندارد

اینجاست که Garbage Collector وارد عمل می‌شود. GC به صورت دوره‌ای اجرا می‌شود و حلقه‌های Reference را که غیرقابل دسترس شده‌اند، پیدا کرده و حافظه‌ی آنها را آزاد می‌کند.

import gc
gc.collect()       # فراخوانی دستی GC (به ندرت لازم می‌شود)

خلاصه‌ی تفاوت‌ها:

  • Reference Counting: فوری است، اما حلقه‌های Reference را تشخیص نمی‌دهد.
  • Garbage Collector: دوره‌ای است و حلقه‌های Reference را شناسایی و پاک‌سازی می‌کند.

چرا نباید از __del__ استفاده کنیم؟

یک متد جادویی به نام __del__ در پایتون وجود دارد که به عنوان finalizer شناخته می‌شود. زمانی که یک شیء قرار است آزاد شود، این متد (در صورت وجود) فراخوانی می‌شود. اما استفاده از آن بسیار خطرناک و نامطمئن است و کارشناسان پایتون به شدت توصیه می‌کنند که از آن اجتناب کنید.

یک مثال ساده از زندگی روزمره

فرض کنید یک کتابخانه هستید. به هر کسی که کتاب امانت می‌دهید، باید مطمئن شوید که کتاب را به موقع پس می‌گیرید. شما دو راه دارید:

  • راه اول (مشکل‌دار): به خودتان بگویید «هر وقت کتاب برگشت، من آن را ثبت می‌کنم». اما اگر کسی کتاب را نیاورد، شما هیچ‌وقت متوجه نمی‌شوید. حتی اگر کتاب را بیاورد، ممکن است شما سرگرم کار دیگری باشید و متوجه نشوید.
  • راه دوم (درست): یک سیستم قرض‌دادن رسمی داشته باشید. به هر کس کتاب می‌دهید، یک برگه امضا می‌کند که در آن تاریخ بازگشت نوشته شده است. در تاریخ مقرر، شما به طور قطعی می‌دانید که کتاب باید برگردد و پیگیری می‌کنید.

__del__ دقیقاً شبیه راه اول است. شما به پایتون می‌گویید: «هر وقت این شیء قرار است آزاد شود، این کار را هم انجام بده.» اما پایتون همیشه نمی‌داند چه وقت شیء قرار است آزاد شود، و گاهی به شما قول می‌دهد اما نمی‌تواند عمل کند.

چالش اول: زمان آزادسازی مشخص نیست

به این مثال دقت کنید:

class Resource:
    def __del__(self):
        print("من آزاد شدم!")   # کی؟ خدا می‌داند!

r = Resource()
del r   # من می‌خواهم الان آزاد شود و پیام را ببینم

شما فکر می‌کنید با del r، بلافاصله پیام “من آزاد شدم!” را می‌بینید. اما همیشه اینطور نیست.

مشکل: پایتون در برخی موارد، __del__ را به تعویق می‌اندازد. مثلاً اگر بین اشیاء حلقه‌های ارجاعی وجود داشته باشد، Garbage Collector باید پیدا کند که آیا این شیء واقعاً باید آزاد شود یا نه. این کار ممکن است چند ثانیه یا حتی چند دقیقه طول بکشد. در ضمن، اگر برنامه در حال بسته شدن باشد، ممکن است اصلاً __del__ اجرا نشود.

چالش دوم: خطاها بی‌صدا می‌میرند

به این کد نگاه کنید:

class Resource:
    def __del__(self):
        print("آزادسازی...")
        raise Exception("یک مشکلی پیش آمد!")   # این خطا قرار نیست کسی ببیند!

r = Resource()
del r
# هیچ خطایی نمی‌بینید! فقط "آزادسازی..." چاپ می‌شود

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

چالش سوم: GC را پیچیده می‌کند

اگر یک شیء هم __del__ داشته باشد و هم در یک حلقه‌ی ارجاعی قرار بگیرد، Garbage Collector گیج می‌شود. برای درک این موضوع، ابتدا باید بدانیم حلقه‌ی ارجاعی چیست.

یادآوری حلقه‌ی ارجاعی

فرض کنید دو شیء به نام‌های a و b داریم که به یکدیگر اشاره می‌کنند:

class Node:
    def __init__(self):
        self.partner = None

a = Node()
b = Node()
a.partner = b   # a به b اشاره می‌کند
b.partner = a   # b به a اشاره می‌کند — یک حلقه ساخته شد

در این حالت، a و b یک حلقه‌ی ارجاعی تشکیل داده‌اند. حالا فرض کنید برنامه‌نویس تصمیم می‌گیرد که دیگر به این اشیاء نیاز ندارد و می‌نویسد:

del a
del b

پس از این دستورات، دیگر هیچ متغیر مستقیمی به a و b اشاره نمی‌کند. اما چون a.partner به b اشاره دارد و b.partner به a اشاره دارد، شمارنده‌ی ارجاع (Reference Count) هیچ‌کدام به صفر نمی‌رسد. این دو شیء، یک حلقه‌ی غیرقابل دسترس را تشکیل داده‌اند که باید توسط Garbage Collector (GC) شناسایی و پاک‌سازی شود.

گره‌ی کور: وجود __del__

حالا بیایید فرض کنیم که کلاس Node دارای متد __del__ (finalizer) نیز باشد:

class Node:
    def __init__(self):
        self.partner = None

    def __del__(self):
        print(f"Node with id {id(self)} is being destroyed")

حالا همان حلقه‌ی ارجاعی را می‌سازیم:

a = Node()
b = Node()
a.partner = b
b.partner = a

del a
del b

اینجا GC با یک معمای واقعی روبرو می‌شود. وقتی GC می‌خواهد حلقه‌های ارجاعی را پاک‌سازی کند، با این سوال مواجه می‌شود: «اگر اشیاء داخل این حلقه را آزاد کنم و بعداً متد __del__ یکی از آنها اجرا شود که به شیء دیگری در همین حلقه نیاز داشته باشد، چه اتفاقی می‌افتد؟»

GC برای جلوگیری از این آشفتگی، یک تصمیم منطقی می‌گیرد: اشیایی که __del__ دارند را به عنوان «غیرقابل جمع‌آوری» (Uncollectable) علامت‌گذاری می‌کند. به عبارت دیگر، GC از آزاد کردن این حلقه‌ها صرف‌نظر می‌کند.

نتیجه‌ی این تصمیم

  • حافظه آزاد نمی‌شود: آن دو شیء (a و b) برای همیشه در حافظه باقی می‌مانند. برنامه‌ی شما به مرور زمان، حافظه‌ی بیشتری مصرف می‌کند و ممکن است دچار نشت حافظه (Memory Leak) شود.
  • __del__ هرگز اجرا نمی‌شود: GC این اشیاء را «غیرقابل جمع‌آوری» می‌داند و دیگر به آنها توجه نمی‌کند. بنابراین متد __del__ شما، که برای بستن یک منبع مهم نوشته شده بود، هرگز فراخوانی نمی‌شود.
  • GC باید منابع اضافی صرف کند: GC برای شناسایی این که آیا اشیاء با __del__ در حلقه هستند، باید بررسی‌های بیشتری انجام دهد که این کار، سرعت کلی برنامه را کاهش می‌دهد.

یک مثال قابل لمس

تصور کنید یک کلاس DatabaseConnection دارید که در __del__ خود، اتصال به پایگاه داده را می‌بندد. حالا فرض کنید یک شیء از این کلاس، به یک شیء دیگر (که خودش به آن اشاره دارد) ارجاع می‌دهد و یک حلقه می‌سازد. شما انتظار دارید که __del__ اجرا شود و اتصال بسته شود. اما همان‌طور که گفتیم، GC این حلقه را آزاد نمی‌کند. در نتیجه، اتصال به پایگاه داده برای همیشه باز می‌ماند و تعداد اتصالات باز برنامه به مرور افزایش می‌یابد تا جایی که برنامه با خطای “Too many connections” از کار بیفتد. این یک نشت حافظه و همچنین نشت منبع (Resource Leak) است که به سختی قابل ردیابی است.

راه‌حل درست: Context Manager

به جای اینکه به پایتون بگویید «هر وقت شیء آزاد شد، این کار را بکن»، به پایتون می‌گویید «تا وقتی که با شیء کار دارم، آن را باز نگه دار و بعد از اتمام کار، حتماً آن را ببند».

این کار را با with انجام می‌دهیم:

class Resource:
    def __enter__(self):
        print("منبع باز شد")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("منبع بسته شد (حتماً! حتی اگر خطا رخ دهد)")

# استفاده درست
with Resource() as r:
    print("کار با منبع...")
    # اگر اینجا خطا هم رخ دهد، باز هم __exit__ اجرا می‌شود

# خروجی:
# منبع باز شد
# کار با منبع...
# منبع بسته شد (حتماً! حتی اگر خطا رخ دهد)

مزیت: شما می‌دانید که چه وقت منبع بسته می‌شود. اگر خطایی رخ دهد، باز هم بسته می‌شود. و هیچ ابهامی در زمان اجرا وجود ندارد.

الگوی Flyweight

Flyweight یک الگوی طراحی است که هدف آن صرفه‌جویی در حافظه است. ایده‌ی اصلی این است که به جای ساختن هزاران شیء تکراری و مجزا، یک شیء مشترک را بین بخش‌های مختلف برنامه بازاستفاده کنیم.

خود پایتون به صورت داخلی از این الگو برای اعداد صحیح کوچک (از ۲۵۵- تا ۲۵۶) و برخی رشته‌های کوتاه استفاده می‌کند:

a = 256
b = 256
print(a is b)      # True — پایتون اعداد کوچک را cache می‌کند (Flyweight داخلی)

x = 1000
y = 1000
print(x is y)      # احتمالاً False — اعداد بزرگ معمولاً cache نمی‌شوند

اگر با اشیاء سنگین و پرمصرفی کار می‌کنید، می‌توانید الگوی Flyweight را به صورت دستی پیاده‌سازی کنید:

class Color:
    _cache = {}

    def __new__(cls, name):
        if name not in cls._cache:
            instance = super().__new__(cls)
            instance.name = name
            cls._cache[name] = instance      # شیء ساخته شده را در cache ذخیره می‌کنیم
        return cls._cache[name]              # شیء موجود در cache را بازمی‌گردانیم

red1 = Color("red")
red2 = Color("red")
print(red1 is red2)      # True — هر دو به یک شیء اشاره می‌کنند

در اینجا، متد جادویی __new__ (که در فصل دوم معرفی شد) کنترل می‌کند که آیا یک شیء تازه ساخته شود یا شیء موجود از cache بازگردانده شود.

__slots__

__slots__ چیست و چطور حافظه را کم می‌کند؟

به طور پیش‌فرض، هر شیء در پایتون، attributeهای خود را در یک دیکشنری داخلی به نام __dict__ نگهداری می‌کند. این دیکشنری انعطاف‌پذیری بسیار بالایی به شیء می‌دهد، زیرا شما می‌توانید در هر زمان، attributeهای جدیدی به شیء اضافه کنید. اما این انعطاف‌پذیری هزینه دارد: دیکشنری‌ها حافظه‌ی زیادی مصرف می‌کنند، به خصوص زمانی که تعداد اشیاء بسیار زیاد باشد (میلیون‌ها شیء).

با استفاده از __slots__، به پایتون می‌گویید که attributeهای این کلاس ثابت هستند و تعداد آنها تغییر نمی‌کند. در نتیجه، پایتون به جای استفاده از یک دیکشنری پرمصرف، آنها را در یک ساختار حافظه‌ی فشرده‌تر و کارآمدتر ذخیره می‌کند.

class PointNormal:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class PointSlots:
    __slots__ = ("x", "y")       # attributeها از قبل اعلام می‌شوند
    def __init__(self, x, y):
        self.x = x
        self.y = y

import sys
p1 = PointNormal(1, 2)
p2 = PointSlots(1, 2)
print(hasattr(p1, "__dict__"))   # True — یک دیکشنری دارد
print(hasattr(p2, "__dict__"))   # False — ندارد، ساختار فشرده‌تری دارد

در ساخت میلیون‌ها شیء، استفاده از __slots__ می‌تواند مصرف حافظه را به میزان قابل توجهی (اغلب بین ۳۰ تا ۵۰ درصد) کاهش دهد.

تأثیر بر دیکشنری و محدودیت‌ها

هنگامی که از __slots__ استفاده می‌کنید، شیء دیگر __dict__ نخواهد داشت. یک نتیجه‌ی مهم این است که نمی‌توانید attribute جدیدی که در __slots__ اعلام نشده است، به شیء اضافه کنید:

p = PointSlots(1, 2)
# p.z = 3      # AttributeError: 'PointSlots' object has no attribute 'z'

این محدودیت، انعطاف‌پذیری کد شما را کاهش می‌دهد.

چه زمانی از __slots__ استفاده نکنیم؟

استفاده از __slots__ با معایبی همراه است که باید آنها را در نظر بگیرید:

  • انعطاف‌پذیری را از بین می‌برد: نمی‌توانید به راحتی attributeهای پویا به اشیاء اضافه کنید.
  • با ارث‌بری چندگانه پیچیده می‌شود: ترکیب __slots__ در کلاس‌هایی با ارث‌بری چندگانه نیازمند دقت بیشتری است. همچنین این ویژگی به ارث برده نمی‌شود.
  • با برخی ابزارها ناسازگار است: کتابخانه‌هایی که به __dict__ اشیاء تکیه می‌کنند، ممکن است با کلاس‌های __slots__ به درستی کار نکنند.

بنابراین، __slots__ را فقط در شرایط خاصی به کار ببرید: زمانی که تعداد اشیاء بسیار زیاد است، attributeها ثابت هستند، و حافظه به عنوان یک گلوگاه واقعی شناسایی شده است. برای کلاس‌های معمولی با تعداد نمونه‌ی کم، استفاده از __slots__ یک بهینه‌سازی زودهنگام است که تنها انعطاف‌پذیری را بدون نیاز، محدود می‌کند. انتخاب بین حافظه و انعطاف‌پذیری، یک مصالحه‌ی آگاهانه است.

__slots__ و property با هم

می‌توانید از __slots__ و property به طور همزمان استفاده کنید، اما باید دقت داشته باشید که نام property نباید در __slots__ قرار گیرد (در غیر این صورت با خطا مواجه می‌شوید). روش معمول این است که یک slot با یک نام داخلی (مثلاً با زیرخط) تعریف کنید و property روی آن قرار دهید:

class Temperature:
    __slots__ = ("_celsius",)        # slot با نام داخلی

    def __init__(self, c):
        self._celsius = c

    @property
    def celsius(self):
        return self._celsius

weakref

weakref چیست؟

یک Weak Reference به یک شیء اشاره می‌کند، اما شمارنده‌ی Reference آن شیء را افزایش نمی‌دهد. این یعنی اگر تنها Referenceهای باقی‌مانده به یک شیء، از نوع Weak باشند، آن شیء می‌تواند آزاد شود و حافظه‌اش بازیابی گردد.

import weakref

class Data:
    def __init__(self, value):
        self.value = value

d = Data(42)
weak = weakref.ref(d)          # یک Weak Reference ساخته شد
print(weak())                  # <__main__.Data ...> — شیء هنوز زنده است
del d                          # تنها Reference قوی حذف شد
print(weak())                  # None — شیء آزاد شد

کاربردها: Cache و Observer

weakref دو کاربرد کلیدی در برنامه‌نویسی دارد که هر دو به جلوگیری از Memory Leak کمک می‌کنند.

Cache

می‌خواهید اشیاء را برای دسترسی سریع‌تر در یک Cache نگه دارید. اما اگر Cache Referenceهای قوی را نگه دارد، مانع از آزاد شدن آن اشیاء می‌شود و حافظه به مرور پر می‌شود. با WeakValueDictionary، این مشکل حل می‌شود:

import weakref

class ImageCache:
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()   # مقادیر Weak

    def get(self, key):
        return self._cache.get(key)

    def put(self, key, image):
        self._cache[key] = image

با استفاده از WeakValueDictionary، به محض اینکه هیچ Reference قوی دیگری به یک تصویر وجود نداشته باشد، آن تصویر به طور خودکار از Cache حذف می‌شود. این کار از Memory Leak جلوگیری می‌کند.

الگوی Observer

در الگوی Observer، یک Observer به یک Subject گوش می‌دهد. اگر Subject Referenceهای قوی به Observerهای خود نگه دارد، ممکن است مانع از آزاد شدن آنها شود. استفاده از Weak Reference در این ساختار، از این مشکل جلوگیری می‌کند.

weakref چگونه از Memory Leak جلوگیری می‌کند؟

Memory Leak اغلب زمانی رخ می‌دهد که یک ساختار داده (مانند Cache یا لیست Observerها) Referenceهای قوی را برای مدت طولانی نگه می‌دارد و مانع از آزاد شدن اشیائی می‌شود که دیگر به آنها نیازی نیست. با استفاده از Weak Reference، آن اشیاء به محض اینکه در جای دیگری از برنامه بی‌استفاده شوند، آزاد می‌شوند. این کار، حافظه را به طور مؤثر مدیریت می‌کند و از انباشته شدن اشیاء بی‌استفاده جلوگیری می‌نماید.

رابطه‌ی __slots__ و weakref

یک نکته‌ی ظریف: اگر از __slots__ استفاده می‌کنید، شیء به طور پیش‌فرض قابلیت weakref شدن را ندارد (چون slot مربوط به Weak Reference را ندارد). برای اینکه هم __slots__ و هم weakref را داشته باشید، باید '__weakref__' را صراحتاً به لیست __slots__ اضافه کنید:

class Node:
    __slots__ = ("value", "__weakref__")     # '__weakref__' ضروری است

    def __init__(self, value):
        self.value = value

n = Node(1)
import weakref
w = weakref.ref(n)      # اکنون کار می‌کند
print(w().value)        # 1

بدون وجود '__weakref__' در __slots__، هر تلاش برای ساختن weakref از آن شیء با خطا مواجه می‌شود.

کپی کردن اشیاء

Shallow Copy در برابر Deep Copy

وقتی نیاز به کپی کردن یک شیء دارید، دو نوع کپی در پایتون وجود دارد که تفاوت اساسی با هم دارند:

  • Shallow Copy: یک شیء جدید می‌سازد، اما attributeهای درونی آن شیء را کپی نمی‌کند. در عوض، Referenceها به همان اشیای اصلی را در شیء جدید کپی می‌کند.
  • Deep Copy: یک شیء جدید می‌سازد و سپس به صورت بازگشتی تمام attributeهای درونی را نیز کپی می‌کند. نتیجه، یک شیء کاملاً مستقل از شیء اصلی است.
import copy

class Order:
    def __init__(self, items):
        self.items = items

original = Order(["Book", "pen"])

shallow = copy.copy(original)
shallow.items.append("notebook")
print(original.items)      # ['Book', 'pen', 'notebook'] — لیست بین آنها مشترک است!

original2 = Order(["Book", "pen"])
deep = copy.deepcopy(original2)
deep.items.append("notebook")
print(original2.items)     # ['Book', 'pen'] — کاملاً مستقل، تغییری نکرده است

همان‌طور که در مثال می‌بینید، Shallow Copy لیست items را کپی نکرد؛ فقط Reference به آن را کپی کرد. در نتیجه، تغییر در کپی بر روی شیء اصلی نیز تأثیر گذاشت. اما Deep Copy، یک لیست کاملاً جدید و مستقل ساخت.

سفارشی‌سازی copy و deepcopy

می‌توانید رفتار کپی را برای کلاس خودتان با پیاده‌سازی متدهای __copy__ و __deepcopy__ سفارشی کنید:

import copy

class Connection:
    def __init__(self, host, cache):
        self.host = host
        self.cache = cache

    def __copy__(self):
        # سفارشی‌سازی Shallow Copy
        return Connection(self.host, self.cache)

    def __deepcopy__(self, memo):
        # سفارشی‌سازی Deep Copy
        # 'memo' برای جلوگیری از کپی بی‌نهایت در Referenceهای حلقوی استفاده می‌شود
        return Connection(self.host, copy.deepcopy(self.cache, memo))

چرا به این سفارشی‌سازی نیاز داریم؟ گاهی برخی attributeها (مانند یک اتصال شبکه باز) را نباید کپی کرد، یا فرآیند کپی نیازمند منطق خاصی است.

مزیت اشیاء Immutable در کپی

اشیاء Immutable (مانند tuple، frozenset، یا کلاس‌هایی که خودتان به صورت Immutable طراحی کرده‌اید) یک مزیت بزرگ دارند: نیازی به کپی شدن ندارند. چون هیچ‌گاه تغییر نمی‌کنند، اشتراک‌گذاری Reference بین آنها کاملاً امن است. این یکی از دلایل محبوبیت طراحی اشیاء Immutable است. آنها هم کد را ساده‌تر می‌کنند و هم در محیط‌های multi-thread امن‌تر هستند.

Serialization (مرور از منظر حافظه)

Serialization (تبدیل اشیاء به قالبی قابل ذخیره‌سازی یا انتقال) را در فصل یازدهم به طور کامل بررسی کردیم. در اینجا، صرفاً از منظر حافظه و وضعیت شیء به آن نگاه می‌کنیم.

  • json: به فرمت متنی استاندارد. امن است و بین زبان‌های مختلف قابل استفاده است، اما فقط با انواع ساده‌ی داده (dict، list، str، int، float، bool، None) کار می‌کند.
  • pickle: به فرمت بایت باینری مخصوص پایتون. می‌تواند هر شیء پایتونی (حتی توابع و کلاس‌ها) را serialize کند. اما ناامن است و هرگز نباید داده‌های pickle را از منابع نامطمئن بارگذاری کرد.

متدهای __getstate__ و __setstate__

متدهای __getstate__ و __setstate__ (که در فصل یازدهم دیدیم) دقیقاً به «وضعیت شیء در حافظه» (Heap) مربوط می‌شوند. آنها مشخص می‌کنند که چه بخش‌هایی از وضعیت یک شیء باید ذخیره یا بازیابی شوند.

__getstate__

این متد قبل از سریالسازی فراخوانی می‌شود. شما باید یک شیء (معمولاً یک دیکشنری) برگردانید که نشان‌دهنده‌ی وضعیت قابل ذخیره‌سازی شیء باشد.

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getstate__(self):
        # فقط name را ذخیره کن، age را نه
        return {"name": self.name}

__setstate__

این متد هنگام بازسازی (Deserialization) فراخوانی می‌شود. داده‌هایی که توسط __getstate__ برگردانده شده‌اند، به این متد پاس داده می‌شوند و شما باید وضعیت شیء را بر اساس آنها بازسازی کنید.

class User:
    # ...
    def __setstate__(self, state):
        self.name = state["name"]
        self.age = 0  # مقدار پیش‌فرض برای age

ارتباط با حافظه (Heap)

اشیاء در حافظه (Heap) شامل همه‌ی داده‌هایشان هستند. اما همه‌ی این داده‌ها قابل ذخیره‌سازی یا انتقال نیستند. مثلاً:

  • یک اتصال به پایگاه داده (database connection) را نمی‌توانید ذخیره کنید، چون وقتی برنامه را دوباره اجرا کنید، آن اتصال معتبر نخواهد بود.
  • یک فایل باز را نمی‌توانید سریال کنید، چون وقتی دوباره بازش کنید، ممکن است موقعیت فایل تغییر کرده باشد.
  • یک صفت محاسبه‌شده (مثلاً سن که از تاریخ تولد محاسبه می‌شود) را نباید ذخیره کنید، چون تاریخ امروز تغییر کرده است.

اینجاست که __getstate__ و __setstate__ وارد عمل می‌شوند. آنها مرز بین «داده‌های زنده در حافظه» و «داده‌های قابل ذخیره‌سازی» را مشخص می‌کنند.

یک مثال عملی

فرض کنید کلاسی برای مدیریت اتصال به پایگاه داده داریم:

import pickle

class DatabaseConnection:
    def __init__(self, host, user, password):
        self.host = host
        self.user = user
        self.password = password
        self.connection = self._connect()  # یک اتصال واقعی

    def _connect(self):
        # شبیه‌سازی اتصال به دیتابیس
        return f"Connection to {self.host} with user {self.user}"

    def __getstate__(self):
        # داده‌هایی که می‌خواهیم ذخیره کنیم
        state = self.__dict__.copy()
        # اتصال زنده را از وضعیت حذف می‌کنیم
        del state["connection"]
        return state

    def __setstate__(self, state):
        # داده‌های ذخیره‌شده را بازیابی می‌کنیم
        self.__dict__.update(state)
        # اتصال جدیدی می‌سازیم
        self.connection = self._connect()

db = DatabaseConnection("localhost", "admin", "1234")
print(db.connection)  # Connection to localhost with user admin

# سریالسازی
data = pickle.dumps(db)

# شبیه‌سازی بستن برنامه و باز کردن دوباره
del db

# بازسازی
new_db = pickle.loads(data)
print(new_db.connection)  # Connection to localhost with user admin (اتصال جدید)

در این مثال:

  • وضعیت در حافظه (قبل از سریالسازی): شامل host، user، password و connection (یک اتصال زنده) است.
  • وضعیت ذخیره‌شده (توسط __getstate__): فقط شامل host، user و password است. connection حذف شده چون قابل ذخیره‌سازی نیست.
  • وضعیت بازسازی‌شده (توسط __setstate__): دوباره host، user و password را بازیابی می‌کند و یک connection جدید می‌سازد.

جمع‌بندی نهایی: متدهای __getstate__ و __setstate__ به شما اجازه می‌دهند که:

  1. تصمیم بگیرید چه بخش‌هایی از شیء را ذخیره کنید (__getstate__). این کار به دلیل ماهیت حافظه (Heap) ضروری است، چون برخی داده‌ها (مثل اتصالات شبکه) در حافظه معنا دارند اما قابل ذخیره‌سازی نیستند.
  2. تصمیم بگیرید چگونه شیء را بازسازی کنید (__setstate__). شما می‌توانید داده‌های ذخیره‌شده را گرفته و یک شیء جدید با وضعیت مشابه (اما با اتصالات جدید) بسازید.

خلاصه

مدیریت حافظه در پایتون به کمک دو مکانیزم اصلی انجام می‌شود: Reference Counting (که فوری است اما حلقه‌ها را نمی‌گیرد) و Garbage Collector (که دوره‌ای است و حلقه‌های Reference را پاک‌سازی می‌کند).

ابزارهای بهینه‌سازی مهمی مانند __slots__ (برای کاهش مصرف حافظه به بهای از دست دادن انعطاف)، weakref (برای ایجاد Referenceهای غیرمالکیتی و جلوگیری از Memory Leak)، و الگوی Flyweight (برای بازاستفاده از اشیاء به جای ساخت مجدد) در اختیار شما هستند.

مفهومنکته‌ی کلیدی
Heap و Referenceمتغیرها به شیء اشاره می‌کنند، خود شیء را نگه نمی‌دارند
پیش‌فرض Mutableهرگز def f(x=[]) ننویسید؛ از None استفاده کنید
Reference Counting + GCReference Counting فوری است، GC حلقه‌های Reference را می‌گیرد
__del__نامطمئن؛ به جای آن از Context Manager استفاده کنید
__slots__حافظه‌ی کمتر، انعطاف کمتر
weakrefWeak Reference؛ Cache و Observer بدون Memory Leak
Shallow/Deep Copyاشتراک Reference در برابر کپی مستقل
اشیاء Immutableنیازی به کپی ندارند و در محیط‌های multi-thread امن‌ترند

نکته‌ی طلایی: بیشتر ابزارهایی که در این فصل بررسی کردیم (__slots__، weakref، Flyweight) ابزارهای بهینه‌سازی هستند. بهینه‌سازی زودهنگام، ریشه‌ی بسیاری از مشکلات در کدهای برنامه‌نویسی است. ابتدا کد درست، خوانا و قابل نگهداری بنویسید. تنها پس از اینکه با اندازه‌گیری دقیق متوجه شدید که حافظه واقعاً یک گلوگاه است، به سراغ این ابزارها بروید. اما دام‌های حافظه‌ای که در این فصل معرفی شدند (مانند مقدار پیش‌فرض Mutable) را باید از همان ابتدا بشناسید، زیرا آنها باگ هستند، نه بحث بهینه‌سازی.

پرسش‌های مصاحبه

مدیریت حافظه در CPython را توضیح دهید.

پاسخ خوب، دو مکانیزم اصلی را شرح می‌دهد: Reference Counting که هر شیء یک شمارنده دارد. وقتی شمارنده به صفر برسد، شیء بلافاصله آزاد می‌شود. و Generational Garbage Collector که برای شناسایی و پاک‌سازی حلقه‌های Reference که Reference Counting نمی‌تواند آنها را بگیرد، به کار می‌رود. اشاره به این که GC فقط برای حلقه‌هاست و نه همه‌ی اشیاء، نشان‌دهنده‌ی درک عمیق شماست.

__slots__ چه می‌دهد و چه می‌گیرد؟

می‌دهد: حذف __dict__ هر نمونه و در نتیجه، کاهش چشمگیر مصرف حافظه و دسترسی کمی سریع‌تر به attributeها. این ویژگی برای میلیون‌ها شیء کوچک بسیار تعیین‌کننده است. می‌گیرد: امکان افزودن attributeهای پویا، سازگاری ساده با ارث‌بری چندگانه، و به طور پیش‌فرض قابلیت weakref. تأکید بر این که __slots__ یک ابزار بهینه‌سازی برای شرایط خاص است، نه پیش‌فرض هر کلاس، نشان‌دهنده‌ی درک صحیح شماست.

weakref چه کاربردی دارد و کجا به کار می‌آید؟

کاربرد اصلی در Cacheها و الگوی Observer است. در Cacheها، می‌خواهید اشیاء را نگه دارید اما نمی‌خواهید مانع آزاد شدن آنها شوید. WeakValueDictionary این امکان را فراهم می‌کند. در Observer، یک Observer نباید با Reference قوی خود، مانع آزاد شدن Subject شود. مثال کلاسیک: یک registry که با Reference قوی، اشیاء را برای همیشه زنده نگه می‌دارد و باعث Memory Leak می‌شود. WeakValueDictionary و WeakMethod راه‌حل‌های مناسبی برای این مشکلات هستند.

تفاوت copy و deepcopy چیست؟ هرکدام را چه زمانی استفاده می‌کنید؟

copy (Shallow Copy) فقط یک شیء جدید در سطح اول می‌سازد، اما attributeهای درونی آن با شیء اصلی مشترک باقی می‌مانند. deepcopy (Deep Copy) تمام سطوح را به صورت بازگشتی کپی می‌کند و یک شیء کاملاً مستقل می‌سازد. deepcopy هزینه‌ی محاسباتی و حافظه‌ی بیشتری دارد. یک پاسخ ممتاز، این است که ابتدا بپرسد «آیا اصلاً به کپی نیاز داریم؟» چون طراحی با اشیاء Immutable، اغلب نیاز به کپی کردن را از بین می‌برد.

در فصل بعد: حالا که می‌دانیم اشیاء در حافظه چگونه زندگی می‌کنند، یک لایه پایین‌تر می‌رویم و به زیر کاپوت خود attributeها نگاه می‌کنیم. با Descriptorها آشنا می‌شوید؛ سازوکاری قدرتمند که @property، @classmethod و حتی فیلدهای ORMها بر روی آن ساخته شده‌اند.

Please login to bookmark Close
نظرات

دیدگاهتان را بنویسید

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم