יותר מדי פעמים ובמיוחד בשפות דינמיות אנחנו משתמשים בקוד גנרי מדי בלי להצהיר על הכוונות שלנו - מה שמוביל לבאגים לא צפויים שלפעמים גם קשה למצוא אותם. הסיפור שלנו היום קשור לתרגיל רדוקציית פולימרים. המשימה בתרגיל היא לקבל מחרוזת של אותיות באנגלית ולחפש בה רצפים של אות גדולה ומיד אחריה אותה אות בקטן, או ההיפך, אות קטנה ומיד אחריה אותה אות בגדול - לדוגמה aA, Bb, fF וכן הלאה. כל פעם שמוצאים רצף כזה יש למחוק אותו ואז להמשיך בלולאה לחפש רצפים במחרוזת החדשה שהתקבלה.
בעזרת while וגישה דרך אינדקס למחרוזת הפיתרון עשוי להיראות פשוט:
def reduce_polymer(polymer):
i = 0
while i < len(polymer) - 1:
if reducts_with(polymer[i], polymer[i+1]):
polymer = polymer[0:i] + polymer[i+2:]
i -= 1
continue
i += 1
return polymer
def reducts_with(unit1, unit2):
return abs(ord(unit1) - ord(unit2)) == 32
ובגלל שאני אחד שתמיד טועה באינדקסים הלכתי גם לכתוב תוכנית בדיקה קצרה:
from aoc2018day5 import reduce_polymer
def test_aA():
assert reduce_polymer("aA") == ""
def test_bug():
assert reduce_polymer("aAff") == "ff"
def test_abBA():
assert reduce_polymer("abBA") == ""
def test_abAB():
assert reduce_polymer("abAB") == "abAB"
def test_dabAcCaCBAcCcaDA():
assert reduce_polymer("dabAcCaCBAcCcaDA") == "dabCBAcaDA"
שעברה בלי בעיה.
עכשיו קחו רגע לחזור להסתכל על הפיתרון ונסו למצוא את הבאג.
...
עוד לא מצאתם? שימו לב לאינדקסים
...
הבעיה היא כמובן השורה i -= 1
. אם הרדוקציה מופיעה בתו הראשון במחרוזת, i הופך שלילי ואז הפעם הבאה שנחפש תווים נסתכל על אלמנטים מהסוף וכל הלוגיקה תישבר.
הקוד הבא למשל מכניס את הפונקציה ללולאה אינסופית:
if __name__ == "__main__":
print(len(reduce_polymer("aAaAffaA")))
כשאני מסתכל על קוד כזה מה שמטריד אותי הרבה לפני שאני ניגש לתקן את הבאג זה איך להפוך את הבאג הזה ליותר ברור, איך לשנות את הקוד כך שיהיה ברור שיש שם בעיה הרבה לפני הלולאה האינסופית. וכאן אנחנו מגיעים לדבר על הצהרת כוונות:
בשורת התנאי אנחנו בונים על זה ש i אינו שלילי, אבל לא מוודאים את זה.
בשורת המחיקה אנחנו בונים על זה ש i אינו שלילי, ושוב לא דואגים לספר על זה לאף אחד.
שינוי ראושן של הקוד יקרב אותנו לכתיבה נכונה גם בלי לפתור את הבעיה:
def reduce_polymer(polymer: str) -> str:
i = 0
while i < len(polymer) - 1:
if has_reduction(polymer, i):
polymer = remove_reduction(polymer, i)
i -= 1
continue
i += 1
return polymer
def has_reduction(polymer: str, i: int):
if not 0 <= i < len(polymer) - 1:
raise ValueError(f"Index should be >= 0, got {i}")
unit1 = polymer[i]
unit2 = polymer[i + 1]
return abs(ord(unit1) - ord(unit2)) == 32
def remove_reduction(polymer: str, i: int):
if not 0 <= i < len(polymer) - 1:
raise ValueError(f"Index should be >= 0, got {i}")
return polymer[0:i] + polymer[i+2:]
עכשיו אותה תוכנית בדיקה שקודם עברה בלי בעיה כבר נכשלת עם הודעת שגיאה על האינדקס השלילי - וזה מעולה, כי כשהבדיקה נכשלת אנחנו כבר ב 90% מהפיתרון.
והלקח הכללי יותר מהסיפור? בעבודה עם פונקציות שילוב של Type Hints עם בדיקת קלט בכניסה לפונקציה יכול לעזור לנו למצוא בעיות בקוד הרבה יותר מהר.