מימוש redo ב Python
יש פורמטים של קבצים המאפשרים "שבירת שורה" מזויפת. הרעיון שאנחנו כבני אדם לא אוהבים לראות שורות ארוכות מדי אבל יש תוכנות מחשב בהן שבירת השורה אומרת משהו על המשמעות. לכן נוצרו פורמטים של קבצים בהם שורות יכולות להסתיים בסימן \ ובמצב כזה נתיחס לשורה הבאה כאל המשך של אותה השורה.
לדוגמא הקובץ הבא:
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 ...