• בלוג
  • שיתוף מידע בין דקורטורים לקוד חיצוני בפייתון

שיתוף מידע בין דקורטורים לקוד חיצוני בפייתון

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

1. תיאור הבעיה

נכתוב דקורטור בשם counting שמוסיף לפונקציה מונה שמראה כמה פעמים קראו לה:

def counting(f):
    def inner(x: int) -> int:
        inner.call_count += 1
        result = f(x)
        return result

    inner.call_count = 0
    return inner

אפשר להשתמש בדקורטור באופן הבא:

@counting
def twice(x: int) -> int:
    return x * 2

twice(10)
twice(20)
twice(30)

print(f"twice was called {twice.call_count} times")

עכשיו ננסה להשתמש בדקורטור בצורה קצת יותר יצירתית ונראה איך הוא נשבר. התוכנית היא:

@lru_cache()
@counting
def twice(x):
    print(f"twice::{x}")
    return x * 2


twice(10)
twice(10)
twice(10)
twice(10)

print(twice.call_count)

והתוצאה היא הדפסת הערך 0.

2. למה זה שבור

הבעיה כמובן היא הדקורטור הנוסף lru_cache. דקורטור זה משנה את הפונקציה twice כך שאני בעצם מפעיל את הפונקציה ש lru_cache מחזיר, בעוד שהערך של call_count נשמר בפונקציה שהדקורטור counting יצר. למעשה בשורה האחרונה של הקוד הבעיה היא שאין לי איך להגיע לאותו ערך שסופר כמה פעמים קראו לפונקציה.

3. מה אפשר לעשות במקום

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

def counting_v2(counter):
    def decorator(f):
        def inner(x: int) -> int:
            print(inner)
            counter.inc()
            # Before calling the decorated function ...
            result = f(x)
            # After calling the decorated function ...
            # Modify the result value
            return result

        return inner
    return decorator

twice_counter = Counter()
@lru_cache()
@counting_v2(twice_counter)
def twice(x):
    print(f"twice::{x}")
    return x * 2


twice(10)
twice(10)
twice(10)
twice(10)

print(twice_counter.value)

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

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