Smart-World Surf

יחידה 11: תכנות פונקציונלי וגנרטורים

פרדיגמות תכנות מתקדמות לטיפול בזרמי נתונים.

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

תכנות פונקציונלי בפייתון: עקרונות וכלים

תכנות פונקציונלי היא פרדיגמה המדגישה את השימוש בפונקציות טהורות, הימנעות ממצב משותף ואי-שינוי נתונים. בפייתון, אף שהיא שפה מרובת פרדיגמות, ניתן לאמץ עקרונות אלו כדי לשפר את קריאות הקוד, יכולת הבדיקה והמקביליות.

פונקציה טהורה (Pure Function): פונקציה שמחזירה תמיד את אותה תוצאה עבור אותן קלטים, ואין לה תופעות לוואי (side effects) על מצב התוכנית מחוץ לתחומה.
אי-שינוי (Immutability): עיקרון לפיו נתונים, לאחר שנוצרו, אינם ניתנים לשינוי. במקום לשנות אובייקט קיים, יוצרים אובייקט חדש עם השינויים הרצויים.

כלים פונקציונליים נפוצים בפייתון:

  • פונקציות מסדר גבוה (Higher-Order Functions): פונקציות שמקבלות פונקציות אחרות כארגומנטים, או מחזירות פונקציות. דוגמאות בולטות הן map, filter ו-reduce.
  • ביטויי רשימה (List Comprehensions): דרך קומפקטית וקריאה ליצירת רשימות חדשות על בסיס רשימות קיימות, תוך יישום טרנספורמציות וסינון.
  • פונקציות למדא (Lambda Functions): פונקציות אנונימיות קטנות וחד-שורתיות, שימושיות במיוחד כארגומנטים לפונקציות מסדר גבוה.

גנרטורים ואיטרטורים: טיפול יעיל בזרמי נתונים

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

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

מנגנון ה-yield:

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

פונקציית גנרטור (Generator Function): פונקציה המכילה לפחות ביטוי yield אחד. כאשר היא נקראת, היא מחזירה אובייקט גנרטור (איטרטור) במקום לבצע את הקוד מיד.
ביטוי גנרטור (Generator Expression): תחביר קומפקטי ליצירת גנרטורים, בדומה לביטויי רשימה אך עם סוגריים עגולים (()) במקום מרובעים ([]).

שילוב פרדיגמות לטיפול בזרמי נתונים

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

רשימות (Lists)

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

גנרטורים (Generators)

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

פונקציות רגילות

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

הערכה עצלנית וחיסכון בזיכרון: זוהי נקודה קריטית למבחן! הבנה מעמיקה של ההבדל בין יצירת רשימה מלאה בזיכרון לבין שימוש בגנרטור המייצר ערכים "על הדרך" (on-the-fly) היא יסודית. שאלות רבות במבחנים ובמטלות יבחנו את יכולתכם ליישם גנרטורים לפתרון בעיות זיכרון או יעילות, במיוחד בהקשר של עיבוד קבצים גדולים או יצירת סדרות ארוכות.

שאלות לדיון

  • הסבירו מתי עדיף להשתמש בביטוי גנרטור על פני ביטוי רשימה, ותנו דוגמה קונקרטית.
  • כיצד עקרונות התכנות הפונקציונלי (כמו פונקציות טהורות) תורמים לקריאות ולתחזוקת קוד המשתמש בגנרטורים?
  • תארו תרחיש שבו שימוש בגנרטורים יכול למנוע שגיאת "זיכרון אזל" (out of memory error) בתוכנית פייתון.
  • האם ניתן להשתמש בפונקציית reduce עם גנרטור? אם כן, מהם היתרונות/חסרונות?

נקודות לתשובת מודל

  • ביטוי גנרטור מול ביטוי רשימה: ביטוי גנרטור עדיף כאשר לא כל האיברים נחוצים מיד, או כאשר מספר האיברים גדול מאוד/אינסופי. הוא חוסך זיכרון מכיוון שהוא מייצר ערכים לפי דרישה בלבד. לדוגמה, עיבוד שורות מקובץ ענק.
  • תרומת תכנות פונקציונלי: פונקציות טהורות המקבלות איטרטור ומחזירות איטרטור חדש מאפשרות בניית צינורות נתונים מודולריים, קלים לבדיקה ולקומפוזיציה. כל שלב בצינור אינו משנה מצב גלובלי, מה שמפשט את ההבנה והניפוי.
  • מניעת שגיאת זיכרון: תרחיש קלאסי הוא קריאת קובץ טקסט ענק (למשל, huffman.py או matrix.py המטפלים בנתונים גדולים). במקום לטעון את כל הקובץ לרשימת שורות בזיכרון, גנרטור יכול לקרוא ולעבד שורה אחר שורה, ובכך למנוע עומס זיכרון.
  • reduce עם גנרטור: כן, ניתן להשתמש ב-reduce (מתוך functools) עם גנרטור. היתרון הוא ש-reduce יעבד את האיברים מהגנרטור אחד-אחד, מבלי לדרוש את כל הנתונים בזיכרון. החיסרון הוא ש-reduce אינו עצלן בעצמו; הוא יצרוך את כל האיברים מהגנרטור עד לקבלת תוצאה סופית אחת.
מצאתם טעות או שחסר משהו?
→ הקודמת
מבני נתונים מורכבים: עצים
הבאה ←
תכנות דינמי ומימוניזציה