פרק קודם:
פייתון 24 - פונקציות קישוט חלק גמה נלמד
- מהו 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 עם אסינכרוניות ומקביליות!