מה פייתון עושה באמת

29/08/2019

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

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

def main():
    print("Hello World")

נוסיף את המודול dis לתוכנית ונבקש ממנו לפרק את הפונקציה כך שהתוכנית המלאה תיראה כך:

import dis

def main():
    print("Hello World")

dis.dis(main)

והפלט:

  4           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Hello World')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

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

נראה אחד יותר מורכב:

import dis

def greet(n):
    for i in range(n):
        print("hello world")


dis.dis(greet)

והפלט:

  4           0 SETUP_LOOP              24 (to 26)
              2 LOAD_GLOBAL              0 (range)
              4 LOAD_FAST                0 (n)
              6 CALL_FUNCTION            1
              8 GET_ITER
        >>   10 FOR_ITER                12 (to 24)
             12 STORE_FAST               1 (i)

  5          14 LOAD_GLOBAL              1 (print)
             16 LOAD_CONST               1 ('hello world')
             18 CALL_FUNCTION            1
             20 POP_TOP
             22 JUMP_ABSOLUTE           10
        >>   24 POP_BLOCK
        >>   26 LOAD_CONST               0 (None)
             28 RETURN_VALUE

כאן כבר יש לנו שתי שורות קוד שהופכות ל-15 פקודות מכונה. שימו לב לפקודת ה JUMP_ABSOLUTE ול FOR_ITER שיחד מייצרות את הלולאה ולקריאה לפונקציה range בשביל לקבל את הטווח.

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

import dis

counter = 0

def one():
    global counter
    counter += 1
    print(counter)

dis.dis(one)

הפונקציה מוסיפה 1 למשתנה גלובאלי, אבל רק כשאנחנו מסתכלים עליה ב dis אפשר לראות שהפעולה += בעצם מורכבת ממספר פעולות:

  7           0 LOAD_GLOBAL              0 (counter)
              2 LOAD_CONST               1 (1)
              4 INPLACE_ADD
              6 STORE_GLOBAL             0 (counter)

  8           8 LOAD_GLOBAL              1 (print)
             10 LOAD_GLOBAL              0 (counter)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

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

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