מה זה garbage collection בפייתון - ולמה כמעט אף פעם לא צריך לחשוב על זה¶
בואו נדבר בכנות. אם באתם מ-C או שמעתם חברים שלומדים אותה מתלוננים, אתם בטח מכירים את הסיוט של malloc ו-free, ואת השגיאה המפורסמת שקוראים לה memory leak. בפייתון כמעט אף אחד לא מדבר על זה, וזה לא במקרה. פייתון עושה את כל העבודה המלוכלכת הזאת בשבילכם, מאחורי הקלעים, בלי שתצטרכו לבקש.
אז בואו נבין מה בעצם קורה שם, כי גם אם לא צריך לנהל את זה ידנית, כדאי להבין למה התוכנית שלכם לא נגמרת בלי זיכרון אחרי חמש דקות ריצה.
מה הבעיה שבכלל פותרים כאן¶
כל פעם שאתם יוצרים אובייקט בפייתון - רשימה, מחרוזת, מופע של מחלקה - הוא תופס מקום בזיכרון. אם התוכנית הייתה ממשיכה ליצור אובייקטים בלי אף פעם לשחרר אותם, תוך זמן קצר לא היה נשאר זיכרון פנוי במחשב. מישהו צריך לזהות מתי אובייקט כבר לא בשימוש ולפנות את המקום שלו. ב-C המתכנת עושה את זה בעצמו. בפייתון, מנגנון שנקרא garbage collector, אספן הזבל, עושה את זה אוטומטית.
המנגנון העיקרי - ספירת רפרנסים¶
הדרך שבה פייתון עוקב אחרי אובייקטים היא פשוטה יחסית - reference counting, ספירת רפרנסים. לכל אובייקט בזיכרון יש מונה שסופר כמה משתנים או מבנים אחרים מצביעים עליו. ברגע שהמונה מגיע לאפס, אין יותר דרך להגיע לאובייקט הזה מהקוד, ופייתון משחררת אותו מיד.
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # יותר מ-1, כי getrefcount עצמה מוסיפה רפרנס זמני
b = a
print(sys.getrefcount(a)) # עלה, כי גם b מצביע לאותה רשימה
del b
print(sys.getrefcount(a)) # ירד בחזרה
כשאתם עושים b = a שני המשתנים מצביעים לאותה רשימה בזיכרון, אז המונה עולה. כשאתם מוחקים את b או שהוא יוצא מהיקף (scope) - למשל בסוף פונקציה - המונה יורד. זו הסיבה שבפייתון כמעט אף פעם לא צריך לשחרר משתנים ידנית. ברגע שאף אחד לא צריך אותם, הם נעלמים לבד.
הבעיה של מעגלי רפרנסים¶
ספירת רפרנסים מצוינת, אבל יש לה חור אחד - מה קורה כששני אובייקטים מצביעים אחד על השני?
class Node:
def __init__(self):
self.next = None
first = Node()
second = Node()
first.next = second
second.next = first
del first
del second
אחרי שתי שורות ה-del, אין שום משתנה בקוד שמצביע על האובייקטים האלה. אבל המונה של כל אחד מהם עדיין לא אפס, כי הם מצביעים אחד על השני. זה נקרא מעגל רפרנסים (reference cycle), וספירת רפרנסים לבד לא יודעת לפתור את זה - שני האובייקטים "תומכים" זה בזה לנצח, גם שאין דרך אמיתית להגיע אליהם מהתוכנית.
פה נכנס האספן המחזורי¶
בשביל לפתור בדיוק את המקרה הזה, לפייתון יש מנגנון שני, שרץ בנוסף לספירת הרפרנסים - cyclic garbage collector, אספן הזבל המחזורי. הוא רץ מדי פעם ברקע, מחפש קבוצות אובייקטים שמצביעים אחד על השני במעגל, בודק אם יש אליהם גישה מבחוץ למעגל, ואם אין - מפנה את כולם ביחד.
זה קורה אוטומטית, בלי שתצטרכו להזמין אותו, אבל מודול ה-gc נותן לכם גישה ידנית אליו אם אתם סקרנים או צריכים לדבג בעיית זיכרון נדירה:
import gc
gc.collect() # מריץ סבב איסוף באופן ידני
print(gc.get_count()) # מציג כמה אובייקטים ממתינים לבדיקה
ברוב הפרויקטים לא תצטרכו לגעת ב-gc בכלל. הוא שם בשביל מקרי קצה, כמו עבודה עם כמויות ענק של אובייקטים או דיבוג memory leak אמיתי.
אז למה בעצם לדעת את זה¶
כי זה מסביר המון דברים שנראים "קסם" כשמתחילים ללמוד תכנות. למה אין free() בפייתון. למה אתם לא צריכים לשחרר קבצים או חיבורים ידנית ברוב המקרים (למרות שעדיין כדאי להשתמש ב-with). ולמה, בניגוד ל-C, קשה הרבה יותר לגרום לתוכנית פייתון "לדלוף" זיכרון בטעות.
זה גם ההבדל המרכזי בין שפה כמו פייתון לשפה כמו C או C++, שבה ניהול זיכרון ידני הוא חלק מרכזי מהעבודה היומיומית. אם אתם רק מתחילים ורוצים להבין את היסודות האלה בצורה מסודרת, שווה לעבור על קורס התכנות הבסיסי שלנו, שבו בונים את ההבנה הזאת שלב אחרי שלב, בלי לקפוץ ישר לפרטים הטכניים.
קורס התכנות הבסיסי שלנו מתחיל מהיסודות ומגיע גם לנושאים כאלה בקצב שמתאים למי שרק מתחיל.
ואם אתם רוצים לשאול שאלות, להיתקע ביחד עם אנשים אחרים שלומדים, או סתם לשמוע איך אחרים מדבגים בעיות דומות - יש לנו קהילה בדיסקורד.