November 8, 2021

פייתון 24 - ג'נרטורים


פרק קודם:

פייתון 23 - פונקציות קישוט חלק ג

מה נלמד

  • מהו Iterator ומהו Iterable
  • דוגמאות וכתיבת קוד ל-Iterator
  • מהי יצירה עצלנית
  • Generators
  • Coroutines

Iterable

מילונים, רשימות סטים או טאפלים כולם מאופיינים בהגדרה אחת בסיסית -

כולם יכולים להחזיק אלמנט אחד או יותר.

למדנו שניתן לרוץ על האלמנטים בעזרת ה-for:

1
2
3
myList = [1,2,3]
for n in myList:
print(n)

ה-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
2
3
4
5
6
myList = [1,2,3]

myListIterator = iter(myList)

while True:
print(next(myListIterator))

ה-iter(myList) נותן אובייקט מסוג איטרטור.
האיטרטור הזה מאפשר לקבל את האובייקט הבא בעזרת next(myListIterator).

מהי הבעיה?
נזרקת שגיאה - StopIteration!

כדי לתקן את זה פשוט נתפוס את השגיאה הזו:

1
2
3
4
5
6
7
8
9
myList = [1,2,3]

myListIterator = iter(myList)

while True:
try:
print(next(myListIterator))
except StopIteration:
break

ניתן להשתמש גם בפונקציות __iter__ ו-__next__.

1
2
3
4
5
6
7
8
9
myList = [1,2,3]

myListIterator = myList.__iter__()

while True:
try:
print(myListIterator.__next__())
except StopIteration:
break

כתיבת Iterable משלנו

על מנת לכתוב Iterable משלנו כל מה שעלינו לעשות זה לממש את שני הפונקציות:

  • __iter__.
  • __next__.

ניתן לפצל את ה-Iterable וה-Iterator,
או להכיל אותו באותה המחלקה.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NamesIterator:
def __iter__(self):
self.mNames = ['Bob', 'Alice', 'Gandalf']
self.mIndex = 0
return self

def __next__(self):
length = len(self.mNames)
if(self.mIndex > length - 1):
raise StopIteration
name = self.mNames[self.mIndex]
self.mIndex+=1
return name

names = NamesIterator()
for name in names:
print(name)

כדי ליצור Iterable חדש אנחנו מחזירים self.
המחלקה NamesIterator מממשת גם Iterator וגם Iterable.

תרגיל

  1. תקבעו מה המחלקות הבאות הוא אובייקט Iterator או Iterable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Names:
def __iter__(self):
return self

class Calculator:
def __init__(self, number):
self.Number = number

def __next__(self):
self.Number+=1
return self.Number

class Equipment:
def __itet__(self):
return self

class KanaLetters:
def __init__(self, letters):
self.Letters = letters
self.Iterator = self.Letters.__iter__()

def __iter__(self):
return self

def __next__(self):
return self.Iterator.__next__()
  1. כתבו Iterable משלכם שמחזירה מספרים רנדומליים עד לכמות מספרים שנקבעה בבנאי.


ריצה עצלנית

ריצה עצלנית לעומת ריצה ישירה היא סוג של טכניקה שנותנת לנו אפשרות ליצור או להשתמש במשאב רק מתי שנצטרך אותו.
למשל אם נרצה ליצור 10 מספרים מ1 עד 10.

דרך ישירה זה ליצור מערך עם הכל:

1
numbers = [1,2,3,4,5,6,7,8,9,10]

דרך עקיפה או עצלנית זה להשתמש ב-range:

1
2
3
4
numbers = []
for num in range(10):
print(num + 1)
numbers.append(num + 1)

אנחנו כל פעם יוצרים את המשאב במקום לטעון אותו מראש או שיוצרים אותו בזמן ריצה ולא בבת אחת.
Lazy initialization היא טכניקה מוכרת על מנת לחסוך זיכרון או לחסוך זמן ריצה במידה והמשאבים יקרים או לוקחים זמן לטעון אותם.

Generator

Generator הוא Iterator עצלן - ז”א המספר לא יווצר אלה אם ייבקשו את המספר הבא.
כדי ליצור Generator משתמשים בערך ההחזרה yield.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import random


def RandomNumbers(count):
i = 0
while i < count:
i += 1
print("Up by one")
yield random.randint(1, 100)


num = RandomNumbers(10)
print(num.__next__())
print(num.__next__())
print(num.__next__())

כמה פעמים יודפס פה Up by one?

xrange ו-range

ההיסטוריה היא חשובה וההבנה לאיך הגענו לפיצ’רים שיש לנו כיום בפייתון תעזור לכם להבין את הקונספטים התכנותיים.

הגרסא הראשונה של range יצרה מערך של מספרים וה-Iterator שלה החזיר מספר לפי המערך.
לכן קריאות כאלו היו יקרות:

