גלו את עוצמת האיטרציה ב-Python. מדריך מקיף למפתחים על מימוש איטרטורים מותאמים אישית באמצעות שיטות __iter__ ו- __next__, עם דוגמאות פרקטיות מהעולם האמיתי.
מפענחים את פרוטוקול האיטרטור של Python: צלילה עמוקה לתוך __iter__ ו- __next__
איטרציה היא אחד המושגים הבסיסיים ביותר בתכנות. בפייתון, זהו המנגנון האלגנטי והיעיל שמניע הכל, מלולאות for פשוטות ועד צינורות עיבוד נתונים מורכבים. אתם משתמשים בזה מדי יום כשאתם עוברים על רשימה, קוראים שורות מקובץ, או עובדים עם תוצאות מסד נתונים. אבל האם אי פעם תהיתם מה קורה מתחת למכסה המנוע? איך פייתון יודע איך לקבל את הפריט 'הבא' מכל כך הרבה סוגים שונים של אובייקטים?
התשובה טמונה בתבנית עיצוב חזקה ואלגנטית הידועה בשם פרוטוקול האיטרטור. פרוטוקול זה הוא השפה המשותפת שבה מדברים כל האובייקטים דמויי הרצף של פייתון. על ידי הבנה ויישום של פרוטוקול זה, תוכלו ליצור אובייקטים מותאמים אישית משלכם התואמים באופן מלא לכלי האיטרציה של פייתון, מה שהופך את הקוד שלכם לאקספרסיבי יותר, יעיל יותר בזיכרון, ובמהותו 'פיית׳וני'.
מדריך מקיף זה ייקח אתכם לצלילה עמוקה לתוך פרוטוקול האיטרטור. נחשוף את הקסם שמאחורי שיטות ה-`__iter__` ו-`__next__`, נבהיר את ההבדל המהותי בין אובייקט בר-איטרציה (iterable) לאיטרטור, וננחה אתכם בבניית איטרטורים מותאמים אישית משלכם מאפס. בין אם אתם מפתחים בינוניים המעוניינים להעמיק את הבנתם בפעילות הפנימית של פייתון או מומחים המכוונים לעיצוב ממשקי API מתוחכמים יותר, שליטה בפרוטוקול האיטרטור היא צעד קריטי במסע שלכם.
ה'למה': החשיבות והעוצמה של איטרציה
לפני שצוללים ליישום הטכני, חיוני להבין מדוע פרוטוקול האיטרטור כל כך חשוב. היתרונות שלו חורגים הרבה מעבר לאפשרות להשתמש בלולאות `for`.
יעילות זיכרון והערכה עצלה (Lazy Evaluation)
תארו לעצמכם שאתם צריכים לעבד קובץ לוג ענק בגודל של מספר ג'יגה-בייטים. אם הייתם קוראים את כל הקובץ לרשימה בזיכרון, סביר להניח שהייתם ממצים את משאבי המערכת שלכם. איטרטורים פותרים בעיה זו בצורה יפהפייה באמצעות קונספט הנקרא הערכה עצלה (lazy evaluation).
איטרטור אינו טוען את כל הנתונים בבת אחת. במקום זאת, הוא יוצר או מאחזר פריט אחד בכל פעם, רק כשהוא נדרש. הוא שומר על מצב פנימי כדי לזכור היכן הוא נמצא ברצף. המשמעות היא שאתם יכולים לעבד זרם נתונים אינסופי בגודלו (בתיאוריה) עם כמות זיכרון קטנה וקבועה מאוד. זהו אותו עיקרון המאפשר לכם לקרוא קובץ ענק שורה אחר שורה מבלי שהתוכנה שלכם תקרוס.
קוד נקי, קריא ואוניברסלי
פרוטוקול האיטרטור מספק ממשק אוניברסלי לגישה סדרתית. מכיוון שרשימות, טאפלים, מילונים, מחרוזות, אובייקטי קבצים וסוגים רבים אחרים כולם דבקים בפרוטוקול זה, אתם יכולים להשתמש באותו תחביר — לולאת ה-`for` — כדי לעבוד עם כולם. אחידות זו היא אבן יסוד בקריאות הקוד של פייתון.
חשבו על הקוד הזה:
קוד:
my_list = [1, 2, 3]
for item in my_list:
print(item)
my_string = "abc"
for char in my_string:
print(char)
with open('my_file.txt', 'r') as f:
for line in f:
print(line)
לולאת ה-`for` לא אכפת אם היא עוברת על רשימת מספרים שלמים, מחרוזת תווים, או שורות מקובץ. היא פשוט מבקשת מהאובייקט את האיטרטור שלו ואז מבקשת שוב ושוב מהאיטרטור את הפריט הבא שלו. הפשטה זו חזקה להפליא.
פירוק פרוטוקול האיטרטור
הפרוטוקול עצמו פשוט באופן מפתיע, ומוגדר על ידי שתי שיטות מיוחדות בלבד, הנקראות לעתים קרובות שיטות "dunder" (double underscore):
- `__iter__()`
- `__next__()`
כדי להבין אותן במלואן, עלינו להבין תחילה את ההבדל בין שני מושגים קשורים אך שונים: אובייקט בר-איטרציה (iterable) ואיטרטור.
אובייקט בר-איטרציה מול איטרטור: הבחנה מכרעת
זוהי לעיתים קרובות נקודת בלבול עבור מתחילים, אך ההבדל קריטי.
מהו אובייקט בר-איטרציה (Iterable)?
אובייקט בר-איטרציה (iterable) הוא כל אובייקט שניתן לעבור עליו בלולאה. זהו אובייקט שאתם יכולים להעביר לפונקציה המובנית `iter()` כדי לקבל איטרטור. מבחינה טכנית, אובייקט נחשב בר-איטרציה אם הוא מיישם את שיטת ה-`__iter__`. המטרה הבלעדית של שיטת ה-`__iter__` שלו היא להחזיר אובייקט איטרטור.
דוגמאות לאובייקטים מובנים ברי-איטרציה כוללות:
- רשימות (`[1, 2, 3]`)
- טאפלים (`(1, 2, 3)`)
- מחרוזות (`"hello"`)
- מילונים (`{'a': 1, 'b': 2}` - עובר על המפתחות)
- קבוצות (`{1, 2, 3}`)
- אובייקטי קבצים
אתם יכולים לחשוב על אובייקט בר-איטרציה כעל מיכל או מקור נתונים. הוא לא יודע איך לייצר את הפריטים בעצמו, אבל הוא יודע איך ליצור אובייקט שיכול: האיטרטור.
מהו איטרטור?
איטרטור הוא האובייקט שמבצע בפועל את העבודה של ייצור הערכים במהלך האיטרציה. הוא מייצג זרם נתונים. איטרטור חייב ליישם שתי שיטות:
- `__iter__()`: שיטה זו צריכה להחזיר את אובייקט האיטרטור עצמו (`self`). זה נדרש כדי שאיטרטורים יוכלו לשמש גם במקומות שבהם מצפים לאובייקטים ברי-איטרציה, למשל, בלולאת `for`.
- `__next__()`: שיטה זו היא המנוע של האיטרטור. היא מחזירה את הפריט הבא ברצף. כאשר אין יותר פריטים להחזיר, היא חייבת לזרוק את חריגת ה-`StopIteration`. חריגה זו אינה שגיאה; זהו האות הסטנדרטי למבנה הלולאה שהאיטרציה הושלמה.
מאפיינים מרכזיים של איטרטור הם:
- הוא שומר על מצב: איטרטור זוכר את מיקומו הנוכחי ברצף.
- הוא מייצר ערכים אחד בכל פעם: באמצעות שיטת ה-`__next__`.
- הוא מתכלה (exhaustible): ברגע שאיטרטור נצרך במלואו (כלומר, הוא זרק `StopIteration`), הוא ריק. לא ניתן לאפס או לעשות בו שימוש חוזר. כדי לבצע איטרציה שוב, עליכם לחזור לאובייקט הבר-איטרציה המקורי ולקבל איטרטור חדש על ידי קריאה נוספת ל-`iter()` עליו.
בניית האיטרטור המותאם אישית הראשון שלנו: מדריך שלב אחר שלב
תיאוריה זה נהדר, אבל הדרך הטובה ביותר להבין את הפרוטוקול היא לבנות אותו בעצמכם. בואו ניצור מחלקה פשוטה שתשמש כמונה, שתבצע איטרציה ממספר התחלתי ועד לגבול מסוים.
דוגמה 1: מחלקת מונה פשוטה
ניצור מחלקה בשם `CountUpTo`. כאשר תיצרו מופע שלה, תציינו מספר מקסימלי, וכאשר תעברו עליה בלולאה, היא תפיק מספרים מ-1 ועד למקסימום זה.
קוד:
class CountUpTo:
"""איטרטור שסופר מ-1 ועד למספר מקסימלי שצוין."""
def __init__(self, max_num):
print("Initializing the CountUpTo object...")
self.max_num = max_num
self.current = 0 # זה יאחסן את המצב
def __iter__(self):
print("__iter__ called, returning self...")
# אובייקט זה הוא האיטרטור של עצמו, לכן אנו מחזירים את self
return self
def __next__(self):
print("__next__ called...")
if self.current < self.max_num:
self.current += 1
return self.current
else:
# זהו החלק המכריע: לאותת שסיימנו.
print("Raising StopIteration.")
raise StopIteration
# איך להשתמש בזה
print("Creating the counter object...")
counter = CountUpTo(3)
print("\nStarting the for loop...")
for number in counter:
print(f"For loop received: {number}")
ניתוח קוד והסבר
בואו ננתח מה קורה כאשר לולאת ה-`for` רצה:
- אתחול: `counter = CountUpTo(3)` יוצר מופע של המחלקה שלנו. שיטת ה-`__init__` רצה, ומגדירה את `self.max_num` ל-3 ואת `self.current` ל-0. מצב האובייקט שלנו מאותחל כעת.
- התחלת הלולאה: כאשר שורת ה-`for number in counter:` מגיעה, פייתון קורא באופן פנימי ל-`iter(counter)`.
- קריאה ל-`__iter__`: הקריאה ל-`iter(counter)` מפעילה את שיטת ה-`counter.__iter__()` שלנו. כפי שאתם יכולים לראות מהקוד שלנו, שיטה זו פשוט מדפיסה הודעה ומחזירה את `self`. זה אומר ללולאת ה-`for`, "האובייקט שאתם צריכים לקרוא לו `__next__` זה אני!"
- הלולאה מתחילה: כעת לולאת ה-`for` מוכנה. בכל איטרציה, היא תקרא ל-`next()` על אובייקט האיטרטור שקיבלה (שהוא אובייקט ה-`counter` שלנו).
- קריאה ראשונה ל-`__next__`: שיטת ה-`counter.__next__()` נקראת. `self.current` הוא 0, שהוא פחות מ-`self.max_num` (3). הקוד מגדיל את `self.current` ל-1 ומחזיר אותו. לולאת ה-`for` מקצה ערך זה למשתנה `number`, וגוף הלולאה (`print(...)`) מבוצע.
- קריאה שנייה ל-`__next__`: הלולאה ממשיכה. `__next__` נקראת שוב. `self.current` הוא 1. הוא מוגדל ל-2 ומוחזר.
- קריאה שלישית ל-`__next__`: `__next__` נקראת שוב. `self.current` הוא 2. הוא מוגדל ל-3 ומוחזר.
- קריאה אחרונה ל-`__next__`: `__next__` נקראת פעם נוספת. עתה, `self.current` הוא 3. התנאי `self.current < self.max_num` הוא שקרי. בלוק ה-`else` מבוצע, ו-`StopIteration` נזרק.
- סיום הלולאה: לולאת ה-`for` מתוכננת ללכוד את חריגת ה-`StopIteration`. כאשר היא עושה זאת, היא יודעת שהאיטרציה הסתיימה ומסתיימת בחן. התוכנית ממשיכה לבצע כל קוד שאחרי הלולאה.
שימו לב לפרט מפתח: אם תנסו להריץ את לולאת ה-`for` על אותו אובייקט `counter` שוב, זה לא יעבוד. האיטרטור מוצה. `self.current` הוא כבר 3, ולכן כל קריאה עוקבת ל-`__next__` תזרוק מיד `StopIteration`. זוהי תוצאה של היות האובייקט שלנו איטרטור של עצמו.
מושגי איטרטור מתקדמים ויישומים בעולם האמיתי
מונים פשוטים הם דרך מצוינת ללמוד, אך הכוח האמיתי של פרוטוקול האיטרטור זורח כאשר הוא מיושם על מבני נתונים מורכבים ומותאמים אישית.
הבעיה בשילוב אובייקט בר-איטרציה ואיטרטור
בדוגמת `CountUpTo` שלנו, המחלקה הייתה גם אובייקט בר-איטרציה וגם האיטרטור. זה פשוט אבל יש לו חיסרון משמעותי: האיטרטור המתקבל מתכלה. ברגע שאתם עוברים עליו בלולאה, הוא נגמר.
קוד:
counter = CountUpTo(2)
print("First iteration:")
for num in counter: print(num) # עובד בסדר גמור
print("\nSecond iteration:")
for num in counter: print(num) # לא מדפיס כלום!
זה קורה מכיוון שהמצב (`self.current`) מאוחסן על האובייקט עצמו. לאחר הלולאה הראשונה, `self.current` הוא 2, וכל קריאה נוספת ל-`__next__` פשוט תזרוק `StopIteration`. התנהגות זו שונה מרשימת פייתון סטנדרטית, שעליה ניתן לבצע איטרציה מספר פעמים.
תבנית חזקה יותר: הפרדת אובייקט בר-איטרציה מהאיטרטור
כדי ליצור אובייקטים ברי-איטרציה הניתנים לשימוש חוזר כמו האוספים המובנים של פייתון, השיטה המומלצת היא להפריד את שני התפקידים. אובייקט המיכל יהיה האובייקט בר-איטרציה (iterable), והוא ייצור אובייקט איטרטור חדש ורענן בכל פעם ששיטת ה-`__iter__` שלו נקראת.
בואו נבצע רה-פקטור לדוגמה שלנו לשתי מחלקות: `Sentence` (האובייקט בר-איטרציה) ו-`SentenceIterator` (האיטרטור).
קוד:
class SentenceIterator:
"""האיטרטור האחראי על המצב ועל ייצור הערכים."""
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
# איטרטור חייב להיות גם אובייקט בר-איטרציה, המחזיר את עצמו.
return self
class Sentence:
"""מחלקה מיכל של אובייקט בר-איטרציה."""
def __init__(self, text):
# המיכל מכיל את הנתונים.
self.words = text.split()
def __iter__(self):
# בכל פעם ש-__iter__ נקראת, היא יוצרת אובייקט איטרטור חדש.
return SentenceIterator(self.words)
# איך להשתמש בזה
my_sentence = Sentence('This is a test')
print("First iteration:")
for word in my_sentence:
print(word)
print("\nSecond iteration:")
for word in my_sentence:
print(word)
כעת, זה עובד בדיוק כמו רשימה! בכל פעם שלולאת ה-`for` מתחילה, היא קוראת ל-`my_sentence.__iter__()`, שיוצר מופע חדש לגמרי של `SentenceIterator` עם מצב משלו (`self.index = 0`). זה מאפשר איטרציות מרובות ועצמאיות על אותו אובייקט `Sentence`. תבנית זו חזקה הרבה יותר וכך מיושמים אוספים פנימיים של פייתון.
דוגמה: איטרטורים אינסופיים
איטרטורים לא חייבים להיות סופיים. הם יכולים לייצג רצף נתונים אינסופי. כאן טמון היתרון הגדול של טבעם העצלני, 'אחד בכל פעם'. בואו ניצור איטרטור לרצף אינסופי של מספרי פיבונאצ'י.
קוד:
class FibonacciIterator:
"""מייצר רצף אינסופי של מספרי פיבונאצ'י."""
def __init__(self):
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
result = self.a
self.a, self.b = self.b, self.a + self.b
return result
# איך להשתמש בזה - זהירות: לולאה אינסופית ללא תנאי עצירה!
fib_gen = FibonacciIterator()
for i, num in enumerate(fib_gen):
print(f"Fibonacci({i}): {num}")
if i >= 10: # עלינו לספק תנאי עצירה
break
איטרטור זה לעולם לא יזרוק `StopIteration` בעצמו. זוהי אחריותו של הקוד הקורא לספק תנאי (כמו פקודת `break`) כדי לסיים את הלולאה. תבנית זו נפוצה בזרמי נתונים, לולאות אירועים וסימולציות נומריות.
פרוטוקול האיטרטור במערכת האקולוגית של פייתון
הבנת `__iter__` ו-`__next__` מאפשרת לכם לראות את השפעתם בכל מקום בפייתון. זהו הפרוטוקול המאחד שגורם לתכונות רבות כל כך של פייתון לעבוד יחד בצורה חלקה.
איך לולאות `for` *באמת* עובדות
דנו בכך במרומז, אך בואו נבהיר זאת במפורש. כאשר פייתון נתקל בשורה זו:
`for item in my_iterable:`
הוא מבצע את הצעדים הבאים מאחורי הקלעים:
- הוא קורא ל-`iter(my_iterable)` כדי לקבל איטרטור. זה, בתורו, קורא ל-`my_iterable.__iter__()`. בואו נקרא לאובייקט המוחזר `iterator_obj`.
- הוא נכנס ללולאת `while True` אינסופית.
- בתוך הלולאה, הוא קורא ל-`next(iterator_obj)`, אשר בתורו קורא ל-`iterator_obj.__next__()`.
- אם `__next__` מחזיר ערך, הוא מוקצה למשתנה `item`, והקוד שבתוך בלוק לולאת ה-`for` מבוצע.
- אם `__next__` זורק חריגת `StopIteration`, לולאת ה-`for` לוכדת חריגה זו ויוצאת מלולאת ה-`while` הפנימית שלה. האיטרציה הושלמה.
הבנות (Comprehensions) וביטויי גנרטור (Generator Expressions)
הבנות רשימה, קבוצה ומילון כולן מופעלות על ידי פרוטוקול האיטרטור. כשאתם כותבים:
`squares = [x * x for x in range(10)]`
פייתון מבצע למעשה איטרציה על אובייקט ה-`range(10)`, מקבל כל ערך, ומבצע את הביטוי `x * x` כדי לבנות את הרשימה. הדבר נכון גם לגבי ביטויי גנרטור, שהם שימוש ישיר עוד יותר באיטרציה עצלה:
`lazy_squares = (x * x for x in range(1000000))`
זה לא יוצר רשימה של מיליון פריטים בזיכרון. זה יוצר איטרטור (באופן ספציפי, אובייקט גנרטור) שיחשב את הריבועים אחד אחד, כשאתם עוברים עליו בלולאה.
גנרטורים: הדרך הפשוטה יותר ליצור איטרטורים
בעוד שיצירת מחלקה מלאה עם `__iter__` ו-`__next__` מעניקה לכם שליטה מרבית, זה יכול להיות מילולי עבור מקרים פשוטים. פייתון מספק תחביר תמציתי הרבה יותר ליצירת איטרטורים: גנרטורים.
גנרטור הוא פונקציה המשתמשת במילת המפתח `yield`. כאשר אתם קוראים לפונקציית גנרטור, היא אינה מריצה את הקוד. במקום זאת, היא מחזירה אובייקט גנרטור, שהוא איטרטור מן המניין.
בואו נשכתב את דוגמת `CountUpTo` שלנו כגנרטור:
קוד:
def count_up_to_generator(max_num):
"""פונקציית גנרטור שמפיקה מספרים מ-1 ועד max_num."""
print("Generator started...")
current = 1
while current <= max_num:
yield current # עוצר כאן ושולח ערך בחזרה
current += 1
print("Generator finished.")
# איך להשתמש בזה
counter_gen = count_up_to_generator(3)
for number in counter_gen:
print(f"For loop received: {number}")
תראו כמה פשוט זה! מילת המפתח `yield` היא הקסם כאן. כאשר נתקלים ב-`yield`, מצב הפונקציה קופא, הערך נשלח למתקשר, והפונקציה מושהית. בפעם הבאה ש-`__next__` נקרא על אובייקט הגנרטור, הפונקציה ממשיכה את הביצוע בדיוק מהמקום שבו הפסיקה, עד שהיא נתקלת ב-`yield` נוסף או שהפונקציה מסתיימת. כאשר הפונקציה מסתיימת, חריגת `StopIteration` נזרקת אוטומטית עבורכם.
מתחת למכסה המנוע, פייתון יצר אוטומטית אובייקט עם שיטות `__iter__` ו-`__next__`. בעוד שגנרטורים הם לעיתים קרובות הבחירה הפרקטית יותר, הבנת הפרוטוקול הבסיסי חיונית לניפוי באגים, תכנון מערכות מורכבות והבנת האופן שבו מכניקות הליבה של פייתון פועלות.
שיטות עבודה מומלצות ומלכודות נפוצות
בעת יישום פרוטוקול האיטרטור, זכרו את ההנחיות הבאות כדי למנוע שגיאות נפוצות.
שיטות עבודה מומלצות
- הפרדת אובייקט בר-איטרציה ואיטרטור: עבור כל אובייקט מיכל שאמור לתמוך במעברים מרובים, יש ליישם תמיד את האיטרטור במחלקה נפרדת. שיטת ה-`__iter__` של המיכל צריכה להחזיר מופע חדש של מחלקת האיטרטור בכל פעם.
- לזרוק תמיד `StopIteration`: שיטת ה-`__next__` חייבת לזרוק באמינות `StopIteration` כדי לאותת על הסיום. שכחת זאת תוביל ללולאות אינסופיות.
- איטרטורים צריכים להיות ברי-איטרציה: שיטת ה-`__iter__` של איטרטור צריכה תמיד להחזיר את `self`. זה מאפשר להשתמש באיטרטור בכל מקום שבו מצפים לאובייקט בר-איטרציה.
- העדיפו גנרטורים לפשטות: אם לוגיקת האיטרטור שלכם פשוטה וניתנת לביטוי כפונקציה בודדת, גנרטור הוא כמעט תמיד נקי וקריא יותר. השתמשו במחלקת איטרטור מלאה כאשר אתם צריכים לשייך מצב או שיטות מורכבים יותר לאובייקט האיטרטור עצמו.
מלכודות נפוצות
- בעיית האיטרטור המתכלה: כפי שנדון, שימו לב שכאשר אובייקט הוא האיטרטור של עצמו, ניתן להשתמש בו רק פעם אחת. אם אתם צריכים לבצע איטרציה מספר פעמים, עליכם ליצור מופע חדש או להשתמש בתבנית אובייקט בר-איטרציה/איטרטור המופרדים.
- שכחת מצב: שיטת ה-`__next__` חייבת לשנות את המצב הפנימי של האיטרטור (לדוגמה, הגדלת אינדקס או קידום מצביע). אם המצב אינו מתעדכן, `__next__` יחזיר את אותו ערך שוב ושוב, מה שעלול לגרום ללולאה אינסופית.
- שינוי אוסף במהלך איטרציה: מעבר על אוסף בלולאה תוך כדי שינויו (לדוגמה, הסרת פריטים מרשימה בתוך לולאת ה-`for` שעוברת עליה) עלול להוביל להתנהגות בלתי צפויה, כגון דילוג על פריטים או זריקת שגיאות בלתי צפויות. בדרך כלל בטוח יותר לעבור על עותק של האוסף אם אתם צריכים לשנות את המקור.
סיכום
פרוטוקול האיטרטור, עם שיטות ה-`__iter__` ו-`__next__` הפשוטות שלו, הוא אבן היסוד של האיטרציה בפייתון. הוא עדות לפילוסופיית העיצוב של השפה: העדפת ממשקים פשוטים ועקביים המאפשרים התנהגויות חזקות ומורכבות. על ידי מתן חוזה אוניברסלי לגישה לנתונים סדרתיים, הפרוטוקול מאפשר ללולאות `for`, להבנות (comprehensions) ואינספור כלים אחרים לעבוד בצורה חלקה עם כל אובייקט שבוחר לדבר בשפתו.
על ידי שליטה בפרוטוקול זה, פתחתם את היכולת ליצור אובייקטים דמויי רצף משלכם שהם אזרחים ממדרגה ראשונה במערכת האקולוגית של פייתון. אתם יכולים כעת לכתוב מחלקות שהן יעילות יותר בזיכרון על ידי עיבוד נתונים בצורה עצלה, אינטואיטיביות יותר על ידי השתלבות נקייה עם תחביר פייתון סטנדרטי, ובסופו של דבר, חזקות יותר. בפעם הבאה שתכתבו לולאת `for`, קחו רגע להעריך את הריקוד האלגנטי של `__iter__` ו-`__next__` המתרחש ממש מתחת לפני השטח.