Python מרוכז — פונקציות ומודולריות
Python מרוכז — פונקציות, מודולריות וטיפול בשגיאות
בשיעור 5 למדת את הבניינים הבסיסיים. בשיעור הזה עולים קומה — לוקחים את אלה ובונים איתם קוד שאפשר לתחזק, לבדוק ולהרחיב. זה ההבדל בין "כותב קוד שעובד" לבין "כותב תוכנה".
- Functions — def, return, default values, keyword args, docstrings
- *args ו-**kwargs — פרמטרים שרירותיים, unpacking
- Scope — LEGB rule, global, nonlocal, Closures
- Lambda ו-Higher-Order Functions — map(), filter(), sorted(key=)
- Exception Handling — try/except/else/finally, Custom exceptions
- Context Managers — with statement, @contextmanager
- Modules — import, from...import, os, json, random
- File I/O — open, read, write, append, JSON files
- Decorators — @decorator, wrapper pattern, functools.wraps
פונקציות (Functions)
פונקציה היא גוש קוד שנותנים לו שם, שניתן להפעיל שוב ושוב. עיקרון DRY (Don't Repeat Yourself) — אם כתבת אותו קוד פעמיים, זה מועמד לפונקציה.
הגדרה בסיסית
# def = Define → הגדרת פונקציה
def greet(name): # name = פרמטר (parameter)
message = f"שלום, {name}!"
return message # החזרת ערך
result = greet("Alice") # "Alice" = ארגומנט (argument)
print(result) # שלום, Alice!
print(greet("Bob")) # שלום, Bob!
# פונקציה ללא return — מחזירה None!
def say_hi():
print("היי!")
x = say_hi() # מדפיסה "היי!"
print(x) # None ← כי אין returnארגומנטים ופרמטרים
# ברירות מחדל (Default Values)
def power(base, exponent=2):
return base ** exponent
print(power(3)) # 9 (exponent לא הועבר → 2)
print(power(3, 3)) # 27
print(power(2, 10)) # 1024
# Keyword Arguments — העברה לפי שם
def create_user(name, age, role="user"):
return f"{name}, גיל {age}, תפקיד: {role}"
print(create_user("Alice", 25))
print(create_user(age=28, name="Carol")) # סדר לא חשוב!! ברירות מחדל Mutable — באג קלאסי ›
לעולם לא ברירת מחדל Mutable! def f(lst=[]) = באג קלאסי. הרשימה משותפת בין כל הקריאות!
# ❌ WRONG
def bad(lst=[]):
lst.append(1)
return lst
bad() # [1]
bad() # [1, 1] ← משותפת!
# ✅ CORRECT
def good(lst=None):
if lst is None:
lst = []
lst.append(1)
return lstdef f(x, y=10): return x + y; print(f(5))*args ו-**kwargs
*args ו-**kwargs מאפשרים לפונקציות לקבל מספר לא ידוע של ארגומנטים. זו טכניקה עוצמתית שתראה בכל ספרייה Python גדולה.
# *args — ארגומנטים positional שרירותיים
def add_all(*args):
print(type(args)) # <class 'tuple'>
return sum(args)
print(add_all(1, 2, 3)) # 6
print(add_all(10, 20, 30, 40)) # 100
# **kwargs — ארגומנטים keyword שרירותיים
def show_info(**kwargs):
print(type(kwargs)) # <class 'dict'>
for key, val in kwargs.items():
print(f" {key} = {val}")
show_info(name="Alice", age=25, city="TLV")
# שילוב: positional + *args + **kwargs
def full_func(first, second, *args, **kwargs):
print(f"ראשון: {first}, שני: {second}")
print(f"שאר positional: {args}")
print(f"keyword: {kwargs}")
full_func(1, 2, 3, 4, 5, x=10, y=20)
# Unpacking בעת קריאה
nums = [1, 2, 3]
print(add_all(*nums)) # → add_all(1, 2, 3)
info = {"name": "Bob", "age": 30}
show_info(**info) # → show_info(name="Bob", age=30)def f(*args): return sum(args); print(f(1,2,3,4))d={}; (lambda **k: d.update(k))(a=1,b=2); print(d)Scope: היכן חי משתנה
Scope (תחום) קובע היכן בקוד ניתן לגשת למשתנה. Python מחפשת משתנים לפי כלל LEGB.
| אות | שם | תיאור |
|---|---|---|
| L | Local | בתוך הפונקציה הנוכחית |
| E | Enclosing | פונקציה חיצונית (closure) |
| G | Global | ברמת המודול (קובץ) |
| B | Built-in | Python built-ins כמו len, print |
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local (L קודם)
inner()
print(x) # enclosing
outer()
print(x) # global
# global keyword
counter = 0
def increment():
global counter
counter += 1
# nonlocal — שינוי משתנה Enclosing
def make_counter():
count = 0
def add(n):
nonlocal count
count += n
return count
return add
c = make_counter()
print(c(5)) # 5
print(c(3)) # 8! UnboundLocalError — הטעות הנפוצה ביותר ›
x = 10
def bad():
print(x) # ← UnboundLocalError!
x += 1 # Python "יודעת" ש-x local, אבל הוא לא הוגדר לפני ההדפסה!
# נכון:
def good():
global x
print(x) # 10
x += 1Closure: פונקציה שזוכרת
Closure היא פונקציה פנימית שמשתמשת במשתנה מהפונקציה החיצונית, גם אחרי שהחיצונית סיימה לרוץ.
def multiplier(factor):
def multiply(x):
return x * factor # ← זוכרת factor!
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(7)) # 14
# בדיקה:
print(double.__closure__[0].cell_contents) # 2def outer():
x = 10
def inner():
return x * 2
return inner
f = outer()
print(f())מה הפלט?- חיפוש שם מתבצע לפי סדר LEGB: Local → Enclosing → Global → Built-in.
- global כותב למשתנה גלובלי, nonlocal כותב למשתנה של הפונקציה העוטפת.
- Closure = פונקציה פנימית ששומרת גישה למשתני הפונקציה העוטפת גם אחרי שזו הסתיימה.
- כל קריאה לפונקציה העוטפת יוצרת closure נפרד עם מצב משלו.
Lambda ו-Higher-Order Functions
Lambda היא פונקציה קצרה בשורה אחת. Higher-Order Function = פונקציה שמקבלת פונקציה אחרת כפרמטר.
# Lambda — שורה אחת
square = lambda x: x**2
print(square(5)) # 25
add = lambda x, y: x + y
print(add(3, 4)) # 7
# Lambda עם תנאי
classify = lambda x: "חיובי" if x > 0 else ("שלילי" if x < 0 else "אפס")
print(classify(5)) # חיובי
# map() — מפעיל func על כל איבר
nums = [1, 2, 3, 4, 5, 6]
squares = list(map(lambda x: x**2, nums))
print(squares) # [1, 4, 9, 16, 25, 36]
# filter() — מסנן לפי תנאי (True = נשמר)
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4, 6]
# sorted() עם key
words = ["banana", "apple", "fig", "cherry"]
by_length = sorted(words, key=lambda w: len(w))
print(by_length) # ['fig', 'apple', 'banana', 'cherry']
# מיון dict
students = [
{"name": "Alice", "score": 92},
{"name": "Bob", "score": 85},
{"name": "Carol", "score": 95},
]
by_score = sorted(students, key=lambda s: s["score"], reverse=True)
for s in by_score:
print(f"{s['name']}: {s['score']}")list(filter(lambda x: x>3, [1,2,3,4,5]))list(map(str, [1,2,3]))(lambda x,y: x**y)(2,8)Exception Handling: טיפול בשגיאות
שגיאות (Exceptions) הן מצבים לא צפויים שקורים בזמן ריצה. try/except מאפשר "ללכוד" שגיאות ולטפל בהן במקום לקרוס.
# מבנה מלא: try / except / else / finally
def safe_access(lst, index):
try:
value = lst[index]
result = 100 / value
except IndexError:
print(f"אינדקס {index} מחוץ לטווח!")
return None
except ZeroDivisionError:
print("הערך הוא 0 — אי אפשר לחלק!")
return None
except Exception as e:
print(f"שגיאה לא צפויה: {type(e).__name__}: {e}")
return None
else:
# מתבצע רק אם לא הייתה שגיאה!
print(f"הצלחה: {result:.2f}")
return result
finally:
# מתבצע תמיד — גם אם הייתה שגיאה!
print("(ניסיון הסתיים)")
safe_access([5, 0, 2], 0) # הצלחה: 20.00
safe_access([5, 0, 2], 1) # שגיאה חלוקה
safe_access([5, 0, 2], 9) # אינדקס חוץ לטווח| Exception | מתי נזרקת | דוגמה |
|---|---|---|
| ValueError | ערך לא תקין לסוג | int("abc") |
| TypeError | סוג טיפוס שגוי | "5" + 5 |
| IndexError | אינדקס מחוץ לטווח | [1,2][5] |
| KeyError | מפתח לא קיים ב-dict | {}["key"] |
| ZeroDivisionError | חלוקה באפס | 5/0 |
| FileNotFoundError | קובץ לא קיים | open("x.txt") |
| AttributeError | attr לא קיים | None.split() |
try:
x = 1/0
except ZeroDivisionError:
print("err")
finally:
print("done")מה הפלט?- try מריץ, except תופס שגיאה ספציפית, else רץ רק כשלא הייתה שגיאה, finally רץ תמיד.
- תופסים חריגה ספציפית (ValueError) ולא except: עירום — אחרת מסתירים באגים אמיתיים.
- with סוגר משאבים (קבצים, סוקטים) אוטומטית גם כשנזרקת שגיאה.
- חריגות מותאמות (class MyError(Exception)) מאפשרות טיפול מדויק לפי סוג התקלה.
Context Managers, מודולים ו-File I/O
Context Manager מנהל "כניסה" ו"יציאה" ממשאב ומבטיח cleanup תמיד. with הוא הדרך הבטוחה לפתוח קבצים.
Context Manager — with statement
# בלי with — מסוכן!
f = open("data.txt", "r")
content = f.read()
# מה אם תיזרק exception כאן? f.close() לא יקרא!
f.close()
# עם with — בטוח ומומלץ
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# f.close() נקרא אוטומטית, גם אם הייתה שגיאה!
# כמה context managers ביחד
with open("input.txt", "r") as fin, open("output.txt", "w") as fout:
for line in fin:
fout.write(line.upper())File I/O — קריאה וכתיבה
# מודים: "r"=קריאה, "w"=כתיבה (מאפס!), "a"=append, "rb"=binary
# קריאה שלמה
with open("file.txt", "r", encoding="utf-8") as f:
content = f.read() # כל הקובץ כstring
# קריאה שורה-שורה (יעיל לקבצים גדולים)
with open("file.txt", "r", encoding="utf-8") as f:
for line in f: # iterator — לא טוען הכל לזיכרון!
print(line.strip())
# כתיבה
with open("output.txt", "w", encoding="utf-8") as f:
f.write("שורה ראשונה\n")
# JSON
import json
data = {"name": "Alice", "scores": [95, 87]}
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
with open("data.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["name"]) # Alicewith open("a.txt","w") as f: f.write("hi")Decorators: מבוא
Decorator הוא פונקציה שמעטפת פונקציה אחרת ומוסיפה לה פונקציונליות — מבלי לשנות את הקוד המקורי. סינטקס: @decorator_name מעל הגדרת הפונקציה.
import time
# Decorator בסיסי — מדוד זמן ריצה
def timer(func): # ← מקבל פונקציה
def wrapper(*args, **kwargs): # ← עוטף אותה
start = time.time()
result = func(*args, **kwargs) # ← מפעיל המקורית
end = time.time()
print(f"{func.__name__} לקח {end-start:.4f}s")
return result
return wrapper # ← מחזיר הגרסה העטופה
@timer # שקול ל: slow_func = timer(slow_func)
def slow_func():
time.sleep(0.1)
return "done"
slow_func() # slow_func לקח 0.1002s
# Decorator שימושי — logger
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"→ {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"← {func.__name__} החזיר: {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 4)
# → add((3, 4), {})
# ← add החזיר: 7@app.route("/path")- @decorator מעל def f שקול ל-f = decorator(f).
- ה-wrapper מקבל *args, **kwargs כדי לעטוף כל פונקציה, ומחזיר את ערך המקור.
- @functools.wraps(func) משמר את שם הפונקציה והתיעוד המקוריים.
- שימושים נפוצים: לוגים, מדידת זמן, caching, אימות הרשאות.
אתגר מעשי — פונקציות, Closures ו-Decorators
שיא השיעור: לבנות, לתקן ולנתח — לא לזהות תשובה. פתרו את משימת החובה ואז את האתגר שמעליה; ההגשה נבדקת אוטומטית מול מקרי-קצה נסתרים (וחלקן אוכפות יעילות דרך גודל-קלט).
def add_item(item, bucket=[]): # באג כאן
bucket.append(item)
return bucketסיכום השיעור
- פונקציות: def, return, default values, keyword args, docstrings
- *args ו-**kwargs: פרמטרים שרירותיים, unpacking
- Scope: LEGB rule, global, nonlocal, Closures
- Lambda ו-Higher-Order Functions: map(), filter(), sorted(key=)
- Exception Handling: try/except/else/finally, Custom exceptions, Exception hierarchy
- Context Managers: with statement, ביטוח cleanup
- Modules: import, from...import, os, json, random
- File I/O: open, read, write, append, JSON files
- Decorators: @decorator, wrapper pattern, functools.wraps
לפני שיעור 7 — ודא שאתה שולט ב-closures ו-decorators מהשיעור הזה. OOP בנוי ישירות עליהם.
def process_data(filename, threshold=50):
results = {"passed": [], "failed": [], "errors": []}
try:
with open(filename, "r") as f:
for line in f:
try:
name, score = line.strip().split(",")
score = int(score)
if score >= threshold:
results["passed"].append(name)
else:
results["failed"].append(name)
except ValueError:
results["errors"].append(line.strip())
except FileNotFoundError:
return None
return results
מה יחזיר process_data("grades.csv", 80) אם grades.csv מכיל: "Alice,95\nBob,72\nCarol,abc\n"?def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
add10 = make_adder(10)
results = []
for val in [1, 3, 7]:
results.append(add5(val))
results.append(add10(val))
print(results)מה הפלט?def count_up(start=0):
n = start
def inc():
nonlocal n
n += 1
return n
return inc
c = count_up(10)
print(c(), c(), c())מה הפלט?שאלות תרגול ותשובות (7)
שאלה 1 · מה מחזירה פונקציה ב-Python שאין בה פקודת return?
- א. 0
- ב. None
- ג. False
- ד. שגיאה
פונקציה ללא return מחזירה None באופן אוטומטי — זו לא שגיאה.
שאלה 2 · מה יודפס?
def f(x, y=10):
return x + y
print(f(5))- א. 5
- ב. 10
- ג. 15
- ד. שגיאה
y מקבל את ערך ברירת המחדל 10, ולכן 5 + 10 = 15.
שאלה 3 · מה ההבדל בין global לבין nonlocal?
- א. שניהם זהים לחלוטין
- ב. global כותב למשתנה גלובלי, nonlocal למשתנה של הפונקציה העוטפת
- ג. nonlocal כותב למשתנה גלובלי, global לפונקציה העוטפת
- ד. שניהם עובדים רק בתוך מחלקות
global מפנה לתחום הגלובלי (G ב-LEGB), nonlocal לתחום העוטף (E).
שאלה 4 · מה יודפס?
def outer():
x = 10
def inner():
return x * 2
return inner()
print(outer())- א. 10
- ב. 20
- ג. None
- ד. שגיאת NameError
inner רואה את x של outer דרך תחום ה-Enclosing, ולכן 10*2 = 20.
שאלה 5 · מדוע עדיף except ValueError על פני except: ללא סוג חריגה?
- א. אין הבדל, שניהם זהים
- ב. except: מהיר יותר בביצוע
- ג. except: תופס גם שגיאות שלא התכוונו אליהן ומסתיר באגים
- ד. except: אינו חוקי בפייתון
except עירום תופס כל חריגה כולל KeyboardInterrupt ובאגים אמיתיים — ומקשה על איתור תקלות.
שאלה 6 · מה עושה @functools.wraps(func) בתוך decorator?
- א. משמר את __name__ ו-__doc__ של הפונקציה המקורית
- ב. יוצר עותק גיבוי של הפונקציה
- ג. מאיץ את הריצה באמצעות caching
- ד. מונע שגיאות בפונקציה המעוטרת
בלי wraps, שם הפונקציה והתיעוד שלה מוחלפים בשם ה-wrapper.
שאלה 7 · מה יודפס?
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(4))- א. 10
- ב. 12
- ג. 24
- ד. 256
4 × 3 × 2 × 1 = 24 — רקורסיה עם תנאי עצירה ב-n <= 1.