• בלוג
  • האק בפייתון: תלות מעגלית

האק בפייתון: תלות מעגלית

31/05/2023

תלות מעגלית בין מודולים היא תמיד בעייתית. או שהיא גורמת עכשיו לבאגים או שהיא מוקש שיגרום לבאגים איומים בהמשך. אבל לפעמים אנחנו תקועים עם עיצוב תוכנה מסוים והחלטות שכבר התקבלו בעבר, שגרמו לזה שהצבעה מעגלית היא בלתי נמנעת (לפחות עד הריפקטורינג הגדול הבא).

בפייתון קל מאוד לשבור קוד בגלל תלות מעגלית. הנה לדוגמה שני קבצים שתלויים אחד בשני:

# File: a.py
import b

def hi(n):
    print("Hello from a")
    if n < 10:
        b.hi(n+1)

hi(0)
# File: b.py

import a

def hi(n):
    print("hello from b")
    if n < 10:
        a.hi(n+1)

אם נשמור את קטעי הקוד בקבצים בשמות a.py ו b.py בהתאמה ונריץ python a.py נקבל את השגיאה:

Hello from a
Traceback (most recent call last):
  File "/Users/ynonp/tmp/python/circular/a.py", line 1, in <module>
    import b
  File "/Users/ynonp/tmp/python/circular/b.py", line 1, in <module>
    import a
  File "/Users/ynonp/tmp/python/circular/a.py", line 8, in <module>
    hi(0)
  File "/Users/ynonp/tmp/python/circular/a.py", line 6, in hi
    b.hi(n+1)
    ^^^^
AttributeError: partially initialized module 'b' has no attribute 'hi' (most likely due to a circular import)

הפונקציה hi מהמודול a מתחילה לרוץ, מגלה שהיא צריכה לקרוא לפונקציה hi מהמודול b וזורקת שגיאה כי פונקציה זו עדיין לא נטענה. בעצם פייתון התחיל לטעון את המודול b עוד בתחילת הטעינה של a, אבל אז הוא ראה שם את הפקודה:

import a

והבין שעליו לסיים קודם עם a ורק אז יוכל לחזור לסיים עם b. לכן כשמפעילים את hi מתוך המודול a פייתון עדיין לא סיים לאתחל את b ולא מצא עדיין את הפונקציה hi ממנו.

במבנה כל כך קטן יש מגוון דרכים להתמודד עם התלות המעגלית: אפשר לחבר את שתי הפונקציות לאותו קובץ או להוציא את הקריאה ל hi לקובץ שלישי. אבל במבנים יותר מורכבים לפעמים האופציות האלה לא אפשריות.

האק מרושע שיכול לעזור במצבים כאלה הוא להעביר את ה import לתוך הפונקציה. אחרי השינוי המודול b.py נראה כך:

# File: b.py

def hi(n):
    import a
    print("hello from b")
    if n < 10:
        a.hi(n+1)

הפעלה חוזרת והכל עכשיו עובד:

Hello from a
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a
hello from b
Hello from a

החיסרון המרכזי בטעינת מודול מתוך פונקציה הוא הקריאות - מאוד נוח לנו כשמסתכלים על קובץ להסתכל בחלק העליון שלו ולדעת באיזה קבצים אחרים הוא תלוי. מצד שני אם ממש חייבים להשתמש בתלות מעגלית מבנה כזה יכול לתת פיתרון מהיר עד שיהיה זמן לריפקטור רציני.