python-logo

למה אסור לכם לבצע import ישירות לפונקציות או אובייקטים

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

import module
module.func('yummy')

השיטה השניה היא ביצוע import ישירות לאובייקט או פונקצייה אותם אנו צריכים:

from module import func
func('meh')

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

כתיבת קוד בדיק

נניח שהקוד שלנו מכיל את הפונקציה הבאה, המבצעת חישוב כלשהו, ואם נזרק exception מחכה 10 שניות.

from time import sleep

def process_or_wait(value):
    try:
        # process value
        return True
    except:
        sleep(10)
        return False

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

import my_module
import unittest

class Test1(unittest.TestCase):
    def test_process_or_wait__failure(self):
        ret_val = my_module.process_or_wait('invalid')
        self.assertFalse(ret_val)

הבדיקה אכן עובדת ומוודאת שעבור ערך לא תקין, הפונקציה שלנו מחזירה False. אז מה בכל זאת הבעיה? הבעיה היא שבמקרה של כישלון, הפונקציה process_or_wait מחכה 10 שניות, כלומר הבדיקה תימשך 10 שניות!

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

import my_module
import unittest
from mock import patch

class Test1(unittest.TestCase):
    @patch('time.sleep', side_effect = lambda _ : None)
    def test_process_or_wait_failure(self, sleep_mock):
        ret_val = my_module.process_or_wait('invalid')
        self.assertFalse(ret_val)
        sleep_mock.assert_called_with(10)

נריץ את קוד הבדיקה ו… הוא לא עובד! הבדיקה עדיין אורכת 10 שניות, ובנוסף השורה האחרונה, המוודאת שפונקציית הדמה שלנו נקראה, נכשלת. הסיבה לכך נעוצה בצורה בה ביצענו import לפונקציה sleep.

המשמעות של import

בכל פעולת import משתתפים שני מודולים: המודול המייבא – זה שמכיל את פקודת ה-import, והמודול המיובא. כאשר מבצעים import ישירות לפונקציה:

from time import sleep

נוצר במודול המייבא משתנה מקומי בשם sleep המפנה (reference) לפונקציה sleep במודול time. למעשה אין קשר בין השם sleep במודול המייבא, לשם sleep במודול time (המיובא), ואכן שפת פייתון מאפשרת לנו לבחור שרירותית את השם של המשתנה המקומי במודול המייבא:

from time import sleep as zzz
print zzz # <built-in function sleep>

עכשיו נבין מה קרה בקוד הבדיקה. הפונקציה patch אכן החליפה את הפונקציה sleep במודול time המקורי, אבל היא לא החליפה את העותק המקומי של הפונקציה במודול המייבא mymodule. זאת הסיבה שקוד הבדיקה לא עבד.

אך אם היינו כותבים את הקוד הנבדק באופן הבא, הבדיקה הייתה עובדת:

import time

def process_or_wait(value):
    try:
        # process value
        return True
    except:
        time.sleep(10)
        return False

זאת כי כעת המודול המייבא מחזיק reference למודול time עצמו, ולא ישירות לפונקצייה sleep. וכאשר הקוד ניגש ל-time.sleep, הוא למעשה ניגש למודול time, ומביא את ה-attribute בשם sleep כל פעם מחדש. מכיוון שבפייתון כל מודול נטען לזיכרון פעם אחת בלבד, קוד הבדיקה שלנו יחליף בדיוק את אותה הפנייה לפונקציה sleep שהקוד הנבדק שלנו משתמש בה.

המסקנה היא שכדי לוודא שהקוד שלנו יהיה בדיק, אסור (אוקיי, לא מומלץ) לבצע import לפונקצייה ישירות.

וכמובן שמסקנה זאת תקפה לגבי כל* הפונקציות והאובייקטים.

* ובכן, כמעט עבור כל הפונקציות. ישנן פונקציות מובנות אותן לא ניתן לדרוס ישירות, למשל datetime.datetime.utcnow. במקרה זה אחד הפתרונות הוא אכן לדרוס את העותק המקומי של המודול המייבא.


גילוי נאות: למעשה, יכלנו להסתדר גם עם הקוד המקורי ע"י כתיבת קוד הבדיקה כך שיחליף את העותק המקומי של הפונקציה sleep:

@patch('mymodule.sleep', side_effect = lambda _ : None)

אמנם זהו קוד תקין לחלוטין, אך לא מומלץ מכיוון שהוא מניח דברים לגבי המימוש הפנימי של mymodule (במקרה שלנו – שהוא מבצע import לפונקציה sleep ישירות), ולא מתייחס אליו בתור קופסה שחורה. הנחות מסוג זה הופכות את קוד הבדיקה לשביר, מכיוון ששינויים באופן המימוש של המודול הנבדק, אפילו אם לא השפיעו על התנהגותו, עלולים לגרום לקוד הבדיקה לא לעבוד. מסיבה זאת אני מעדיף שלא להשתמש ב-patch באופן זה.

2 תגובות בנושא “למה אסור לכם לבצע import ישירות לפונקציות או אובייקטים”

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים