Pure Functions — פונקציות טהורות ו-Side Effects
- להסביר מה זו Pure Function — אותו קלט, תמיד אותה תוצאה, בלי לשנות שום דבר בחוץ
- לזהות Side Effects בקוד — global, שינוי רשימה שהתקבלה, הדפסה, כתיבה לקובץ
- להפוך פונקציה שמשנה את הקלט שלה לגרסה טהורה שמחזירה ערך חדש
- להסביר למה פונקציות טהורות קלות יותר לבדיקה ולשימוש חוזר
מה נלמד?
- מה זו Pure Function (פונקציה טהורה).
- מה זה Side Effect (תופעת לוואי) — ואילו סוגים קיימים.
- המלכודת של שינוי רשימה שהתקבלה כפרמטר.
- איך כותבים גרסה טהורה של פונקציה.
למה צריך את זה?
הנה פונקציה שנראית תמימה:
total = 0
def add_to_total(price):
global total
total += price
return total
print(add_to_total(10))
print(add_to_total(10))פלט:
10
20אותה קריאה בדיוק — ותוצאה אחרת. הפונקציה תלויה במשתנה חיצוני וגם משנה אותו. כדי לדעת מה היא תחזיר, צריך לדעת את כל ההיסטוריה של התוכנית. קשה לבדוק אותה, וקשה לסמוך עליה.
הסבר מהבסיס
מה זו Pure Function
פונקציה טהורה עומדת בשני תנאים:
- אותו קלט ← תמיד אותה תוצאה. התוצאה תלויה רק בפרמטרים — לא במשתנים גלובליים, לא בשעה, לא בקלט מהמשתמש.
- אין Side Effects. היא לא משנה שום דבר מחוץ לה. כל מה שהיא “מוציאה” יוצא דרך
return.
מה נחשב Side Effect
| Side Effect | דוגמה |
|---|---|
| שינוי משתנה גלובלי | global total ואז total += price |
| שינוי רשימה או מילון שהתקבלו | prices[i] = ... על הרשימה שהגיעה כפרמטר |
| קלט ופלט מול המשתמש | print(...), input(...) |
| כתיבה לקובץ או למסד נתונים | open(..., "w"), INSERT |
פונקציה טהורה היא כמו מסחטת מיץ: מכניסים 3 תפוזים — יוצאת כוס מיץ. תמיד אותה כמות, והמסחטה לא נוגעת בשום דבר אחר במטבח. פונקציה לא טהורה היא מסחטה שלפעמים גם מזיזה את המקרר — ופתאום משהו אחר בבית לא עובד, ואין לכם מושג למה.
תחביר
אין תחביר מיוחד — זו דרך לכתוב פונקציות:
# pure: result depends only on the parameters
def add_price(total, price):
return total + price
# not pure: reads and changes something outside
def add_to_total(price):
global total
total += price
return totalדוגמת קוד ראשונה
אותה פונקציה מתחילת השיעור, בגרסה טהורה. week-2/pure_total.py:
def add_price(total, price):
return total + price
total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)
print(add_price(0, 10))
print(add_price(0, 10))פלט:
20
10
10פירוק הקוד שורה אחר שורה
| שורה | הסבר |
|---|---|
def add_price(total, price): | כל מה שהפונקציה צריכה מגיע בפרמטרים. |
return total + price | היא לא משנה כלום — רק מחשבת ומחזירה. |
total = add_price(total, 10) | מי שקורא מחליט מה לעשות עם התוצאה: כאן — לשמור אותה ב-total. |
print(add_price(0, 10)) פעמיים | אותו קלט ← אותה תוצאה, 10, בכל פעם. |
דוגמה נוספת
המלכודת: שינוי רשימה שהתקבלה. week-2/discount_trap.py:
def add_discount(prices):
for i in range(len(prices)):
prices[i] = prices[i] * 0.9
return prices
original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)פלט:
[90.0, 180.0]
[90.0, 180.0]original השתנה! כמו שלמדנו בשיעור על Mutability, רשימה עוברת לפונקציה כהפניה — prices ו-original הם שני שמות לאותה רשימה. מי שהשתמש ב-original בהמשך התוכנית מקבל פתאום מחירים אחרים.
הגרסה הטהורה — בונים רשימה חדשה:
def add_discount(prices):
result = []
for price in prices:
result.append(price * 0.9)
return result
original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)פלט:
[90.0, 180.0]
[100, 200]עכשיו original נשאר בדיוק כמו שהיה, ו-discounted היא רשימה חדשה לגמרי.
טעות נפוצה
הטעות: לחשוב שכל פונקציה חייבת להיות טהורה. תוכנה שלא מדפיסה, לא שומרת לקובץ ולא כותבת למסד — לא עושה שום דבר שימושי. Side Effects הם חלק הכרחי מכל תוכנה.
הכלל הוא להפריד:
- את החישוב — בפונקציות טהורות (
calculate_total,apply_discount). - את התקשורת עם העולם — במקומות מעטים וברורים (
print, שמירה לקובץ).
def calculate_total(prices): # pure: easy to test
return sum(prices)
def show_total(prices): # side effect: printing
print("Total:", calculate_total(prices))תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: טהורה או לא?
לכל פונקציה — האם היא טהורה? אם לא, מה ה-Side Effect?
def square(n):
return n * n
def log_square(n):
print("squaring", n)
return n * n
def add_item(cart, item):
cart.append(item)
return cart
def with_item(cart, item):
return cart + [item]הצג פתרון
| פונקציה | טהורה? | למה |
|---|---|---|
square | כן | התוצאה תלויה רק ב-n, ושום דבר לא משתנה |
log_square | לא | print הוא Side Effect — פלט למסך |
add_item | לא | append משנה את הרשימה שהתקבלה (Mutation) |
with_item | כן | cart + [item] יוצר רשימה חדשה, והמקורית לא נגעה |
תרגיל 2: אותה קריאה — תוצאה אחרת
total = 0
def add_to_total(price):
global total
total += price
return totalהבעיה: כל קריאה ל-add_to_total(10) מחזירה תוצאה אחרת (10, ואז 20, ואז 30), כי הפונקציה תלויה במשתנה גלובלי ומשנה אותו.
שכתבו את הפונקציה כגרסה טהורה בשם add_price(total, price) — היא מקבלת את הסכום הנוכחי בפרמטר ומחזירה את הסכום החדש, בלי global. הריצו את קוד הבדיקה, שמראה שאותו קלט נותן תמיד אותה תוצאה:
total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)
print(add_price(0, 10))
print(add_price(0, 10))פלט צפוי:
20
10
10הצג פתרון
def add_price(total, price):
return total + price
total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)
print(add_price(0, 10))
print(add_price(0, 10))20
10
10כל מה שהפונקציה צריכה מגיע בפרמטרים, והתוצאה יוצאת רק דרך return. מי שקרא מחליט מה לעשות איתה.
תרגיל 3: לא לשנות את הרשימה שהתקבלה
TAX = 1.17
def apply_tax(prices):
for i in range(len(prices)):
prices[i] = prices[i] * TAXהבעיה: הפונקציה משנה את הרשימה שקיבלה, ולכן אחרי הקריאה המחירים המקוריים אבודים.
כתבו גרסה טהורה של apply_tax(prices), שמחזירה רשימה חדשה עם המחירים כולל מס (כל מחיר מעוגל לשתי ספרות) ולא נוגעת ברשימה שקיבלה. הריצו את קוד הבדיקה — השורה הראשונה מוכיחה שהרשימה המקורית לא השתנתה:
prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)פלט צפוי:
[100, 50]
[117.0, 58.5]הצג פתרון
TAX = 1.17
def apply_tax(prices):
result = []
for price in prices:
result.append(round(price * TAX, 2))
return result
prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)[100, 50]
[117.0, 58.5]TAX הוא קבוע שלא משתנה, ולכן קריאה שלו לא פוגעת בטהרה — אותו קלט עדיין נותן תמיד אותה תוצאה.
תרגיל 4: להפריד חישוב מהדפסה
def show_total(prices):
total = 0
for price in prices:
total = total + price
print("Total:", total)הפונקציה גם מחשבת וגם מדפיסה. פרקו אותה לשתיים:
calculate_total(prices)— טהורה: רק מחשבת ומחזירה את הסכום;show_total(prices)— קוראת ל-calculate_totalומדפיסה את התוצאה.
קוד לבדיקה:
show_total([10, 20, 30])פלט צפוי:
Total: 60הצג פתרון
def calculate_total(prices):
return sum(prices)
def show_total(prices):
print("Total:", calculate_total(prices))
show_total([10, 20, 30])Total: 60החישוב (calculate_total) טהור וקל לבדוק. ה-Side Effect (print) מרוכז במקום אחד וברור.
תרגיל 5: שלושת הגדולים — בלי לשנות
כתבו פונקציה טהורה top_three(numbers) שמחזירה את שלושת המספרים הגדולים, מהגדול לקטן, בלי לשנות את הרשימה המקורית. הדפיסו גם את הרשימה המקורית כדי להוכיח שהיא לא השתנתה.
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)פלט צפוי:
[99, 95, 88]
[70, 95, 88, 60, 99]הצג פתרון
def top_three(numbers):
return sorted(numbers, reverse=True)[:3]
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)[99, 95, 88]
[70, 95, 88, 60, 99]sorted מחזירה רשימה חדשה ממוינת, ו-Slicing ([:3]) מחזיר עוד רשימה חדשה — הרשימה המקורית לא נגעה, ולכן הפונקציה טהורה.
סיכום
- Pure Function = אותו קלט ← תמיד אותה תוצאה, ובלי לשנות שום דבר בחוץ.
- Side Effects: שינוי גלובלי, שינוי רשימה או מילון שהתקבלו,
print/input, כתיבה לקובץ או למסד. - רשימה עוברת כהפניה — פונקציה שמשנה אותה משנה את המקור. הגרסה הטהורה בונה רשימה חדשה.
- פונקציות טהורות קלות לבדיקה, צפויות, וקל להשתמש בהן שוב.
- לא הכול יכול להיות טהור — מפרידים בין חישוב טהור לבין תקשורת עם העולם.
דוקומנטציה רשמית
- Functional Programming HOWTO — פונקציות בלי Side Effects