תדמיין שאתה עובד על פרויקט מדעי נתונים, והמטלה שלך היא לעבד סט נתונים כה גדול שטעינתו לזיכרון גורמת למחשב שלך להתקע. או שאתה מתמודד עם רצפיות אינסופיות, כמו זרם נתונים חי, שבהם אי אפשר לאחסן הכל בו זמנית. אלה הם האתגרים שגורמים למדעני נתונים להשתמש בקומקום לקפה – ולפעמים גם לכפתור האיפון.
במאמר זה, נלמד על מחוללי Python, וכיצד ניתן להשתמש בהם כדי לפשט את הקוד שלך. הרעיון הזה מחייב קצת תרגול, לכן, אם אתה חדש ב-Python ונתקעת קצת במאמר זה, תנסה את הקורס המבואי ל-Python שלנו כדי לבנות בסיס חזק.
מהם מחוללי Python?
בליבם, מחוללי Python הם סוג מיוחד של פונקציה או אפילו ביטוי קומפקטי המייצר רצף של ערכים באופן עצל. חשוב לחשוב על מחוללים כמו רצפי הנתונים במפעל: במקום לשים את כל המוצרים במקום אחד ולהיגמר מקום, אתה מעבד כל פריט כשהוא מורד מהקו. זה הופך את המחוללים ליעילים מבחינת זיכרון ולהרחבה טבעית של פרוטוקול ה־iterator
של Python, שמשמש בסיס לכלים מובנים רבים ב־Python כמו לולאות ואיגודים.
הקסם שבמחוללים נמצא במילת המפתח yield
. להבדיל מ־return
, שמחזיר ערך יחיד ויוצא מהפונקציה, yield
מייצר ערך, משהה את ביצוע הפונקציה, ושומר על מצבה. כאשר מחולל מופעל שוב, הוא ממשיך מאיפה שעצר.
לדוגמה, דמיינו שאתם קוראים קובץ לוג ענק שורה אחרי שורה. מחולל יכול לעבד כל שורה כפי שנקראת בלי לטעון את כל הקובץ לזיכרון. ה"הערכה עצלה" הזו מפרידה את המחוללים מפונקציות מסורתיות ועושה אותם כלי עיקרי למשימות שדורשות ביצוע יעיל.
דוגמה בסיסית למחולל Python
בואו נתרגל קצת כדי להבין את הרעיון. הנה פונקציית מחולל שמייצרת את המספרים הראשוניים n
.
def generate_integers(n): for i in range(n): yield i # עצירה כאן ומחזיר i # בשימוש במחולל for num in generate_integers(5): print(num)
0 1 2 3 4
יצרתי תמונה ויזואלית כדי לעזור לך לראות מה קורה מתחת לקפואה:
תחביר ותבניות של מחולל Python
מחוללים ניתן ליישם בכמה דרכים. אמנם, ישנן שתי דרכים עיקריות: פונקציות מחוללות וביטויי מחולל.
פונקציות מחוללות
פונקציית מחולל מוגדרת כמו פונקציה רגילה אך משתמשת במילת המפתח yield
במקום return.
כאשר נקראת, היא מחזירה אובייקט מחולל שניתן לעבור עליו.
def count_up_to(n): count = 1 while count <= n: yield count count += 1 # בשימוש במחולל counter = count_up_to(5) for num in counter: print(num)
1 2 3 4 5
מהדוגמה לעיל, אנו רואים כי כאשר הפונקציה count_up_to
מוזנת, היא מחזירה אובייקט מחולל. בכל פעם שלולאת for מבקשת ערך, הפונקציה רצה עד שהיא מגיעה ל־yield
, מייצרת את הערך הנוכחי של count
ושומרת את מצבה בין איטרציות כך שהיא יכולה להמשיך בדיוק מאיפה שהפסיקה.
ביטויי מחולל
ביטויי מחולל הם דרך קומפקטית ליצירת מחוללים. הם דומים לרצפיות של רשימות אך עם סוגריים עגולים במקום סוגריים מרובעים.
# רצפיות של רשימה (הערכה מיידית) squares_list = [x**2 for x in range(5)] # [0, 1, 4, 9, 16] # ביטוי מחולל (הערכה עצלנית) squares_gen = (x**2 for x in range(5)) # שימוש במחולל for square in squares_gen: print(square)
0 1 4 9 16
אז, מה ההבדל בין רכיב רשימה ובין ביטוי מחולל? רכיב הרשימה יוצר את רשימת המלאה בזיכרון, בעוד שביטוי המחולל מייצר ערכים באחד על כל מופע, חוסך בזיכרון. אם אינך מוכר ברכיבי רשימה, תוכל לקרוא עליהם במדריך שלנו Python List Comprehension Tutorial.
המחולל של Python לעומת תצורה
המחזירים המסורתיים ב־Python דרשו קלאסים עם __iter__()
ו־__next__()
מתודות מפורשות, שכללו הרבה קוד תבניתי וניהול סטטוס ידני, בעוד שפונקציות המחולל מפשטות את התהליך על ידי שמירה אוטומטית על הסטאטוס והסרת הצורך בפונקציות אלו—כפי שמוצג על ידי פונקציה פשוטה שמחזירה את ריבוע המספרים עד ל־n
.
למה אנו משתמשים במחוללי Python
בהסברת מהם מחוללי Python, העברתי גם חלק מהרעיון של מדוע הם משמשים. בסעיף זה, אני רוצה להעמיק קצת יותר. מחוללים אינם רק תכונת Python מפוארת, אלא בפועל הם מפתרים בעיות אמיתיות.
יעילות בזיכרון
בניגוד לרשימות או מערכי נתונים, שמאחסנים את כל האיברים שלהם בזיכרון בו זמן כלשהו, מחוללי יוצרים ערכים על הסף, ולכן הם מחזיקים רק פריט אחד בזיכרון בכל פעם.
לדוגמה, נשים לב להבדל בין range()
של Python 2 וְ-xrange()
:
-
range()
יצר רשימה בזיכרון, דבר שיכול להיות בעיה עבור טווחים גדולים. -
xrange() התנהג כמחולל, מייצר ערכים באופן פרסני.
מאחר וההתנהגות של xrange() הייתה יותר שימושית, כעת, ב-Python 3, range() גם מתנהג כמחולל, ולכן הוא ממנה את העומס הזיכרון שנדרש לאחסון כל הערכים בו זמנית.
כדי להדגים את הרעיון, בואו נשווה בין שימוש בזיכרון בעת יצירת רצף של 10 מיליון מספרים:
import sys # בשימוש ברשימה numbers_list = [x for x in range(10_000_000)] print(f"Memory used by list: {sys.getsizeof(numbers_list) / 1_000_000:.2f} MB") # בשימוש במחולל numbers_gen = (x for x in range(10_000_000)) print(f"Memory used by generator: {sys.getsizeof(numbers_gen)} bytes")
Memory used by list: 89.48 MB Memory used by the generator: 112 bytes
כפי שאתה יכול לראות, המחולל משתמש בכמעט כל זיכרון בהשוואה לרשימה, וההבדל הזה משמעותי.
שיפורי ביצועים
בזכות הערכה עצלה, הערכים מחושבים רק כאשר נדרשים. זה אומר שניתן להתחיל לעבד נתונים מיידית מבלי להמתין לסיום יצירת כל הרצפים.
לדוגמה, דמיינו לחשב את סכום ריבועי המספרים הראשוניים במיליון:
# בשימוש ברשימה (הערכה נאמנה) sum_of_squares_list = sum([x**2 for x in range(1_000_000)]) # בשימוש במחולל (הערכה עצלה) sum_of_squares_gen = sum(x**2 for x in range(1_000_000))
אולם, למרות שני השיטות נותנות את אותו התוצאה, גרסת המחולל ממנעת יצירת רשימה עצומה, ולכן אנו מקבלים את התוצאה במהירות יותר.
פשטות ונגישות
מחוללי קוד פשוטים מאפשרים ליישם איטרטורים על ידי הסרת קוד תבניתי. ניתן להשוות בין איטרטור מבוסס קלאס בערך פונקציית מחולל:
זהו האיטרטור מבוסס קלאס:
class SquaresIterator: def __init__(self, n): self.n = n self.current = 0 def __iter__(self): return self def __next__(self): if self.current >= self.n: raise StopIteration result = self.current ** 2 self.current += 1 return result # שימוש squares = SquaresIterator(5) for square in squares: print(square)
וזוהי פונקציית המחולל:
def squares_generator(n): for i in range(n): yield i ** 2 # שימוש squares = squares_generator(5) for square in squares: print(square)
גרסת המחולל קצרה יותר, קלה יותר לקריאה, ואינה דורשת קוד תבניתי. זהו דוגמה מושלמת לפילוסופיה של פייתון: פשוט זה טוב יותר.
טיפול ברצפיות אינסופיות
לסיכום, רוצה להגיד שמחוללים מתאים באופן ייחודי לייצוג רצפיות אינסופיות, משהו שפשוט בלתי אפשרי עם רשימות. לדוגמה, נשקול את רצפיית הפיבונצ'י:
def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b # שימוש fib = fibonacci() for _ in range(10): print(next(fib))
0 1 1 2 3 5 8 13 21 34
מחולל זה יכול ליצור מספרי פיבונאצ'י ללא הפסקה בלעדי ניצול זיכרון. דוגמאות נוספות כוללות עיבוד זרמי נתונים חיים או עבודה עם נתוני סדרות זמן.
מושגים מתקדמים של מחוללי Python
כעת, בואו נסתכל על רעיונות קשים יותר. בסעיף זה, נחקור איך לחבר מחוללים ולהשתמש בשיטות מיוחדות של מחוללים כמו .send()
, .throw()
, ו-.close()
.
שרשור מחוללים יחד
ניתן לשרשר מחוללים. ניתן להמיר, לסנן ולעבד נתונים בצורה מודולרית על ידי שרשור מחוללים.
נניח שיש לך רצפיות אינסופיות של מספרים וברצונך להכפיל את כל מספר ולסנן תוצאות אי-זוגיות:
def infinite_sequence(): num = 0 while True: yield num num += 1 def square_numbers(sequence): for num in sequence: yield num ** 2 def filter_evens(sequence): for num in sequence: if num % 2 == 0: yield num # לחבר את המחוללים numbers = infinite_sequence() squared = square_numbers(numbers) evens = filter_evens(squared) # להדפיס את 10 הריבועים הזוגיים הראשונים for _ in range(10): print(next(evens))
0 4 16 36 64 100 144 196 256 324
התהליך כולל את הפונקציה infinite_sequence
שיוצרת מספרים ללא הפסקה, בזמן שהפונקציה square_numbers
מחזירה את הריבוע של כל מספר, ולאחר מכן filter_evens
מסנן מספרים אי-זוגיים כדי לייצר ריבועים זוגיים בלבד.המסלול המקצועי שלנו למפתח פייתון מתעסק בסוגים כאלה של דברים, כך שתוכל לראות איך לבנות ולאתר באגים בצינורות מורכבים באמצעות יוצרים, כמו גם מתיבות ורשימות תמליליות. ושיטות יוצרים מיוחדות.
היוצרים מגיעים עם שיטות מתקדמות המאפשרות תקשורת דו-כיוונית וסיום מבוקר.
השיטה .send()
מאפשרת לך להעביר ערכים חזרה אל היוצר, ומשנה אותו לקורוטין. זה מועיל ליצירת יוצרים אינטראקטיביים או בעלי מצב.
def accumulator(): total = 0 while True: value = yield total if value is not None: total += value #שימוש בייצור acc = accumulator() next(acc) #התחל את הייצור print(acc.send(10)) #פלט: 10 print(acc.send(5)) #פלט: 15 print(acc.send(20)) #פלט: 35
כך זה עובד:
-
הייצור מתחיל עם
next(acc)
לאתחול אותו. -
כל קריאה ל־
.send(value)
מעבירה ערך לייצור, שמוקצה ל־value
בהצהרת ה־yield
. -
הגנרטור מעדכן את מצבו (
total
) ומחזיר את התוצאה החדשה.
throw()
השימוש בשיטת .throw()
מאפשר לך לזרוק חריגה בתוך הגנרטור, מה שיכול להיות מועיל לטיפול בשגיאות או לשינון תנאים מסוימים.
def resilient_generator(): try: for i in range(5): yield i except ValueError: yield "Error occurred!" # שימוש בגנרטור gen = resilient_generator() print(next(gen)) # פלט: 0 print(next(gen)) # פלט: 1 print(gen.throw(ValueError)) # פלט: "שגיאה התרחשה!"
הנה כיצד פעולת זו עובדת:
-
המחולל רגילה רצה עד ש-
.throw()
נקרא. -
החריגה מופעלת בתוך המחולל, שיכול לטפל בה באמצעות בלוק
try-except
.
close()
הפעולה .close()
מפסיקה מחולל על ידי זריקת חריגת GeneratorExit
. זה שימושי לניקוי משאבים או עצירת מחוללים אינסופיים.
def infinite_counter(): count = 0 try: while True: yield count count += 1 except GeneratorExit: print("Generator closed!") # בשימוש במחולל counter = infinite_counter() print(next(counter)) # פלט: 0 print(next(counter)) # פלט: 1 counter.close() # פלט: "מחולל סגור!"
וכך זה עובד:
-
המחולל רץ עד שנקרא
.close()
. -
החריג
GeneratorExit
מתעורר, מאפשר למחולל לנקות או לשלוח הודעה לקובץ לוג לפני הסיום.
יישומים בעולם האמיתי במדע הנתונים
אני מקווה שאתה מתחיל להעריך שמחוללים הם שימושיים. בסעיף זה, אני הולך לנסות להדגיש מקרי שימוש כדי שתוכל לדמיין איך הם בפועל עובדים עבורך ביומיום.
עיבוד סטים נתונים גדולים
אחת האתגרים הנפוצים ביותר במדע הנתונים הוא העבודה עם סטים נתונים גדולים מדי כדי להתאים לזיכרון. מחוללים מספקים דרך לעבד נתונים כאלו שורה אחרי שורה.
דמיינו שיש לכם קובץ CSV בגודל של 10 ג'יגה־בייט המכיל נתוני מכירות ואתם צריכים לסנן רשומות עבור אזור מסוים. הנה כיצד ניתן להשתמש בצינור מחוללים כדי להשיג זאת:
import csv def read_large_csv(file_path): """ Generator to read a large CSV file line by line.""" with open(file_path, mode="r") as file: reader = csv.DictReader(file) for row in reader: yield row def filter_by_region(data, region): """ Generator to filter rows by a specific region.""" for row in data: if row["Region"] == region: yield row # צינור מחוללים file_path = "sales_data.csv" region = "North America" data = read_large_csv(file_path) filtered_data = filter_by_region(data, region) # מעבד את הנתונים שעברו את הסינון for record in filtered_data: print(record)
כך זה עובד:
-
read_large_csv
קורא את הקובץ שורה אחר שורה, מחזיר כל שורה כמילון. -
filter_by_region
מסנן שורות בהתאם לאזור המצוין. -
הצינור מעבד נתונים באופן תדרותי, ממנע עומס בזיכרון.
הגישה הזו משפרת את זרימות ה-ETL ו-ELT, בהן נדרש לנקות ולהמיר נתונים לפני הניתוח. תראו דברים כאלה בקורס ETL ו-ELT בפייתון שלנו.
זרמים וצינורות
לעיתים נתונים מגיעים כזרם רציף. דמיינו נתוני חיישן, מזרים חיים או מדיה חברתית.
נניח שאתה עובד עם התקני IoT שיוצרים קריאות טמפרטורה בכל שניה. ברצונך לחשב את הטמפרטורה הממוצעת על חלון גליל של 10 קריאות:
def sensor_data_stream(): """Simulate an infinite stream of sensor data.""" import random while True: yield random.uniform(0, 100) # מדמה נתוני חיישן def sliding_window_average(stream, window_size): """ Calculate the average over a sliding window of readings.""" window = [] for value in stream: window.append(value) if len(window) > window_size: window.pop(0) if len(window) == window_size: yield sum(window) / window_size # צינורי יצירה sensor_stream = sensor_data_stream() averages = sliding_window_average(sensor_stream, window_size=10) # הדפס את הממוצע בכל שניה for avg in averages: print(f"Average temperature: {avg:.2f}")
כאן ההסבר:
-
sensor_data_stream
מדמה זרם אינסופי של קריאות חיישן. -
sliding_window_average
שומר חלון נע עם הקריאות האחרונות בעשרה ומחזיר את ממוצען. -
הצינור מעבד נתונים בזמן אמת, ולכן מושלם למעקב ולניתוח.
מקרים נוספים של שימוש
מחוללים גם משמשים במצבים שבהם גודל הנתונים אינו ניחוך או כאשר הם נמצאים בהמשך ישיר/אינסופי.
חיפוש ברשת
כאשר אתה חוקר אתרים, לעתים תמיד אינך יודע כמה עמודים או פריטים יהיה עליך לעבד. מחוללים מאפשרים לך לטפל באי צפייה זו בצורה אלגנטית:
def scrape_website(url): """ Generator to scrape a website page by page.""" while url: # סימולציה של קבלת וניתוח עמוד print(f"Scraping {url}") data = f"Data from {url}" yield data url = get_next_page(url) # פונקציה היפותטית לקבלת העמוד הבא # שימוש scraper = scrape_website("https://example.com/page1") for data in scraper: print(data)
משימות סימולציה
בסימולציות, כמו שיטות מונטה קרלו או פיתוח משחקים, מחוללים יכולים לייצג רצפים אינסופיים או דינמיים:
def monte_carlo_simulation(): """ Generator to simulate random events for Monte Carlo analysis.""" import random while True: yield random.random() # שימוש simulation = monte_carlo_simulation() for _ in range(10): print(next(simulation))
מדדי זיכרון ומהירות
בגלל איך שהם פועלים, מחוללים מתקדמים בתרחישים שבהם יעילות הזיכרון חשובה, אך (אתה עשוי להיות בהפתעה לדעת) לא תמיד הם האפשרות המהירה ביותר. בוא נשווה בין מחוללים לרשימות כדי להבין את הפשרותיהם.
בעבר, הראינו כיצד מחוללים הם טובים יותר מרשימות מבחינת זיכרון. זו הייתה החלק בו השווינו שימוש בזיכרון בעת יצירת רצפי 10 מיליון מספרים. נעשה כעת השוואת מהירות אחרת:
import time # רכיב של רשימה start_time = time.time() sum([x**2 for x in range(1_000_000)]) print(f"List comprehension time: {time.time() - start_time:.4f} seconds") # ביטוי של מחולל start_time = time.time() sum(x**2 for x in range(1_000_000)) print(f"Generator expression time: {time.time() - start_time:.4f} seconds")
List comprehension time: 0.1234 seconds Generator expression time: 0.1456 seconds
בזמן שמחולל חוסך בזיכרון, במקרה זה, זה בעצם איטי יותר מהרשימה. זה בגלל שעבור קבוצת הנתונים הזו הקטנה, יש הוצאת עלות של השהייה ושיבוץ של הביצוע.
ההבדל בביצועים הוא בלתי ניתן לגילוי עבור קבוצות נתונים קטנות, אך עבור קבוצות נתונים גדולות, חיסכון הזיכרון של מחוללים נוטה להערים על קלטת המהירות הקלה.
בעיות שעלולות להתעורר
לבסוף, בואו נסתכל על כמה טעויות או בעיות נפוצות:
מחוללים הם מוגמרים
פעם שמחולל נגמר, לא ניתן להשתמש בו שוב. תצטרך ליצור אותו מחדש אם ברצונך לעבור עליו שוב.
gen = (x for x in range(5)) print(list(gen)) # פלט: [0, 1, 2, 3, 4] print(list(gen)) # פלט: [] (המחולל נגמר)
הערכה עצלה יכולה להיות מסובכת
מאחר שמחוללים מייצרים ערכים על פי דרישה, שגיאות או אפקטים צד לא ייראו עד שהמחולל ייעבר עליו.
יש אפשרות לעבור על השימוש במחוללים ביתרון
עבור קבוצות נתונים קטנות או משימות פשוטות, העלות הנוספת של שימוש במחולל עשויה שלא להיות שווה לחיסכון בזיכרון. שקול את הדוגמה הזו שבה אני מממש נתונים למטרת מספר עברות.
# ביטוי מחולל gen = (x**2 for x in range(10)) # מממש אל רשימה squares = list(gen) # השתמש ברשימה print(sum(squares)) # פלט: 285 print(max(squares)) # פלט: 81
בחירת המקרים בהם להשתמש במחוללים
לסיכום, אספק כמה כללים כלליים מאוד לשימוש בגנרטורים. יש להשתמש בהם עבור:
- סט נתונים גדול: יש להשתמש בגנרטורים כאשר עובדים עם סטים שגדולים מדי כדי להכיל בזיכרון.
- רצפיות אינסופיות: יש להשתמש בגנרטורים לייצוג של רצפיות אינסופיות, כמו זרמי נתונים חיים או סימולציות.
- צינורות: יש להשתמש בגנרטורים כדי לבנות צינורות עיבוד נתונים מודולריים שמעבדים ומסננים נתונים באופן תוספתי.
מתי להמיר נתונים לרשימה (להמיר לרשימה)
- סט נתונים קטן: אל תשתמש במחוללים אם הזיכרון אינו בעיה ואתה צריך גישה מהירה לכל האיברים; במקום זאת, השתמש ברשימה.
- מספר רב של איטרציות: אל תשתמש במחוללים אם נדרשת לעבור על אותם נתונים פעמים רבות; במקום זאת, המציא אותם לרשימה כדי למנוע שיבוש של המחולל.
מסקנה ונקודות מרכזיות לקחויות
במהלך מאמר זה, חקרנו כיצד מחוללים עשויים לסייע לך בהתמודדות עם אתגרים בעולם המדעים על מנת לעבד סט נתונים גדולים ולבניית צינורות נתונים בזמן אמת. המשך התרגול. הדרך הכי טובה לשלוט במחוללים היא להשתמש בהם בעבודה שלך. כהתחלה, נסה להחליף ביטוי רשימה בביטוי מחולל או לשפר לולאה לפונקצית מחולל.
כאשר תשלט ביסודות, תוכל לחקור נושאים חדשים ומתקדמים יותר שמבוססים על המושג של המחולל:
-
קורוטינות: השתמש ב־
.send()
וב־.throw()
כדי ליצור מחוללים שיכולים לקבל ולעבד נתונים, מאפשרים תקשורת דו־כיוונית. -
תכנות אסינכרוני: שלבו מחוללים עםספריית asyncio של פייתון כדי לבנות יישומים יעילים שאינם חוסמים.
-
קונקורנסיה: למדו כיצד מחוללים יכולים ליישם ריבוי משימות מרותק וקונקורנס קל משקל.
המשך ללמוד ותהפוך למומחה. קח את המסלול המקצועי פיתון מפתח שלנו או את מסלול היכולת תכנות בפייתון היום. לחץ על הקישור למטה כדי להתחיל.