מבוא ל-Debugging ועבודה עם ipdb
פוסט זה הוא מבוא לאחת הפעולות ההכי חשובות בתכנות - debugging. נראה כיצד לדבג תוכנית בסיסית ב-python, ולצורך כך ניעזר ב-ipdb (ראשי תיבות של Interactive Python Debugger). נראה בהתחלה כיצד להתקין את ipdb, נראה שימוש בסיסי ולאחר מכן דוגמה מפורטת למציאת באג בקוד רקורסיבי.
מה זה בכלל Debugging?
Debugging היא פעולה של מציאת בעיות (באגים) בקוד, כלומר דברים שגורמים לקוד לא לעבוד כמו שצריך. לדעתי זוהי פעולה מאוד בסיסית שכל מתכנת חייב לשלוט בה היטב. באופן כללי, תמיד יש באגים בקוד שאנחנו כותבים, אבל אחד מהפרמטרים שמבדלים בין מתכנת גרוע למתכנת מעולה הוא היכולת להתמודד עם הבאגים שנוצרים והיכולת לפתור אותם.
התקנה + שימוש בסיסי
התקנה בעזרת pip
את ההתקנה נבצע בעזרת pip - מנהל חבילות נפוץ של Python.
pip install ipdb
שימוש בסיסי
על מנת להשתמש ב-debugger, צריך פשוט להדביק את השורה הבאה בנקודה בקוד שבה אנחנו רוצים להתחיל לדבג את הקוד:
import ipdb;ipdb.set_trace()
לאחר מכן מריצים את הקוד כרגיל דרך ה-terminal והוא יעצר בנקודה שבה הוספנו את השורה.
למשל, פה הדבקתי קוד זה בשורה 3 של הקובץ example.py
, והרצתי אותו דרך ה-terminal. כעת הקוד נעצר, ונוכל לנתח מה קורה כרגע בקוד. בדוגמה הבאה נראה מה ניתן לעשות בקטע הקוד שבו עצרנו.
$ python /tmp/example.py
> /tmp/example.py(4)<module>()
3 import ipdb;ipdb.set_trace()
----> 4 print (a + b)
5
ipdb>
כעת בעצם נפתח לנו shell שאפשר להריץ בעזרתו פקודות. ישנן פקודות רבות, אלו הן הבסיסיות והשימושיות ביותר:
-
n - מריץ את השורה הבאה (זו שמסומנת בעזרת
---->
) -
s - נכנס לתוך השורה הבאה (זו שמסומנת בעזרת
---->
). ההבדל בין s לבין n הוא שאם יש פונקצייה, n מריץ את כל הפונקצייה ו-s נכנס לתוך הפונקצייה ואז ניתן להריץ כל שורה שם בנפרד. -
ll - מדפיס את הכמה שורות הקרובות סביב הסמן.
-
b - הוספת נקודת עצירה (break point), כלומר מקום בו הקוד יעצור. ניתן להוסיף נקודות עצירה באופן הבא:
b sample-filename.py:<line no> b <function> b <line_number>
-
c - להמשיך להריץ את התוכנית עד להגעה לנקודת עצירה.
-
a - מדפיס את כל הארגומנטים שהפונקצייה הנוכחית קיבלה
-
r - ממשיך את ריצת התוכנית עד לסוף הפונקצייה.
בנוסף, מדובר בסוג של interpreter פייטוני, לכן להריץ קל פקודה שהיא תקינה ב-python. אפשר להדפיס משתנים ולקרוא לפונקציות, בדיוק כמו בעבודה רגילה עם interpreter.
דוגמה - מציאת באג בסדרת פיבונאצ’י
נראה את השימוש ב-ipdb בעזרת קטע הקוד הבא שמדפיס את 20 המספרים הראשונים בסדרת פיבונאצ’י. ממליץ בחום למי שלא יצא לו לפגוש פונקצייה זו בעבר לקרוא עליה קצת בוויקפדיה לפני המשך קריאת הפוסט.
שימו לב שבקוד הבא ישנו באג קטן, ובמהלך הדוגמה ננסה למצוא אותו. מוזמנים לנסות לחפש אותו בעין על ידי הסתכלות על הקוד.
def fibonacci(num):
if num <= 1:
return num
num1 = fibonacci(num - 1)
num2 = fibonacci(num - 3)
return num1 + num2
# Print first 20 Fibonacci numbers
for i in range(0, 20):
print(fibonacci(i), end=" ")
print("\n")
מצאתם את הבאג? יופי. אם לא, אל דאגה נמצא אותו ביחד בדקות הקרובות.
נוודא מהם המספרים הראשונים שצריכים להיות מוצגים (הכי קל לבדוק בערך בוויקפידיה). נצפה כי המספר הראשון יהיה 0, ואז 1, ואז 2, ואז 3 ואז 5…..
על ידי הרצה פשוטה ניתן לראות כי התוכנית לא עובדת כמצופה:
רגע, למה לא פשוט להוסיף הדפסות?
להוסיף הדפסות זאת אפשרות טובה, אבל מכיוון שמדובר בקוד רקורסיבי יהיו המון הדפסות למסך ויהיה מאוד קשה להבין מה קורה. למשל, ניסתי להוסיף הדפסה של המספר הנוכחי בתחילת הפונקצייה fibonacci
אך מכיוון שפונקצייה זו נקראת המון פעמים, קשה מאוד להבין מה קורה.
def fibonacci(num):
print(num, end=" ")
if num <= 1:
return num
# Rest of the code ....
הפלט מאוד ארוך ומסורבל, וזה חלק ממנו:
קשה מאוד להבין מהפלט הזה מה הפונקצייה עושה, לכן ננסה להיעזר ב-Debugger כדי לנתח בצורה טובה יותר מה קורה בקוד.
עבודה עם ipdb
ראשית נוסיף את הקריאה ל-ipdb. לדעתי הכי נוח יהיה להוסיף זאת ישר לפני הקריאה לפונקצייה הרקורסיבית fibonacci
. (בשורה 12 בקטע הקוד שלמטה)
def fibonacci(num):
if num <= 1:
return num
num1 = fibonacci(num - 1)
num2 = fibonacci(num - 3)
return num1 + num2
# Print first 20 Fibonacci numbers
for i in range(0, 20):
# Start debugging!
import ipdb;ipdb.set_trace()
print(fibonacci(i), end=" ")
print("\n")
כעת נריץ את הקוד ונוכול לראות כי אנחנו נעצור בשורה 12.
על ידי הרצת הפקודה s
אנחנו נבצע step_into, כלומר ניכנס לתוך הפונקצייה הרקורסיבית.
כעת, מכיוון שמדובר ב-interpreter פייטוני, אפשר לוודא שהערך של num
הוא אכן 0 כמו שאנחנו מצפים. זאת הפעם הראשונה שאנחנו קוראים לפונקצייה, וזה הערך שהעברנו.
מכיוון ש-num
קטן מ-1, אנחנו מצפים שהקוד פשוט יחזיר את num
. נתקדם לשורה הבאה בעזרת הפקודה n
. ניתן להריץ פקודה זה מספר פעםמים נוספות עד שנצא מהפונקצייה.
המספר הראשון התקבל באופן תקין, כעת נעבור למספר הבא.
אנו מצפים כי המספר הבא יהיה 1. נבצע את אותם השלבים בדיוק כמו קודם, ונראה כי אכן מתקבל המספר המתאים:
גם המספר השני התקבל באופן תקין, כעת נעבור למספר הבא.
נתקדם שתי שורות בעזרת הפקודה n
ולאחר מכן ניכנס לתוך הפונצייה הרקורסיבית שלנו בעזרת s
. נתקדם מספר שורות בעזרת n
ונגיע לשורה 5 בקוד, ששם מתחילות הקריאות הרקורסיביות האמיתיות.
בנקודה הזאת כדאי לעצור ולחשוב למה אנחנו מצפים. ניתן להיכנס לתוך הפונקצייה (בעזרת s
כמובן) ולראות שורה-שורה מה היא עושה, אך ניתן גם לנסות להבין מה אנחנו רוצים שהקוד יעשה. אנחנו יודעים שכל איבר בסדרת פיבונאצ’י הוא הסכום של שני האיברים הקודמים, לכן נצפה ש-num1
ו-num2
יהיו שני המספרים הקודמים, כלומר נרצה שהם יהיו 0 ו-1.
לכן, נבצע את שתי השורות הבאות, ונראה מה הערך של המספרים בפועל.
אנחנו יכולים לראות כי הערך של num2
איננו תקין, משום שהוא -1 ולא 0. כלומר, קיימת בעיה בשורה שבה מאתחלים את num2
, כלומר בשורה 6 של הקוד. כעת, כשהבעיה שלנו מאוד ממוקדת ניתן לראות כי אנחנו בכל פעם הולכים שלושה מספרים אחורה במקום שניים אחורה, כלומר צריך לשנות את שורה 6 להיות:
num2 = fibonacci(num - 2)
עבור בעיה מעט יותר מורכבת, אולי היינו צריכים להריץ את הקוד מחדש, ולהיכנס לתוך הפונקצייה כאשר num=2
בשורה 6 (בעזרת s
כמובן) ולראות מה בדיוק היא עושה, אך כעת לא היה צורך מכיוון שמיקדנו מספיק את הבעיה כדי למצוא אותה.
סיכום
ברור כי הדוגמה הזאת מאוד מפורטת, ואני בטוח שרובכם מצאתם את הבאג בקוד הרבה לפני שהגענו אליו ביחד, אך לדעתי זו כן דוגמה טובה שמראה למה deubgger יכול להיות שימושי. בפוסטים עתידיים אעסוק בפעולות מתקדמות יותר שניתן לבצע בעזרת ה-debugger.