• בלוג
  • ספירת שורות בקובץ בפייתון

ספירת שורות בקובץ בפייתון

17/07/2025

הסיפור היום מתחיל כשביקשתי מ ChatGPT להדפיס פונקציה שסופרת כמה שורות יש בקובץ:

def count_lines(filepath):
    with open(filepath, 'r') as f:
        return sum(1 for _ in f)

אבל - איך זה עובד? בואו נפרק את הקוד יחד.

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

>>> open('/etc/passwd', 'r')
<_io.TextIOWrapper name='/etc/passwd' mode='r' encoding='UTF-8'>

פקודת open מחזירה TextIOWrapper. שתי פונקציות העטיפה שלו הן:

>>> f.__enter__
<built-in method __enter__ of _io.TextIOWrapper object at 0x100e879f0>

ו:

>>> f.__exit__
<built-in method __exit__ of _io.TextIOWrapper object at 0x100e879f0>

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

>>> f=open('/etc/passwd', 'r')
>>> f.__exit__()
>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

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

>>> (1 for _ in f)
<generator object <genexpr> at 0x101013ed0>

אם אמרנו ש f הוא TextIOWrapper, אז פקודת for ... in f היא פקודת יצירת גנרטור. היא עדיין לא מפעילה שום לולאה על הקובץ אלא רק מחזירה אוביקט שיודע "לעשות משהו" עם השורות שבקובץ:

>>> f=open('/etc/passwd', 'r')
>>> g=(1 for _ in f)
>>> next(g)
1
>>> next(g)
1
>>> next(g)
1

כל קריאה ל next על הגנרטור מבצעת איטרציה בודדת של האוביקט. במקרה של TextIOWrapper, איטרציה בודדת היא קריאה של מידע מהקובץ עד לגודל החוצץ המקסימלי (מוגדר ב io.DEFAULT_BUFFER_SIZE), חיפוש השורה הבאה והחזרת אותה השורה. הגנרטור עוטף את ההתנהגות הזאת ומחזיר משהו אחר במקום השורה. נתבונן רגע בגנרטור הפשוט יותר:

>>> g=(line for line in f)
>>> next(g)
'# Note that this file is consulted directly only when the system is running\n'
>>> next(g)
'# in single-user mode.  At other times this information is provided by\n'
>>> next(g)
'# Open Directory.\n'

הגנרטור (line for line in f) מחזיר את ערך המשתנה line בכל איטרציה. באותו אופן הגנרטור (1 for line in f) מחזיר את הערך 1 בכל איטרציה ומתעלם מהמשתנה line. ובגלל שאנחנו לא מתיחסים למשתנה הלולאה הרבה אנשים מעדיפים לקרוא לו בשם _, פשוט כדי שנדע שאנחנו לא משתמשים בערך שלו.

השורה המלאה:

return sum(1 for _ in f)

מפעילה את sum על גנרטור. זה מעניין! אנחנו רגילים להפעיל sum על רשימות:

>>> sum([10, 20, 30, 40])
100

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

>>> f=open('/etc/passwd', 'r')
>>> g=(1 for line in f)
>>> line_count = 0
>>> for value in g:
...     line_count += 1
...
>>> line_count
140

למעשה אם היינו כותבים לולאה כנראה שלא היינו מגדירים את הגנרטור ומראש כותבים לולאה כזאת:

def count_lines(filepath):
    with open(filepath, 'r') as f:
        line_count = 0
        for line in f:
            line_count += 1
        return line_count

הגנרטור אפשר לארגן מחדש את המבנה בצורה שיותר מתאימה ל sum וכך הצלחנו לכתוב:

def count_lines(filepath):
    with open(filepath, 'r') as f:
        return sum(1 for _ in f)

כי האיטרציה מבוצעת בתוך הפונקציה sum.

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