مقدمه
در این فصل، به زیر پوست پایتون سفر میکنیم. خواهیم دید که اشیاء در حافظه چگونه نگهداری و آزاد میشوند. یاد میگیریم که __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._celsiusweakref
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__ به شما اجازه میدهند که:
- تصمیم بگیرید چه بخشهایی از شیء را ذخیره کنید (
__getstate__). این کار به دلیل ماهیت حافظه (Heap) ضروری است، چون برخی دادهها (مثل اتصالات شبکه) در حافظه معنا دارند اما قابل ذخیرهسازی نیستند. - تصمیم بگیرید چگونه شیء را بازسازی کنید (
__setstate__). شما میتوانید دادههای ذخیرهشده را گرفته و یک شیء جدید با وضعیت مشابه (اما با اتصالات جدید) بسازید.
خلاصه
مدیریت حافظه در پایتون به کمک دو مکانیزم اصلی انجام میشود: Reference Counting (که فوری است اما حلقهها را نمیگیرد) و Garbage Collector (که دورهای است و حلقههای Reference را پاکسازی میکند).
ابزارهای بهینهسازی مهمی مانند __slots__ (برای کاهش مصرف حافظه به بهای از دست دادن انعطاف)، weakref (برای ایجاد Referenceهای غیرمالکیتی و جلوگیری از Memory Leak)، و الگوی Flyweight (برای بازاستفاده از اشیاء به جای ساخت مجدد) در اختیار شما هستند.
| مفهوم | نکتهی کلیدی |
|---|---|
| Heap و Reference | متغیرها به شیء اشاره میکنند، خود شیء را نگه نمیدارند |
| پیشفرض Mutable | هرگز def f(x=[]) ننویسید؛ از None استفاده کنید |
| Reference Counting + GC | Reference Counting فوری است، GC حلقههای Reference را میگیرد |
__del__ | نامطمئن؛ به جای آن از Context Manager استفاده کنید |
__slots__ | حافظهی کمتر، انعطاف کمتر |
weakref | Weak 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ها بر روی آن ساخته شدهاند.