פרק קודם:
פייתון 23 - פונקציות קישוט חלק גמה נלמד
- מהו Iterator ומהו Iterable
- דוגמאות וכתיבת קוד ל-Iterator
- מהי יצירה עצלנית
- Generators
- Coroutines
Iterable
מילונים, רשימות סטים או טאפלים כולם מאופיינים בהגדרה אחת בסיסית -
כולם יכולים להחזיק אלמנט אחד או יותר.
למדנו שניתן לרוץ על האלמנטים בעזרת ה-for
:
1 | myList = [1,2,3] |
ה-myList
הוא אובייקט מסוג Iterable
.
כל אובייקט שהוא Iterable
ניתן לשים אותו בלולאת ה-for x in y
.
מה מיוחד באובייקט? הוא נותן לנו Iterator
.
אובייקט Iterable
הוא אובייקט שהמחלקה שלו מממשת את הפונקציה __iter__
.
Iterator
אובייקט שרץ על האלמנטים ונותן לנו אפשרות לקבל את האלמנט הבא בתור.
אובייקט Iteartor
הוא אובייקט שהמחלקה שלו מממשת את הפונקציה __next__
.
כדי לקבל אובייקט Iterator
ניתן להשתמש בפונקציה iter()
או __iter__
.
ניתן לקבל את האלמנט הבא בתור בעזרת הפונקציה next()
או __next__
של ה-Iterator
.
StopIteration Exception
את לולאת ה-for x in y:
ניתן ליישם כך:
1 | myList = [1,2,3] |
ה-iter(myList)
נותן אובייקט מסוג איטרטור.
האיטרטור הזה מאפשר לקבל את האובייקט הבא בעזרת next(myListIterator)
.
מהי הבעיה?
נזרקת שגיאה - StopIteration
!
כדי לתקן את זה פשוט נתפוס את השגיאה הזו:
1 | myList = [1,2,3] |
ניתן להשתמש גם בפונקציות __iter__
ו-__next__
.
1 | myList = [1,2,3] |
כתיבת Iterable משלנו
על מנת לכתוב Iterable
משלנו כל מה שעלינו לעשות זה לממש את שני הפונקציות:
__iter__
.__next__
.
ניתן לפצל את ה-Iterable
וה-Iterator
,
או להכיל אותו באותה המחלקה.
1 | class NamesIterator: |
כדי ליצור Iterable
חדש אנחנו מחזירים self
.
המחלקה NamesIterator
מממשת גם Iterator
וגם Iterable
.
תרגיל
- תקבעו מה המחלקות הבאות הוא אובייקט
Iterator
אוIterable
.
1 | class Names: |
- כתבו
Iterable
משלכם שמחזירה מספרים רנדומליים עד לכמות מספרים שנקבעה בבנאי.
Names
היא Iterable
.
Calculator
היא Iterator
.
Equipment
היא לא Iterable
בגלל שהפונקציה מכילה שגיאת כתיב - itet
במקום iter
.
KanaLetters
היא גם Iterable
וגם Iterator
.
1 | import random |
ריצה עצלנית
ריצה עצלנית לעומת ריצה ישירה היא סוג של טכניקה שנותנת לנו אפשרות ליצור או להשתמש במשאב רק מתי שנצטרך אותו.
למשל אם נרצה ליצור 10 מספרים מ1 עד 10.
דרך ישירה זה ליצור מערך עם הכל:
1 | numbers = [1,2,3,4,5,6,7,8,9,10] |
דרך עקיפה או עצלנית זה להשתמש ב-range
:
1 | numbers = [] |
אנחנו כל פעם יוצרים את המשאב במקום לטעון אותו מראש או שיוצרים אותו בזמן ריצה ולא בבת אחת.Lazy initialization
היא טכניקה מוכרת על מנת לחסוך זיכרון או לחסוך זמן ריצה במידה והמשאבים יקרים או לוקחים זמן לטעון אותם.
Generator
Generator
הוא Iterator
עצלן - ז”א המספר לא יווצר אלה אם ייבקשו את המספר הבא.
כדי ליצור Generator
משתמשים בערך ההחזרה yield
.
1 | import random |
כמה פעמים יודפס פה Up by one
?
xrange ו-range
ההיסטוריה היא חשובה וההבנה לאיך הגענו לפיצ’רים שיש לנו כיום בפייתון תעזור לכם להבין את הקונספטים התכנותיים.
הגרסא הראשונה של range
יצרה מערך של מספרים וה-Iterator
שלה החזיר מספר לפי המערך.
לכן קריאות כאלו היו יקרות:
1 | for num in range(10000): |
לכן הוסיפו את הפונקציה xrange
שהייתה מייצרת מספרים אחד אחד ולא מערך שלם.
בסופו של דבר לא היה צורך בגרסא “יקרה” יותר ולכן ה-range
של היום הוא ה-xrange
של פעם!
yield from
דרך נוספת להחזיר ג’נרטור בצורה ישירה היא לקרוא ל - yield from
.
עבודה איטרטיבית עם רשימות
למשל אם יש לנו רשימה שאנחנו רוצים להחזיר:
1 | def GetResult(): |
בצורה מאוד פשוטה זה מתורגם ל-
1 | def GetResult(): |
למה זה עובד?
כי רשימות הן איטרטיביות ומאפשרות שימוש ב-generator
.
עבודה רקורסיבית עם Generator
כמו ה-yield from
נותן לנו יכולת לעבוד עם generator
רקורסיביים!
מה הכוונה - כאשר פונקציית generator
הולכת להחזיר מפונקציית generator
אחרת.
1 | import time |
yield from
עוזר לפשט את הצורה שבה אנחנו מטפלים ברשימות אך זה עוזר לנו להשיג עוד 2 תכונות:
- ערך החזר דו כיווני - מה-
generator
ול-generator
. - טיפול בשגיאות
Generator send
generator
הוא כביש חד סטרי.
פונקציית send
פותחת נתיב נגדי ל-generator
.
אם נרצה לקבל מה-yield
תשובה, נוכל להשתמש בזה בהשמה:
1 | def Func(): |
שימו לב שאנחנו צריכים להעביר בשורה הראשונה לשימוש של ה-generator
: f.send(None)
.
הצורך של זה הוא “לאתחל” את ה-Generator
.
כמו להתניע את המנוע של הרכב - אנחנו צריכים לבצע איטרציה אחת כדי שהוא ייתאחל את המשתנים.
דוגמא נוספת לאתחול יהיה ניתן להשתמש ישירות ב- __next__()
.
1 | f = Func() |
Generator throw
נתחיל מדוגמא ללא throw
.
מה ייקרה בקטע קוד הבא?
1 | def gen(): |
יודפס
1 | 1 |
מה ייקרה אם נרצה להוציא ש-2
זו לא תוצאה תקינה ונרצה לזרוק?
1 | for i in g: |
יודפס:
1 | 1 |
למה?
כי 1
נקרא,
לאחר מכן ב2
אנחנו נכנסים ל-if
וזורקים לתוך הפונקציה שגיאה.
הפונקציה תופסת אותה ומחזירה 4
.
מודפס 4
ובסוף מודפס 2
!
Generator close
הפונקצייה הזו עוזרת לנו לטפל במקרים שה-generator
הוא אינסופי ולהודיע לו שהסוף קרב.
1 | def myGen(): |
במקרה הזה פונקציית ה-generator
היא אינסופית ואנחנו צריכים לומר לה לבד להיסגר.
פונקציית close
בעצם שולחת StopIteration
והג’נרטור נסגר.
מה ייקרה במידה ונאלץ אותו להמשיך?
תיזרק השגיאה!
Coroutines
כדי לעבוד עם generators
באופן איטרטיבי אנחנו לא צריכים את הפונקציונאליות הנוספת הזו…
אז למה פייתון הוסיפו את זה?
הפונקציות הנוספות נותנות לשפה את היכולת לממש קונספט מאוד חשוב שנקרא - Courtines
.
תזמון ריצה
Courintes
הם פונקציות המאפשרות “לעצור” או “להמשיך” את הריצה שלהן.
לאיזה צורך אנחנו משתמשים בהם?
על מנת לבצע ריבוי משימות ללא מקביליות!
בדיוק לצורך הזה נבנו שלושת הפונקציות הללו:
send
- מאפשר להמשיך ריצה עם פרמטר - כמו שצוין ליצור כביש “דו סיטרי”.throw
- הכביש הדו סיטרי ייאפשר גם לזרוק ולתפוס שגיאותclose
- ניתן להחליט לסגור את הכביש לחלוטין.
בצורה הזו נוכל לממש courtines
משלנו.
אם היינו קוראים כל קובץ בבת אחת ומחזירים את כל התוצאות הריצה שלנו הייתה נראית כך:
כל קובץ בעצם היה נקרא, מעובד, ומיוצא.
בעזרת courtines
נוכל בעצם לממש תהליכים קטנים יותר שעובדים ביחד - אבל עדיין בצורה סדרתית:
1 | import time |
נסביר על הפונקצייה העיקרית ReadAllAsync
:
1 | def ReadAllAsync(names): |
1 | routines = [ReadFileAsync(name) for name in names] |
כדי ליצור את התהליכים אנחנו משתמשים ב-for
מקוצר.routines
מחזיק רשימה של generator
.
לאחר מכן אנחנו בונים 2 פרמטרים נוספים - רשימה של משימות לביצוע,
ורשימה של כלל המשימות המבוצעות כרגע.
1 | while len(pending) > 0: |
כל עוד יש לנו מה לבצע אנחנו צריכים לקרוא למשימות האלו, כיצד הקריאה מתבצעת?
כאן כל הקסם קורה:
1 | tasks[gen] = gen.send(tasks[gen]) |
כמו שלמדנו על פונקציית send
הקריאה הזו מבצעת את ה-coroutine
עד אשר מוחזר ה-yield
הבא.
בדוגמא הזו ה-yield
נמצא בפונקציית sleep
.
אנחנו שומרים את מצב ה-generator
עם ההשמה כדי שבפעם הבאה נמשיך אותו לביצוע:tasks[gen] =
.
זכרו שכל generator
חייב להיפסק מתישהו, ולכן אנחנו תופסים את השגיאה בצורה הקלאסית:
1 | except StopIteration: |
בצורה הזו פייתון מממשת את עקרון ה-coroutines
.
זה קיים כמעט בכל שפת תכנות מודרנית ומעניק לנו בעצם את היכולת “לעצור” ו”להמשיך” את הריצה של משימה.
זה מתבצע בעזרת yield
ו-yield from
.
אנחנו במאמר הזה לא נתעמק יותר מזה כי זה יורחב בפרקים הבאים יותר לעומק :)
בדוגמא הזו בעצם מימשנו סוג של event loop
מאוד מאוד בסיסי.
חשוב לזכור
הדוגמא הזו היא לצורך לימודי ועדיף לא להשתמש בזה בקוד שאמור לרוץ ב-production
.
ז”א קוד שיומצא למוצר כלשהו, בשביל זה פייתון כתבה את asyncio
.
נלמד על זה עוד בהרחבה בפרקים הבאים!
תרגיל
- כתבו
Generator
המקבל כתובת לתיקייה במחשב, ומחזיר את תוכן כל קובץ שנמצא בתיקייה.
- ניתן להיעזר בפרק 11: פייתון 11 - קידוד וקבצים
בדקו אם מחרוזת היא
Generator
, בצעו ניסיונות עם הפונקציות שלמדנו ותקבעו אם כן או לא.בעזרת
yield from
תכתבו פונקצייה שמאגדת כמה פונקציותgenerator
אחרות.
יש לממש 2 פונקציותgenerator
בסיסיות.
- אחת שמקבלת מספר ומחזירה כל פעם את המספר + 1.
- אחת שמקבלת מספר ומחזירה כל פעם מספר - 1.
יש לבנות פונקציה אחת שתקבל כמות generator
אינסופית (ברשימה למשל) ותדאג אותם לפונקצייה אחת.
- תבנו פונקציית
counter
בעזרתgenerator
שתחזיר את המספר הנוכחי ותקבל כקלט את המספר הבא.
1 | import os |
בשביל לבדוק את זה תיצרו תיקייה עם קבצי txt
ותנסו את הקוד!
מחרוזת היא אכן Iterator
.
1 | myStr = 'Hello World' |
1 | def CountUp(num, Total): |
שתי הפונקציות הראשונות הן פשוטות למימוש - אחת מורידה מספר ואחת מעלה מספר ומבצעת yield
פשוט.
עבור פונקציית CountAll
השתמשנו בפיצ’ר של השפה yield from
כדי להחזיר עבור כל מתודה.
מכיוון שאנחנו מבצעים for method in countingMethods
אנחנו מאגדים הכל ל-generator
אחד.
בסופו של דבר ניתן לרוץ על הכל עם לולאה אחת.
1 | import random |
התרגיל הזה ביצע את מה שלמדנו לגבי send
.
בעזרת yield
ביצענו החזרה וביקשנו מה-generator
לקבל מספר חדש שאותו אנחנו סוכמים.
בעזרת send
אנחנו שולחים את המספר.
קצת גיוונתי בתרגיל והוספתי מספר רנדומלי בעזרת random.randint()
וכך גם כדי לראות את האיטרציות הוספתי sleep
קטן :)
זכרו:counter.send(None)
מבצע “אתחול” ל-generator
.
Generator
חוסך לנו זמן ריצה ועבודה.
פייתון שוב פעם מראה לנו את הפשטות שלה בכתיבת קוד!
מעבר לכך - התכונות החדשות send
, throw
ו-close
נותנות לפייתון כלי עוצמתי חדש.
היכולת לכתוב coroutines
.
בפרק הבא נעסוק במקביליות ועולם התהליכונים.
כיצד לסנכרן וכיצד לעבוד נכון איתם.
ולאחר מכן נשלב coroutines
עם אסינכרוניות ומקביליות!