תיעוד

הקדמה

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

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

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

הערות ותיעוד

לפני שנדון לעומק בתיעוד, נדבר מעט על ההבדל שבין הערות בקוד לבין תיעוד בקוד.

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

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

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

הערות בקוד

Block Comments

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

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


In [ ]:
import math


def factorize_prime(number):
    while number % 2 == 0: 
        yield 2
        number = number // 2
          
    # `number` must be odd at this point (we've just factored 2 out).
    # Skip even numbers.  Square root is good upper limit, check
    # https://math.stackexchange.com/a/1039525 for more info.
    divisor = 3
    max_divisor = math.ceil(number ** 0.5)
    while number != 1 and divisor <= max_divisor: 
        if number % divisor == 0:
            yield divisor
            number = number // divisor
        else:
            divisor += 2
              
    # If `number` is a prime, just print `number`.
    # 1 is not a prime, 2 already taken care of.
    if number > 2:
        yield number


print(list(factorize_prime(5)))
print(list(factorize_prime(100)))

בקוד שלמעלה מופיעים שני מקרים של "Block Comment".
מדובר בשורה אחת או יותר של הערה שבאה לפני פיסת קוד, ומטרתה לבאר דברים בקוד.
ה־Block ייכתב באותה רמת הזחה של הקוד שאליו הוא מתייחס, וכל שורה בו תתחיל בתו # שלאחריו יבוא רווח.

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

  • הערה תמיד תסביר למה עשינו משהו מסוים – ובשום אופן לא מה עשינו.
    המתכנת המפורסם ג'ף אטווד אומר: "הקוד יגיד לך מה, ההערות יגידו לך למה".
  • הערות מורכבות ממשפטים שלמים שמתחילים באות גדולה ומסתיימים בנקודה.
  • לעולם לא נשנה שמות של מזהים (משתנים, פונקציות וכו') – גם אם הם מופיעים בתחילת משפט.
  • הערות חייבות להיכתב באנגלית.

Inline Comments

הערה יכולה להיות ממוקמת גם בסוף שורת הקוד:


In [ ]:
print("Hello World")  # This is a comment

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

מתכנתים מתחילים נוטים להסביר מה הקוד עושה, ולשם כך הם משתמשים לעיתים קרובות ב־Inline Comments.
הימנעו מלהסביר מה הקוד שלכם עושה.

דוגמה להערה לא טובה:


In [ ]:
snake_y = snake_y % 10  # Take the remainder from 10

ולעומתה, הערה המתקבלת על הדעת:


In [ ]:
snake_y = snake_y % 10  # Wrap from the bottom if the snake hits the top

הוויכוח על הערות

בעולם התוכנה המקצועי ניטש ויכוח רב שנים על מתי נכון להוסיף הערות לקוד.

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

  • הסבר מילולי על פעולת הקוד.
  • הסבר מדוע קוד מסוים עושה משהו.
  • הסבר מדוע קוד נראה פגום בצורה מסוימת (לא עוקב אחרי מוסכמות, לא יעיל) ולמה יש להשאיר אותו כך.
  • הסבר על החלטות שהתקבלו בנוגע לצורת הקוד שנכתב ולארכיטקטורה שלו.
  • שמירה של קוד לשימוש עתידי (נניח, במקרים של קוד שעוזר לנפות שגיאות).
  • צירוף נתונים נוספים על אודות הקוד – מהיכן הוא לקוח, תנאי הרישיון שלו וכדומה.
  • תיוג, שמטרתו להקל בחיפוש עתידי של בעיות נפוצות בקוד. לדוגמה:
    • # FIXME לציון קטע קוד שצריך לתקן.
    • # TODO ואחריו מלל שמסביר משהו שעדיין צריך לבצע ועוד לא נפתר.
    • # HACK לציון מעקף שנועד לפתור בעיה, פעמים רבות בדרך בעייתית.
  • שחרור קיטור. בקוד המקור של פרויקט הקוד הפתוח Linux, לדוגמה, מופיעה המילה "crap" מעל 150 פעמים, מספר שנמצא במגמת עלייה לאורך השנים.

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

טענותיהם של הנמנים עם אסכולת צמצום ההערות מגוונות יחסית:

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

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

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

מחרוזות תיעוד

ניזכר בפונקציה help, שמטרתה להציג לנו תיעוד בנוגע לערך מסוים בתוכנית:


In [ ]:
quote = "So many books, so little time."
help(quote.upper)

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


In [ ]:
quote = "So many books, so little time."
help(str)

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


In [ ]:
def add(a, b):
    return a + b


help(add)

אז מה עלינו לעשות כדי להוסיף תיעוד?
מתברר שזה לא כזה מסובך. בסך הכול צריך להוסיף משהו שנקרא "מחרוזת תיעוד".

מחרוזות תיעוד של שורה אחת

נוסיף לפונקציה שלנו מחרוזת תיעוד של שורה אחת (One-line Docstring) בצורה הבאה:


In [ ]:
def add(a, b):
    """Return the result of a + b."""
    return a + b

בדוגמה האחרונה הוספנו לשורה הראשונה של גוף הפונקציה מחרוזת תיעוד (Docstring), שמתחילה ומסתיימת ב־3 מירכאות כפולות.
אפשר להשתמש בסוג מירכאות אחר, אך 3 מירכאות זו המוסכמה ואנחנו נדבוק בה.
בין המירכאות תיארנו בקצרה מה הפונקציה עושה.

כעת הפונקציה help תשתף איתנו פעולה, ונוכל לקבל את התיעוד על הפונקציה שכתבנו:


In [ ]:
help(add)

נקודות חשובות בהקשר זה:

  • תיעוד של שורה אחת מיועד עבור מקרים ברורים במיוחד, כמו הפונקציה add שכתבנו.
  • התיעוד ייכתב בשורה אחת, צמוד למירכאות, ללא שורות ריקות לפניו או אחריו.
  • התיעוד ינוסח בצורת פקודה ולא כסיפור ("החזר את התוצאה" ולא "הפונקציה מחזירה...").
    כלל אצבע טוב הוא לשמור על הצורה "עשה X, החזר Y" (באנגלית: Do X, Return Y).
  • תיעוד של שורה אחת לא יכלול את סוג הפרמטרים (a או b, במקרה שלנו). הוא יכול לכלול את הסוג של ערך ההחזרה.

מחרוזות תיעוד מרובות שורות

ניקח לדוגמה פונקציה שמקבלת נתיב ומחזירה את חלקיו:


In [ ]:
def get_parts(path):
    current_part = ""
    for char in self.fullpath:
        if char in r"\/":
            yield current_part
            current_part = ""
        else:
            current_part = current_part + char
    if current_part != "":
        yield current_part

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


In [ ]:
def get_parts(path):
    """Split the path, return each part separately."""
    current_part = ""
    for char in self.fullpath:
        if char in r"\/":
            yield current_part
            current_part = ""
        else:
            current_part = current_part + char
    if current_part != "":
        yield current_part

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


In [ ]:
def get_parts(path):
    """Split the path, return each part separately.

    Each "part" of the path can be defined as a drive, folder, or
    file, separated by a forward slash (/, typically used in Linux/Mac)
    or by a backslash (usually used in Windows).
    
    path -- String that consists of a drive (if applicable), folders
            and files, separated by a forward slash or by a backslash.
    """
    current_part = ""
    for char in self.fullpath:
        if char in r"\/":
            yield current_part
            current_part = ""
        else:
            current_part = current_part + char
    if current_part != "":
        yield current_part

סוג כזה של תיעוד נקרא "מחרוזת תיעוד מרובת שורות" (Multi-line Docstring).
נכתוב אותו כדי לעזור למי שישתמש בפונקציה להבין מה מטרתה, אילו פרמטרים היא מצפה לקבל ואילו ערכים יוחזרו ממנה.

היכן נשתמש בתיעוד מרובה שורות?

  • בראש הקוד – נסכם מה מטרת הקוד, ונסביר בקצרה מה הוא כולל.
  • בראש מחלקה – נסכם את התנהגות המחלקה ונתעד את הפעולה __init__.
  • בפונקציה או בפעולה – נסכם:
    • מה מטרתה.
    • אילו ארגומנטים היא מצפה לקבל.
    • מהם ערכי ההחזרה שלה.
    • אילו שגיאות היא עלולה להחזיר.
    • על מה היא משפיעה חוץ מאשר על המשתנים בפונקציה עצמה (נניח – כתיבה לקובץ).

דוגמה לתיעוד בסיסי


In [ ]:
"""A demonstration of writing well documented Python code.

This snippet demonstrates how a well documented code should look.
Each method and class is documented, and there is also
documentation for the script itself.
"""
import os


class Path:
    """Represent a filesystem path.
    
    It is used to simplify the work with paths across different
    operating systems. The initialization method takes a string and
    populates the full path property along with "parts," which is a
    version of the path after we split it using path separator
    characters.

    Basic Usage:

    >>> Path(r'C:\Yossi').get_drive_letter()
    'C:'
    >>> str(Path(r'C:\Messed/Up/Path\To\file.png'))
    'C:/Messed/Up/Path/To/file.png'
    """

    def __init__(self, path):
        self.fullpath = path
        self.parts = list(self.get_parts())

    def get_parts(self):
        """Split the path, return each part separately.

        Each "part" of the path can be defined as a drive, folder, or
        file, separated by a forward slash (/, typically used in
        Linux/Mac) or by a backslash (usually used in Windows).

        path -- String that consists of a drive (if applicable),
                folders, and files, separated by a forward slash or by
                a backslash.
        """
        current_part = ""
        for char in self.fullpath:
            if char in r"\/":
                yield current_part
                current_part = ""
            else:
                current_part = current_part + char
        if current_part != "":
            yield current_part

    def get_drive_letter(self):
        """Return the drive letter of the path, when applicable."""
        return self.parts[0].rstrip(":")

    def get_dirname(self):
        """Return the full path without the last part."""
        path = "/".join(self.parts[:-1])
        return Path(path)

    def get_basename(self):
        """Return the last part of the path."""
        return self.parts[-1]

    def get_extension(self):
        """Return the extension of the filename.
        
        If there is no extension, return an empty string.
        This does not include the leading period.
        For example: 'txt'
        """
        name = self.get_basename()
        i = name.rfind('.')
        if 0 < i < len(name) - 1:
            return name[i + 1:]
        return ''


    def is_exists(self):
        """Check if the path exists, return boolean value."""
        return os.path.exists(str(self))

    def normalize_path(self):
        """Create a normalized string of the path for printing."""
        normalized = "\\".join(self.parts)
        return normalized.rstrip("\\")

    def info_message(self):
        """Return a long string with essential details about the file.
        
        The string contains:
        - Normalized path
        - Drive letter
        - Dirname
        - Basename
        - File extension (displayed even if not applicable)
        - If the file exists
        
        Should be used to easily print the details about the path.
        """
        return f"""
            Some info about "{self}":
            Drive letter: {self.get_drive_letter()}
            Dirname: {self.get_dirname()}
            Last part of path: {self.get_basename()}
            File extension: {self.get_extension()}
            Is exists?: {self.is_exists()}
        """.strip()

    def __str__(self):
        return self.normalize_path()


EXAMPLES = (
    r"C:\Users\Yam\python.jpg",
    r"C:/Users/Yam/python.jpg",
    r"C:",
    r"C:\\",
    r"C:/",
    r"C:\Users/",
    r"D:/Users/",
    r"C:/Users",
)
for example in EXAMPLES:
    path = Path(example)
    print(path.info_message())
    print()

מאחורי הקלעים

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


In [ ]:
help(quote.upper)

In [ ]:
print(quote.upper.__doc__)

ומה קורה כשניצור פונקציה משלנו?
ננסה ליצור לדוגמה את ידידתנו הוותיקה, הפונקציה add:


In [ ]:
def add(a, b):
    return a + b

כיוון שלא הוספנו לפונקציה תיעוד, התכונה __doc__ תוגדר כ־None:


In [ ]:
print(add.__doc__)

נוסיף תיעוד ונראה את השינוי:


In [ ]:
def add(a, b):
    """Return the result of a + b."""
    return a + b

print(add.__doc__)

מדובר בידע כללי מגניב, אבל כאנשים מתורבתים לעולם לא נבצע השמה ישירה ל־__doc__ ולא ניגש אליה ישירות.

התפתחות התיעוד

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

שפת סימון לתיעוד

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

לצורך כך פותחה בשנת 2002 שפת הסימון reStructuredText (או בקיצור – reST), שמאפשרת להפוך מלל שאנחנו כותבים לטקסט מסוגנן.
מטרתה העיקרית של reST הייתה לאפשר הכנסת טקסט מסוגנן לתיעוד טכני בפייתון, ופייתון אכן אימצה אותה רשמית לצורך זה.
היא מאפשרת לכתוב לא רק תיעוד בקוד, אלא גם תיעוד מילולי כללי על מטרותיו של הפרויקט ועל דרך השימוש בו (כמו ספר תיעוד קטן).

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

מלל שנכתב ב־reStructuredText ייראה כך עבור מי שכתב אותו:

אפשר לראות בטקסט *הזה* טעימה קטנה מהיכולות של **reStructuredText**.
חלק מהאפשרויות הפחות מתוחכמות שלו כוללות:

* הדגשה, מלל מוטה וקו תחתון.
* רשימות.
* סימון של קוד, כמו `print("Hello World")`.

וייראה כך בתוצאה הסופית:

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

  • הדגשה, מלל מוטה וקו תחתון.
  • רשימות.
  • סימון של קוד, כמו print("Hello World").

מנוע ליצירת קובצי תיעוד

בשנת 2008 פותח בקהילת הפייתון כלי בשם Sphinx.
מטרתו לסרוק את התיעוד של פרויקט הקוד שלכם, וליצור ממנו מסמך תיעוד שנעים לקרוא ב־PDF או ב־HTML.
Sphinx, כמובן, תומך במסמכים שנכתבו ב־reStructuredText, והוא הפך במהרה לפופולרי מאוד.
אתר התיעוד הנוכחי של פייתון נוצר באמצעותו.

בהמשך הקורס נכתוב פרויקטים, ובהם נוכל להשתמש ב־Sphinx ליצירת מסמכים שיעזרו למשתמשים בפרויקט להתמצא בו.
משתמשים בולטים ב־Sphinx כוללים, בין השאר, את:

אתר לאחסון קובצי תיעוד

בשנת 2010 פותח אתר בשם Read the Docs, שמטרתו לרכז תיעוד לפרויקטים שנכתבו בפייתון.
האתר מאפשר להעלות לרשת בקלות תיעודים שנוצרו בעזרת Sphinx ולהנגיש אותם לקהל הרחב.

משתמשים בולטים ב־Read the Docs כוללים, בין השאר, את:

  • המודול הפופולרי לעיבוד ולשליחה של בקשות אינטרנט requests.
  • מנהל החבילות של פייתון pip.
  • פלטפורמת המחברות Jupyter Notebooks.
  • המודול הנפוץ ביותר לעבודה עם כלים מתמטיים NumPy.

סגנונות תיעוד

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

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

  • אפשר לבנות מנוע חיפוש עבור תיעודי מודולים.
  • אפשר להפעיל כלים כמו Sphinx, שיסרקו את המודול ויצרו מהתיעוד שיש בו אתר תיעוד – באופן אוטומטי!
  • אפשר להפסיק לבזבז זמן מיותר בוויכוחים אינטרנטיים על איך יפה יותר לכתוב 😜

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

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


In [ ]:
class PostOffice:
    def __init__(self, usernames):
        self.message_id = 0
        self.boxes = {user: [] for user in usernames}
        
    def send_message(self, sender, recipient, message_body, urgent=False):
        user_box = self.boxes[recipient]
        self.message_id = self.message_id + 1
        message_details = {
            'id': self.message_id,
            'body': message_body,
            'sender': sender,
        }
        if urgent:
            user_box.insert(0, message_details)
        else:
            user_box.append(message_details)
        return self.message_id

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


In [ ]:
def show_example():
    """Show example of using the PostOffice class."""
    users = ('Newman', 'Mr. Peanutbutter')
    post_office = PostOffice(users)
    message_id = post_office.send_message(
        sender='Mr. Peanutbutter',
        recipient='Newman',
        message_body='Hello, Newman.',
    )
    print(f"Successfuly sent message number {message_id}.")
    print(post_office.boxes['Newman'])


show_example()

מבולבלים? איזה מזל שאנחנו הולכים לתעד את המחלקה הזו.
קדימה, לעבודה.

Google Docstrings

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

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


In [ ]:
class PostOffice:
    """A Post Office class. Allows users to message each other.
    
    Args:
        usernames (list): Users for which we should create PO Boxes.

    Attributes:
        message_id (int): Incremental id of the last message sent.
        boxes (dict): Users' inboxes.
    """

    def __init__(self, usernames):
        self.message_id = 0
        self.boxes = {user: [] for user in usernames}
        
    def send_message(self, sender, recipient, message_body, urgent=False):
        """Send a message to a recipient.

        Args:
            sender (str): The message sender's username.
            recipient (str): The message recipient's username.
            message_body (str): The body of the message.
            urgent (bool, optional): The urgency of the message.
                                    Urgent messages appear first.

        Returns:
            int: The message ID, auto incremented number.

        Raises:
            KeyError: If the recipient does not exist.

        Examples:
            After creating a PO box and sending a letter,
            the recipient should have 1 message in the
            inbox.

            >>> po_box = PostOffice(['a', 'b'])
            >>> message_id = po_box.send_message('a', 'b', 'Hello!')
            >>> len(po_box.boxes['b'])
            1
            >>> message_id
            1
        """
        user_box = self.boxes[recipient]
        self.message_id = self.message_id + 1
        message_details = {
            'id': self.message_id,
            'body': message_body,
            'sender': sender,
        }
        if urgent:
            user_box.insert(0, message_details)
        else:
            user_box.append(message_details)
        return self.message_id

לפי מסמך הסגנון של גוגל, לפעולה צריכים להיות 3 חלקי תיעוד:

  • Args: – רשימת הארגומנטים שהיא הולכת לקבל, סוגם והסבר קצר על כל אחד מהם.
  • Returns: – הערך שהפונקציה מחזירה והסוג שלו. במקרה של generator החלק יקרא Yields : במקום.
  • Raises: – השגיאות שהפונקציה עלולה לזרוק ובאילו מקרים זה עלול לקרות.
  • אפשר להוסיף גם חלקים משלנו, כמו Examples: שיראה כיצד הפונקציה פועלת. מומלץ לא להגזים עם זה.

יש לתעד גם מחלקות כמובן:

  • Attributes: – התכונות של המופעים שייווצרו על ידי המחלקה.
  • כל חלקי התיעוד ששייכים לפעולה רגילה, בהתייחס לפעולת ה־__init__ של המחלקה.

NumPy Docstrings

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


In [ ]:
class PostOffice:
    """A Post Office class. Allows users to message each other.
    
    Parameters
    ----------
    usernames : list
        Users for which we should create PO Boxes.

    Attributes
    ----------
    message_id : int
        Incremental id of the last message sent.
    boxes : dict
        Users' inboxes.
    """

    def __init__(self, usernames):
        self.message_id = 0
        self.boxes = {user: [] for user in usernames}
        
    def send_message(self, sender, recipient, message_body, urgent=False):
        """Send a message to a recipient.

        Parameters
        ----------
        sender : str
            The message sender's username.
        recipient : str
            The message recipient's username.
        message_body : str
            The body of the message.
        urgent : bool, optional
            The urgency of the message.
            Urgent messages appear first.

        Returns
        -------
        int
            The message ID, auto incremented number.

        Raises
        ------
        KeyError
            If the recipient does not exist.

        Examples
        --------
        After creating a PO box and sending a letter,
        the recipient should have 1 messege in the
        inbox.

        >>> po_box = PostOffice(['a', 'b'])
        >>> message_id = po_box.send_message('a', 'b', 'Hello!')
        >>> len(po_box.boxes['b'])
        1
        >>> message_id
        1
        """
        user_box = self.boxes[recipient]
        self.message_id = self.message_id + 1
        message_details = {
            'id': self.message_id,
            'body': message_body,
            'sender': sender,
        }
        if urgent:
            user_box.insert(0, message_details)
        else:
            user_box.append(message_details)
        return self.message_id

Sphinx

מעבר להיותו כלי ליצירת מסמכי תיעוד, ב־Sphinx קיימת גם הגדרה לצורה שבה לדעתם מחרוזות תיעוד אמורות להיראות.
בלי לחץ – Sphinx ידע להמיר את התיעוד שלכם למסמך גם אם תשתמשו ב־Google Docstrings או ב־NumPy Docstrings.
סגנון זה תופס את השטח המזערי ביותר לאורך הדף, אך הוא מעט קשה יותר לקריאה.
מחרוזות התיעוד ש־Sphinx מגדירים נראות כך:


In [ ]:
class PostOffice:
    """A Post Office class. Allows users to message each other.

    :ivar int message_id: Incremental id of the last message sent.
    :ivar dict boxes: Users' inboxes.

    :param list usernames: Users for which we should create PO Boxes.
    """

    def __init__(self, usernames):
        self.message_id = 0
        self.boxes = {user: [] for user in usernames}
        
    def send_message(self, sender, recipient, message_body, urgent=False):
        """Send a message to a recipient.

        :param str sender: The message sender's username.
        :param str recipient: The message recipient's username.
        :param str message_body: The body of the message.
        :param urgent: The urgency of the message.
        :type urgent: bool, optional
        :return: The message ID, auto incremented number.
        :rtype: int
        :raises KeyError: if the recipient does not exist.
        """
        user_box = self.boxes[recipient]
        self.message_id = self.message_id + 1
        message_details = {
            'id': self.message_id,
            'body': message_body,
            'sender': sender,
        }
        if urgent:
            user_box.insert(0, message_details)
        else:
            user_box.append(message_details)
        return self.message_id

לסיכום

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

מונחים

הערה (Comment)
קטע מילולי המשובץ בקוד ומתייחס אליו, ומטרתו להסביר, לבאר או להפנות את תשומת הלב של מתכנתים אחרים לנקודה מסוימת.
ההערה יכולה להופיע בסוף שורת קוד, בשורה עצמאית או כרצף של כמה שורות זו אחרי זו.
כתיבת הערות הסבר עבור קוד לא מובן נחשבת בעיני רבים לרעיון רע, ולכן במקרה כזה עדיף פשוט לשפר את הקוד.
תיעוד (Documentation)
אוסף הנחיות עבור משתמשי הקוד.
התיעוד יעזור למשתמש חיצוני להבין כיצד מתנהגת כל ישות בקוד, מה היא מצפה לקבל ומה היא עשויה להחזיר.
ראוי שכל ישות בקוד תהיה מתועדת היטב.
מחרוזת תיעוד (Docstring)
מחרוזת המוצבת בשורה הראשונה של פונקציה, פעולה, מחלקה או מודול ומטרתה לתאר את התפקיד של אותה ישות.
היא יכולה להיכתב בשורה אחת או בשורות רבות, והיא מתארת את תכלית הקוד ואת דרכי השימוש בו.
בניגוד להערה רגילה, מחרוזות תיעוד מתארות ממש מה הפונקציה עושה, ולא למה.
לעיתים מחרוזות התיעוד יכללו דוגמאות של ממש לשימוש בקוד.
reStructuredText
שפת סימון שנוצרה ב־2002 במטרה להקל על כותבי תיעוד ומסמכים טכניים.
מטרת השפה היא לייצר תסדיר שנוח לקרוא בעין אנושית, ושיהיה אפשר ליצור ממנו אוטומטית מסמכים מסוגננים.
שפת הסימון הזו תוקננה כשפת הסימון הרשמית שבה כותבים מסמכי תיעוד בפייתון.
Sphinx
כלי שמטרתו לעזור לייצר מסמכי דוקומנטציה.
כיום הוא הכלי הפופולרי ביותר לצורך הזה בעולם הפייתון, בפער ניכר מכלים דומים.
Sphinx גם יצרו סגנון תיעוד פופולרי (ע"ע "סגנון תיעוד").
Read the Docs
אתר המאפשר למשתמשים להעלות אליו מסמכי תיעוד.
סגנון תיעוד
ידוע בשמות: Documentation Style, Documentation Format, Docstring Format, Docstring Style.
צורה מקובלת לכתיבת תיעוד ולפירוט התכונות של פונקציה, פעולה, מחלקה או מודול.
שלושת הסגנונות הנפוצים ביותר הם של Sphinx, של Google ושל NumPy.

תרגילים

צָב שָׁלוּחַ

ממשו שתי פעולות נוספות למחלקת PostOffice:

  • הפעולה read_inbox תקבל כפרמטרים את שם המשתמש ואת מספר ההודעות שהוא מעוניין לקרוא (נקרא לה $N$).
    היא תחזיר את $N$ ההודעות הראשונות בתיבת הדואר הנכנס של המשתמש.
    אם לא הועבר מספר הודעות, החזירו את כל ההודעות בתיבת הדואר הנכנס של המשתמש.
    ההודעות יסומנו כנקראו ולא יוחזרו למשתמש בקריאה הבאה.
  • הפעולה search_inbox תקבל כפרמטרים שם משתמש ומחרוזת.
    היא תחזיר כרשימה את כל ההודעות שמכילות את המחרוזת, בכותרת שלהן או בגופן.

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

אורטל קומבט – חלק 1

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

  • name – שם השחקן.
  • hp – חיים (ערך מספרי שלם) שמאותחלים לערך המספרי 100.
  • exp – נקודות ניסיון (ערך מספרי שלם) שמאותחלות לערך המספרי 0.
  • level – רמת השחקן (ערך מספרי שלם) שמאותחלת לערך המספרי 1. לצורך תרגיל זה רמת השחקן תהיה תמיד 1.
  • nemeses – רשימת אויבים.
  • פעולת "attack" – שמקבלת מופע של שחקן, ומורידה לו בין $L \cdot 5$ ל־$L \cdot 20$ חיים (הגרילו), כאשר $L$ מייצגת את רמת השחקן המתקיף.
    אם הפעולה לא קיבלה מופע של שחקן אחר, היא מתקיפה את האויב האחרון שנוסף לרשימת nemeses של השחקן.
    אם הפעולה לא קיבלה מופע של שחקן אחר ואין לשחקן אויבים, הפעולה תקפיץ IndexError.
  • פעולת "revive" – מחזירה לשחקן את החיים לערך המרבי שלהם.

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

תעדו את התוכנית שלכם היטב.