מימוש redo ב Python

28/06/2018

 

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

לדוגמא הקובץ הבא:

I can see\
a tree behind\
the wall

And it's lovely

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

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

while (defined($line = <FH>) ) {
    chomp $line;
    if ($line =~ s/\\$//) {
        $line .= <FH>;
        redo unless eof(FH);
    }
    # process full record in $line here
}

הקוד קורא שורות מתוך הקובץ ובכל פעם שמגיע לשורה שמסתיימת ב \ הוא יקרא גם את השורה הבאה ואז יחזור לתחילת הלולאה, בלי לקרוא שורה חדשה ב while.

זה עובד כי redo יודע לדלג ל"תחילת" הלולאה בלי לבצע את הקוד שבתוך ה while. הוא קצת דומה ל continue רק שמתחיל שורה אחת קדימה.

בפייתון אין redo אבל אולי אפשר להתקרב. גם כאן תחילה הקוד ואחריו ההסבר:

def redo_loop(iterable):
    fake_next = None
    def redo(n):
        nonlocal fake_next
        fake_next = n

    def read_next():
        nonlocal fake_next
        return next(iterable)

    while True:
        try:
            if fake_next is not None:
                val = fake_next
                fake_next = None
            else:
                val = next(iterable)

            yield val, redo, read_next
        except StopIteration:
            break


for line, redo, read_next in redo_loop(iter(fileinput.input('demo.txt'))):
    line = line.strip()
    if line.endswith('\\'):
        line = line[:-1] + ' '
        line += read_next()
        redo(line)
        continue
    print(line)

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

הפונקציה redo היא נקודת הכניסה שלנו חזרה ללוגיקה של הלולאה. אומנם אי אפשר לדלג על ההשמה שה for מבצע, אבל אפשר לקבוע איזה ערך יישלח לשם. זה בדיוק מה ש redo עושה: אם קוראים לה אז במקום לקחת שורה חדשה מהקובץ השורה שתוחזר היא בדיוק מה שעבר ל redo. כך אנחנו "עובדים" על הלולאה להחזיר את הערך שכבר נמצא במשתנה line.

הפונקציה read_next שחוזרת גם היא מ redo_loop עוקפת את הבעיה שלא תמיד יש לנו בגוף הלולאה את ה Generator Object עליו אנחנו רצים. בשביל לקרוא את השורה הבאה מהקובץ צריך לדעת משהו על fileinput ועל ה File Object שעכשיו שמור בו. אומנם ב fileinput אפשר להגיע לאוביקט זה (באמצעות שדה _state של fileinput) אבל זה לא נכון במקרה הכללי וגם כאן לא בטוח שרצוי להסתמך עליו.

האם הייתי משתמש ב redo הזה בעולם האמיתי? כלל לא בטוח. אבל אני חושב (מקווה?) שהדוגמא הזאת תעזור לכם לחשוב מזווית נוספת על Generator Functions בפייתון ואולי תיתן לכם רעיונות לדברים מדליקים נוספים שאפשר לממש עם פיצ'ר זה.

 

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

def gen_cat_remove_cont(lines):
    item = ""
    for line in lines:
        line = line.strip()
        if line.endswith('\\'):
            item += line[:-1] + " "
        else:
            item += line
            yield item
            item = ""


for line in gen_cat_remove_cont(fileinput.input("text_with_cont.txt")):
    print(line)
    ... do work ...