ברוכים הבאים לשיעור המבוא בנושא "תכנות פונקציונלי וגנרטורים" במסגרת הקורס "יסודות מדעי המחשב" (371.1.1601) באוניברסיטת בן-גוריון. יחידה זו חושפת אתכם לפרדיגמות תכנות מתקדמות המאפשרות טיפול יעיל ואלגנטי בזרמי נתונים, תוך שימת דגש על קוד קריא, מודולרי וחסכוני במשאבים. הבנה מעמיקה של נושאים אלו חיונית לא רק לפתרון בעיות מורכבות, אלא גם לכתיבת קוד "פיית'וני" אידיומטי, כפי שמצופה במבחני הקורס ובמטלות התכנות.
תכנות פונקציונלי בפייתון: עקרונות וכלים
תכנות פונקציונלי היא פרדיגמה המדגישה את השימוש בפונקציות טהורות, הימנעות ממצב משותף ואי-שינוי נתונים. בפייתון, אף שהיא שפה מרובת פרדיגמות, ניתן לאמץ עקרונות אלו כדי לשפר את קריאות הקוד, יכולת הבדיקה והמקביליות.
כלים פונקציונליים נפוצים בפייתון:
- פונקציות מסדר גבוה (Higher-Order Functions): פונקציות שמקבלות פונקציות אחרות כארגומנטים, או מחזירות פונקציות. דוגמאות בולטות הן
map,filterו-reduce. - ביטויי רשימה (List Comprehensions): דרך קומפקטית וקריאה ליצירת רשימות חדשות על בסיס רשימות קיימות, תוך יישום טרנספורמציות וסינון.
- פונקציות למדא (Lambda Functions): פונקציות אנונימיות קטנות וחד-שורתיות, שימושיות במיוחד כארגומנטים לפונקציות מסדר גבוה.
גנרטורים ואיטרטורים: טיפול יעיל בזרמי נתונים
גנרטורים הם כלי עוצמתי בפייתון המאפשר יצירת איטרטורים עצלניים (lazy iterators), החיוניים לטיפול בזרמי נתונים גדולים או אינסופיים מבלי לטעון את כולם לזיכרון בבת אחת.
next().מנגנון ה-yield:
הליבה של פונקציות גנרטור היא מילת המפתח yield. במקום return, שגורמת לפונקציה לסיים את ריצתה, yield מחזירה ערך ומשהה את ביצוע הפונקציה, שומרת את מצבה, ומאפשרת להמשיך מנקודה זו בקריאה הבאה.
yield אחד. כאשר היא נקראת, היא מחזירה אובייקט גנרטור (איטרטור) במקום לבצע את הקוד מיד.()) במקום מרובעים ([]).שילוב פרדיגמות לטיפול בזרמי נתונים
העוצמה האמיתית של תכנות פונקציונלי וגנרטורים מתגלה בשילובם. ניתן לבנות "צינורות נתונים" (data pipelines) שבהם כל שלב הוא פונקציה טהורה או גנרטור, המעבד את הנתונים באופן עצלני, חוסך בזיכרון ומשפר את ביצועי המערכת.
רשימות (Lists)
אוסף של איברים השמורים בזיכרון. נוצרות במלואן בבת אחת. שימושיות כאשר כל הנתונים נחוצים באופן מיידי וגודלם סביר.
גנרטורים (Generators)
מייצרים איברים לפי דרישה (lazy evaluation). חוסכים בזיכרון עבור זרמי נתונים גדולים או אינסופיים. אידיאליים לטיפול בנתונים שאינם נכנסים כולם לזיכרון.
פונקציות רגילות
מבצעות פעולה ומחזירות תוצאה יחידה (או כלום). אינן שומרות מצב בין קריאות ואינן מיועדות לייצר סדרת ערכים באופן הדרגתי.
שאלות לדיון
- הסבירו מתי עדיף להשתמש בביטוי גנרטור על פני ביטוי רשימה, ותנו דוגמה קונקרטית.
- כיצד עקרונות התכנות הפונקציונלי (כמו פונקציות טהורות) תורמים לקריאות ולתחזוקת קוד המשתמש בגנרטורים?
- תארו תרחיש שבו שימוש בגנרטורים יכול למנוע שגיאת "זיכרון אזל" (out of memory error) בתוכנית פייתון.
- האם ניתן להשתמש בפונקציית
reduceעם גנרטור? אם כן, מהם היתרונות/חסרונות?
נקודות לתשובת מודל
- ביטוי גנרטור מול ביטוי רשימה: ביטוי גנרטור עדיף כאשר לא כל האיברים נחוצים מיד, או כאשר מספר האיברים גדול מאוד/אינסופי. הוא חוסך זיכרון מכיוון שהוא מייצר ערכים לפי דרישה בלבד. לדוגמה, עיבוד שורות מקובץ ענק.
- תרומת תכנות פונקציונלי: פונקציות טהורות המקבלות איטרטור ומחזירות איטרטור חדש מאפשרות בניית צינורות נתונים מודולריים, קלים לבדיקה ולקומפוזיציה. כל שלב בצינור אינו משנה מצב גלובלי, מה שמפשט את ההבנה והניפוי.
- מניעת שגיאת זיכרון: תרחיש קלאסי הוא קריאת קובץ טקסט ענק (למשל,
huffman.pyאוmatrix.pyהמטפלים בנתונים גדולים). במקום לטעון את כל הקובץ לרשימת שורות בזיכרון, גנרטור יכול לקרוא ולעבד שורה אחר שורה, ובכך למנוע עומס זיכרון. reduceעם גנרטור: כן, ניתן להשתמש ב-reduce(מתוךfunctools) עם גנרטור. היתרון הוא ש-reduceיעבד את האיברים מהגנרטור אחד-אחד, מבלי לדרוש את כל הנתונים בזיכרון. החיסרון הוא ש-reduceאינו עצלן בעצמו; הוא יצרוך את כל האיברים מהגנרטור עד לקבלת תוצאה סופית אחת.