• בלוג
  • קוד שמתעד את עצמו

קוד שמתעד את עצמו

03/07/2019

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

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

1. איתחול משתנים בלולאה

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

לדוגמא ניקח את הקוד הבא שסופר כמה מספרים יש בין אחד לאלף בהם אף סיפרה לא מופיעה יותר מפעם אחת:

import re

def find_numbers_with_unique_digits_in_range(start, end):
    numbers_with_unique_digits = 0
    for i in range(start, end+1):
        if not re.search(r'(\d).*\1', str(i)):
            numbers_with_unique_digits += 1

    return numbers_with_unique_digits

print(find_numbers_with_unique_digits_in_range(1, 1_000))

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

גישה יותר טובה תשתמש ב List Comprehensions ובפונקציה quantify. הפונקציה quantify חשובה כיוון שהיא רומזת שהמשתנה יכיל את מספר האיברים ברשימה, וה List Comprehension הוא שיבחר רק את המספרים המתאימים:

import re
from itertools import count, islice

def quantify(iterable, pred=bool):
    "Count how many times the predicate is true"
    return sum(map(pred, iterable))

def find_numbers_with_unique_digits_in_range(start, end):
    numbers_with_unique_digits = quantify(
        islice(count(), start, end+1),
        lambda i: not re.search(r'(\d).*\1', str(i))
    )

    return numbers_with_unique_digits

print(find_numbers_with_unique_digits_in_range(1, 1_000))

2. הפרדה בין רמות אבסטרקציה

אנחנו משתפרים אבל עדיין קשה להגיד שהקוד מתעד את עצמו - הבעיה הבאה היא הפרדה בין רמות אבסטרקציה:

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

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

import re
from itertools import count, islice

def quantify(iterable, pred=bool):
    "Count how many times the predicate is true"
    return sum(map(pred, iterable))

def has_duplicate_digit(i):
    return re.search(r'(\d).*\1', str(i))

def find_numbers_with_unique_digits_in_range(start, end):
    numbers_with_unique_digits = quantify(
        islice(count(), start, end+1),
        lambda i: not has_duplicate_digit(i)
    )

    return numbers_with_unique_digits

print(find_numbers_with_unique_digits_in_range(1, 1_000))

3. יציאה מוקדמת מפונקציה

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

import re
from itertools import count, islice

def quantify(iterable, pred=bool):
    "Count how many times the predicate is true"
    return sum(map(pred, iterable))

def has_duplicate_digit(i):
    return re.search(r'(\d).*\1', str(i))

def find_numbers_with_unique_digits_in_range(start: int, end: int):
    if start >= end:
        return 0

    return quantify(
        islice(count(), start, end+1),
        lambda i: not has_duplicate_digit(i)
    )

print(find_numbers_with_unique_digits_in_range(1, 1_000))

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

  1. הפונקציה מקבלת שני מספרים שלמים בהם המספר השני גדול ממש מהראשון.

  2. הפונקציה סופרת כמה מספרים בטווח לא מכילים ספרות כפולות ומחזירה את התוצאה.

  3. הפונקציה מחזירה את המספר שמצאה