1
2
for num in range(10000):
print(num)

לכן הוסיפו את הפונקציה xrange שהייתה מייצרת מספרים אחד אחד ולא מערך שלם.

בסופו של דבר לא היה צורך בגרסא “יקרה” יותר ולכן ה-range של היום הוא ה-xrange של פעם!

yield from

דרך נוספת להחזיר ג’נרטור בצורה ישירה היא לקרוא ל - yield from.

עבודה איטרטיבית עם רשימות

למשל אם יש לנו רשימה שאנחנו רוצים להחזיר:

1
2
3
4
5
6
def GetResult():
arr = [1,2,3]
yield from arr

for num in GetResult():
print(num)

בצורה מאוד פשוטה זה מתורגם ל-

1
2
3
4
5
6
7
def GetResult():
arr = [1,2,3]
for a in arr:
yield a

for num in GetResult():
print(num)

למה זה עובד?
כי רשימות הן איטרטיביות ומאפשרות שימוש ב-generator.

עבודה רקורסיבית עם Generator

כמו ה-yield from נותן לנו יכולת לעבוד עם generator רקורסיביים!
מה הכוונה - כאשר פונקציית generator הולכת להחזיר מפונקציית generator אחרת.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time

def Sleep(total):
now = time.time()
threshold = now + total
while now < threshold:
yield
now = time.time()


def MyFunc():
print("Waiting 3")
yield from Sleep(3)
print("Waiting 5")
yield from Sleep(5)
return 1


g = MyFunc()
while True:
try:
next(g)
except StopIteration:
break

print("Done")

yield from עוזר לפשט את הצורה שבה אנחנו מטפלים ברשימות אך זה עוזר לנו להשיג עוד 2 תכונות:

  • ערך החזר דו כיווני - מה-generator ול-generator.
  • טיפול בשגיאות

Generator send

generator הוא כביש חד סטרי.
פונקציית send פותחת נתיב נגדי ל-generator.

אם נרצה לקבל מה-yield תשובה, נוכל להשתמש בזה בהשמה:

1
2
3
4
5
6
7
8
9
10
11
12
13
def Func():
while True:
x = yield
print(x)

f = Func()

f.send(None)
f.send(1)
f.send(2)
f.send(3)
f.send(4)
f.send(5)

שימו לב שאנחנו צריכים להעביר בשורה הראשונה לשימוש של ה-generator: f.send(None).
הצורך של זה הוא “לאתחל” את ה-Generator.
כמו להתניע את המנוע של הרכב - אנחנו צריכים לבצע איטרציה אחת כדי שהוא ייתאחל את המשתנים.

דוגמא נוספת לאתחול יהיה ניתן להשתמש ישירות ב- __next__().

1
2
f = Func()
f.__next__()

Generator throw

נתחיל מדוגמא ללא throw.
מה ייקרה בקטע קוד הבא?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def gen():
try:
yield 1
yield 2
yield 3
except:
yield 4

g = gen()

for i in g:
if i == 2:
#a = g.throw(ValueError())
#print(a)
pass
print(i)

יודפס

1
2
3
1
2
3

מה ייקרה אם נרצה להוציא ש-2 זו לא תוצאה תקינה ונרצה לזרוק?

1
2
3
4
5
for i in g:
if i == 2:
a = g.throw(ValueError())
print(a)
print(i)

Generator close

הפונקצייה הזו עוזרת לנו לטפל במקרים שה-generator הוא אינסופי ולהודיע לו שהסוף קרב.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def myGen():
i = 0
while True:
i += 1
yield i


generator = myGen()

for a in generator:
print(a)
if a == 10:
generator.close()
break

במקרה הזה פונקציית ה-generator היא אינסופית ואנחנו צריכים לומר לה לבד להיסגר.

פונקציית close בעצם שולחת StopIteration והג’נרטור נסגר.
מה ייקרה במידה ונאלץ אותו להמשיך?
תיזרק השגיאה!

Coroutines

כדי לעבוד עם generators באופן איטרטיבי אנחנו לא צריכים את הפונקציונאליות הנוספת הזו…
אז למה פייתון הוסיפו את זה?
הפונקציות הנוספות נותנות לשפה את היכולת לממש קונספט מאוד חשוב שנקרא - Courtines.

תזמון ריצה

Courintes הם פונקציות המאפשרות “לעצור” או “להמשיך” את הריצה שלהן.
לאיזה צורך אנחנו משתמשים בהם?

על מנת לבצע ריבוי משימות ללא מקביליות!

בדיוק לצורך הזה נבנו שלושת הפונקציות הללו:

  • send - מאפשר להמשיך ריצה עם פרמטר - כמו שצוין ליצור כביש “דו סיטרי”.
  • throw - הכביש הדו סיטרי ייאפשר גם לזרוק ולתפוס שגיאות
  • close - ניתן להחליט לסגור את הכביש לחלוטין.

בצורה הזו נוכל לממש courtines משלנו.

אם היינו קוראים כל קובץ בבת אחת ומחזירים את כל התוצאות הריצה שלנו הייתה נראית כך:

כל קובץ בעצם היה נקרא, מעובד, ומיוצא.

בעזרת courtines נוכל בעצם לממש תהליכים קטנים יותר שעובדים ביחד - אבל עדיין בצורה סדרתית:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import time

def Sleep(total):
now = time.time()
threshold = now + total
while now < threshold:
yield
now = time.time()

def ReadFileAsync(path):
with open(path, 'r') as file:
lines = file.readlines()
for l in lines:
yield from Sleep(2)
print(l.replace('\n', ''))

def ReadAllAsync(names):
routines = [ReadFileAsync(name) for name in names]
pending = list(routines)
tasks = {task: None for task in pending}

while len(pending) > 0:
for gen in pending:
try:
tasks[gen] = gen.send(tasks[gen])
except StopIteration:
pending.remove(gen)
return


listOfFiles = ['text.txt', 'text2.txt', 'text3.txt']

ReadAllAsync(listOfFiles)

נסביר על הפונקצייה העיקרית ReadAllAsync:

1
2
3
4
5
6
7
8
9
10
11
12
def ReadAllAsync(names):
routines = [ReadFileAsync(name) for name in names]
pending = list(routines)
tasks = {task: None for task in pending}

while len(pending) > 0:
for gen in pending:
try:
tasks[gen] = gen.send(tasks[gen])
except StopIteration:
pending.remove(gen)
return
1
routines = [ReadFileAsync(name) for name in names]

כדי ליצור את התהליכים אנחנו משתמשים ב-for מקוצר.
routines מחזיק רשימה של generator.

לאחר מכן אנחנו בונים 2 פרמטרים נוספים - רשימה של משימות לביצוע,
ורשימה של כלל המשימות המבוצעות כרגע.

1
2
while len(pending) > 0:
for gen in pending:

כל עוד יש לנו מה לבצע אנחנו צריכים לקרוא למשימות האלו, כיצד הקריאה מתבצעת?
כאן כל הקסם קורה:

1
tasks[gen] = gen.send(tasks[gen])

כמו שלמדנו על פונקציית send הקריאה הזו מבצעת את ה-coroutine עד אשר מוחזר ה-yield הבא.
בדוגמא הזו ה-yield נמצא בפונקציית sleep.
אנחנו שומרים את מצב ה-generator עם ההשמה כדי שבפעם הבאה נמשיך אותו לביצוע:
tasks[gen] =.

זכרו שכל generator חייב להיפסק מתישהו, ולכן אנחנו תופסים את השגיאה בצורה הקלאסית:

1
2
except StopIteration:
pending.remove(gen)

בצורה הזו פייתון מממשת את עקרון ה-coroutines.
זה קיים כמעט בכל שפת תכנות מודרנית ומעניק לנו בעצם את היכולת “לעצור” ו”להמשיך” את הריצה של משימה.
זה מתבצע בעזרת yield ו-yield from.

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

חשוב לזכור

הדוגמא הזו היא לצורך לימודי ועדיף לא להשתמש בזה בקוד שאמור לרוץ ב-production.
ז”א קוד שיומצא למוצר כלשהו, בשביל זה פייתון כתבה את asyncio.
נלמד על זה עוד בהרחבה בפרקים הבאים!


תרגיל

  1. כתבו Generator המקבל כתובת לתיקייה במחשב, ומחזיר את תוכן כל קובץ שנמצא בתיקייה.
  1. בדקו אם מחרוזת היא Generator, בצעו ניסיונות עם הפונקציות שלמדנו ותקבעו אם כן או לא.

  2. בעזרת yield from תכתבו פונקצייה שמאגדת כמה פונקציות generator אחרות.
    יש לממש 2 פונקציות generator בסיסיות.

  • אחת שמקבלת מספר ומחזירה כל פעם את המספר + 1.
  • אחת שמקבלת מספר ומחזירה כל פעם מספר - 1.

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

  1. תבנו פונקציית counter בעזרת generator שתחזיר את המספר הנוכחי ותקבל כקלט את המספר הבא.


Generator חוסך לנו זמן ריצה ועבודה.
פייתון שוב פעם מראה לנו את הפשטות שלה בכתיבת קוד!

מעבר לכך - התכונות החדשות send, throw ו-close נותנות לפייתון כלי עוצמתי חדש.
היכולת לכתוב coroutines.

בפרק הבא נעסוק במקביליות ועולם התהליכונים.
כיצד לסנכרן וכיצד לעבוד נכון איתם.

ולאחר מכן נשלב coroutines עם אסינכרוניות ומקביליות!

פייתון 25 - מקביליות ואסינכרוניות חלק א

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#Programming#Python