Clean Code א': שמות טובים, פונקציות קטנות ואחריות אחת
- להסביר מה זה Clean Code ולמה קוד נקרא הרבה יותר משהוא נכתב
- לבחור שמות ברורים למשתנים, פונקציות וקבצים לפי המוסכמות של Python
- לזהות פונקציה שעושה יותר מדבר אחד ולפרק אותה
- ליישם את עקרון Single Responsibility ברמת הפונקציה
מה נלמד?
עד עכשיו המטרה הייתה “שהקוד יעבוד”. מהיום המטרה היא גם שהוא יהיה ברור. נלמד:
- Naming — איך בוחרים שמות למשתנים, פונקציות וקבצים.
- Small Functions — למה פונקציות צריכות להיות קצרות.
- Single Responsibility — כל פונקציה עושה דבר אחד.
למה צריך את זה?
קוד נקרא הרבה יותר פעמים ממה שהוא נכתב. את הפונקציה כותבים פעם אחת, אבל קוראים אותה בכל תיקון באג, בכל הוספת פיצ’ר, ובכל Code Review של חבר צוות. ובעוד חודש, גם אתם תקראו אותה כמו זרים.
בחברות, קוד שעובד אבל לא קריא נדחה ב-Code Review. בראיונות עבודה בודקים לא רק “האם פתרת”, אלא גם “האם אפשר להבין את הפתרון”.
השוו:
def f(l):
t = 0
for x in l:
t = t + x[1] * x[2]
return t * 1.17VAT_RATE = 1.17
def calculate_order_total(items):
subtotal = 0
for item in items:
subtotal = subtotal + item["price"] * item["quantity"]
return subtotal * VAT_RATEשתי הפונקציות עושות בדיוק אותו דבר. את השנייה מבינים בלי מאמץ.
הסבר מהבסיס
מה זה Clean Code?
Clean Code (קוד נקי) הוא קוד שאדם אחר יכול לקרוא, להבין ולשנות בקלות, ובביטחון. זה לא סט של חוקים קשיחים, אלא אוסף הרגלים שהופכים קוד לברור.
Naming — שמות טובים
שם טוב עונה על השאלה “מה זה?” או “מה זה עושה?” בלי שצריך לקרוא את הקוד מסביב.
מוסכמות השמות של Python (לפי PEP 8, מדריך הסגנון הרשמי):
| מה | סגנון | דוגמה |
|---|---|---|
| משתנים ופונקציות | snake_case | total_price, send_email() |
| קבועים (ערכים שלא משתנים) | UPPER_SNAKE_CASE | VAT_RATE, MAX_RETRIES |
| מחלקות (נושא OOP) | PascalCase | Student, BankAccount |
| קבצים ומודולים | snake_case.py | order_utils.py |
כללים לשם טוב:
- מתאר כוונה —
days_until_examעדיף עלd. - פונקציה = פועל —
calculate_total(),send_invoice(),is_valid_email(). משתנה = שם עצם —total,invoice,email. - בוליאני נשמע כמו שאלה —
is_active,has_permission,can_edit. - רשימה ברבים —
students(רשימה),student(פריט אחד). - בלי קיצורים לא ברורים —
customerולאcstmr. (קיצורים מוכרים כמוid,url,maxבסדר.) - בלי מספרי קסם (Magic Numbers) — במקום
* 1.17שאיש לא יודע מה הוא, קבוע עם שם:VAT_RATE = 1.17.
Small Functions — פונקציות קטנות
פונקציה ארוכה (50+ שורות) קשה להבין, קשה לבדוק וקשה לשנות. כלל אצבע: אם פונקציה לא נכנסת במסך אחד, או שקשה לתת לה שם בלי המילה “and”, היא כנראה עושה יותר מדי.
Single Responsibility — אחריות אחת
Single Responsibility Principle (SRP): לכל פונקציה צריכה להיות סיבה אחת להשתנות, כלומר תפקיד אחד.
במסעדה יש מלצר, טבח וקופאי. אם הטבח גם מקבל הזמנות, גם מבשל וגם גובה כסף, כל שינוי (תפריט חדש, מכשיר אשראי חדש) מחייב “לשנות” אותו, וכל טעות שלו פוגעת בהכול. כשלכל אחד תפקיד אחד, קל להחליף, לשפר ולמצוא איפה הבעיה.
תחביר
אין “תחביר חדש” לקוד נקי. זה אותו Python, רק מאורגן אחרת. התבנית:
CONSTANT_NAME = value # constants at the top, UPPER_SNAKE_CASE
def verb_noun(clear_parameter): # a function does ONE thing, named by what it does
...
return result
def main(): # one function that connects the small pieces
data = get_something()
result = process_something(data)
show_something(result)
main()דוגמת קוד ראשונה
לפני — קוד שעובד אבל לא נקי. week-2/grades_before.py:
def do(g):
s = 0
for x in g:
s = s + x
a = s / len(g)
if a >= 55:
print("avg", a, "pass")
else:
print("avg", a, "fail")
do([80, 45, 92, 60])אחרי — אותה התנהגות, קוד נקי. week-2/grades_after.py:
PASSING_GRADE = 55
def calculate_average(grades):
total = 0
for grade in grades:
total = total + grade
return total / len(grades)
def is_passing(average):
return average >= PASSING_GRADE
def print_report(average):
if is_passing(average):
status = "pass"
else:
status = "fail"
print("Average:", average, "-", status)
student_grades = [80, 45, 92, 60]
average = calculate_average(student_grades)
print_report(average)פלט (גרסת “אחרי”. הגרסה הישנה מדפיסה avg 69.25 pass, אותו חישוב בניסוח פחות ברור):
Average: 69.25 - passפירוק הקוד שורה אחר שורה
| בקוד הישן | בקוד הנקי | למה זה טוב יותר |
|---|---|---|
do | שלוש פונקציות עם שמות | השם אומר מה כל אחת עושה. |
g, s, x, a | grades, total, grade, average | אין צורך “לפענח”. |
55 בתוך הקוד | PASSING_GRADE = 55 | ערך עם משמעות ושם. אם הציון משתנה, משנים במקום אחד. |
| חישוב + החלטה + הדפסה בפונקציה אחת | calculate_average, is_passing, print_report | כל פונקציה עושה דבר אחד. |
היתרון המעשי: רוצים להציג את הממוצע באתר במקום במסך? רק print_report משתנה. רוצים לשנות את ציון המעבר? רק PASSING_GRADE. ו-calculate_average שימושית גם במקומות אחרים, כי היא מחזירה ערך ולא מדפיסה.
דוגמה נוספת
פונקציה “שעושה הכול” במערכת הרשמה, ואיך מפרקים אותה. לפני:
def register(n, e, a):
if "@" not in e:
print("bad email")
return
if a < 16:
print("too young")
return
u = n.strip().lower().replace(" ", ".")
print("Welcome " + n + "! Your username is " + u)אחרי:
MIN_AGE = 16
def is_valid_email(email):
return "@" in email
def is_old_enough(age):
return age >= MIN_AGE
def create_username(full_name):
return full_name.strip().lower().replace(" ", ".")
def register_user(full_name, email, age):
if not is_valid_email(email):
print("Invalid email address")
return
if not is_old_enough(age):
print("You must be at least", MIN_AGE, "to register")
return
username = create_username(full_name)
print("Welcome " + full_name + "! Your username is " + username)
register_user("Dana Cohen", "dana@mail.com", 24)
register_user("Avi Levi", "avi-mail.com", 30)פלט:
Welcome Dana Cohen! Your username is dana.cohen
Invalid email addressפירוק:
"@" in email—inבודק אם טקסט מופיע בתוך טקסט אחר, ומחזירTrue/False..strip()— מסירה רווחים מההתחלה ומהסוף..lower()— אותיות קטנות..replace(" ", ".")— מחליפה רווחים בנקודות. אלה מתודות שלstrשאפשר לשרשר אחת אחרי השנייה.returnבלי ערך — יוצא מהפונקציה מיד (מחזירNone). כך לא ממשיכים להרשמה אחרי שגיאה. הטכניקה נקראת Early Return (או Guard Clause).register_userעכשיו “קוראת כמו סיפור”: בדוק מייל, בדוק גיל, צור שם משתמש, ברך. כל הפרטים בפונקציות קטנות.
טעות נפוצה
הטעות 1: שמות שמשקרים.
def get_user(user_id):
user = find_in_list(user_id)
user["last_login"] = "today" # changes data!
delete_old_sessions(user) # deletes data!
return userהשם אומר “get” (קבל), אבל הפונקציה משנה ומוחקת. מי שקורא לה כדי “רק לקבל משתמש” יופתע. שם צריך לתאר את כל מה שהפונקציה עושה. ואם קשה לתאר בלי “and”, יש יותר מאחריות אחת.
הטעות 2: פירוק מוגזם. גם ההפך הוא בעיה:
def add_one(number):
return number + 1פונקציה של שורה אחת שמסתירה פעולה ברורה מאליה רק מוסיפה קפיצות בקריאה. מפרקים כשיש רעיון בעל שם (is_old_enough), לא כל שורה.
הטעות 3: שמות גנריים מדי. data, info, temp, result, manager, process(). שמות כאלה כמעט תמיד אפשר לשפר: data → student_records, process() → apply_discount().
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: שמות שמסבירים את עצמם
הקוד הבא עובד, אבל אי אפשר להבין מה הוא עושה:
def f(a, b):
c = a * b
if c > 500:
c = c - 50
return c
print(f(120, 5))
print(f(30, 2))שכתבו אותו עם שמות טובים לפונקציה ולמשתנים, והחליפו את המספרים 500 ו-50 בקבועים עם שם. הפלט חייב להישאר זהה:
550
60הצג פתרון
DISCOUNT_THRESHOLD = 500
DISCOUNT_AMOUNT = 50
def calculate_order_price(unit_price, quantity):
price = unit_price * quantity
if price > DISCOUNT_THRESHOLD:
price = price - DISCOUNT_AMOUNT
return price
print(calculate_order_price(120, 5))
print(calculate_order_price(30, 2))550
60- שם הפונקציה הוא פועל שמתאר מה היא עושה.
500ו-50היו Magic Numbers — עכשיו ברור מה הם, ואם ההנחה תשתנה, משנים במקום אחד.
תרגיל 2: מוסכמות השמות
לכל שם — האם הוא כתוב לפי מוסכמות Python? אם לא, כתבו את השם הנכון.
TotalPrice (a variable)
numberofstudents (a variable)
max_grade (a variable)
vat_rate (a constant that never changes)
GetAverage (a function)הצג פתרון
| שם | תקין? | שם נכון |
|---|---|---|
TotalPrice | לא — משתנה כותבים ב-snake_case | total_price |
numberofstudents | לא — קשה לקרוא בלי קו תחתון | number_of_students |
max_grade | כן | — |
vat_rate | לא — קבוע כותבים באותיות גדולות | VAT_RATE |
GetAverage | לא — פונקציה כותבים ב-snake_case, ועדיף פועל | get_average / calculate_average |
תרגיל 3: פונקציה אחת — אחריות אחת
הפונקציה הבאה מחשבת ממוצע, מחליטה אם עבר, וגם מדפיסה — שלושה תפקידים:
def report(name, grades):
total = 0
for g in grades:
total = total + g
avg = total / len(grades)
if avg >= 55:
print(name + ": average", avg, "- passed")
else:
print(name + ": average", avg, "- failed")
report("Dana", [80, 85])
report("Avi", [50, 46])פרקו אותה לשלוש פונקציות: calculate_average, is_passing ו-print_report(name, average). print_report מקבלת ממוצע שכבר חושב ורק מדפיסה. הפלט חייב להישאר:
Dana: average 82.5 - passed
Avi: average 48.0 - failedהצג פתרון
PASSING_GRADE = 55
def calculate_average(grades):
return sum(grades) / len(grades)
def is_passing(average):
return average >= PASSING_GRADE
def print_report(name, average):
if is_passing(average):
status = "passed"
else:
status = "failed"
print(name + ": average", average, "-", status)
dana_average = calculate_average([80, 85])
print_report("Dana", dana_average)
avi_average = calculate_average([50, 46])
print_report("Avi", avi_average)Dana: average 82.5 - passed
Avi: average 48.0 - failedcalculate_averageרק מחשבת,is_passingרק מחליטה, ו-print_reportמקבלת ממוצע מוכן ורק מציגה אותו. החישוב קורה מחוץ לה.- אם
print_reportהייתה קוראת בעצמה ל-calculate_average, היא שוב הייתה גם מחשבת וגם מדפיסה. הקוד קצר יותר, אבל האחריות לא מופרדת. - עכשיו אפשר להשתמש ב-
calculate_averageגם במקום שלא צריך להדפיס.
תרגיל 4: בלי מספרי קסם
בחנות אונליין, הפונקציה הבאה מקבלת את סכום ההזמנה ומחזירה את הסכום לתשלום:
def final_price(total):
if total < 200:
total = total + 30
return total
print(final_price(150))
print(final_price(250))- מה המשמעות של 200 ושל 30 בחנות? (רמז: מה קורה להזמנה קטנה?) תנו להם שמות כקבועים.
- שכתבו את הפונקציה כך שהשם שלה יסביר בדיוק מה היא מוסיפה.
פלט צפוי:
180
250הצג פתרון
FREE_SHIPPING_MINIMUM = 200
SHIPPING_FEE = 30
def add_shipping(order_total):
if order_total < FREE_SHIPPING_MINIMUM:
return order_total + SHIPPING_FEE
return order_total
print(add_shipping(150))
print(add_shipping(250))180
250200 הוא סכום מינימלי למשלוח חינם, ו-30 הוא דמי המשלוח. עם שמות, אף אחד לא צריך לנחש.
תרגיל 5: פונקציה קטנה שעונה על שאלה אחת
כתבו פונקציה is_valid_password(password) שמחזירה True אם הסיסמה באורך 8 תווים לפחות וגם מכילה לפחות ספרה אחת, אחרת False. הפונקציה לא מדפיסה — רק מחזירה.
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))פלט צפוי:
True
False
Falseהצג פתרון
MIN_PASSWORD_LENGTH = 8
def has_digit(text):
for char in text:
if char.isdigit():
return True
return False
def is_valid_password(password):
return len(password) >= MIN_PASSWORD_LENGTH and has_digit(password)
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))True
False
Falseהשם is_valid_password מתחיל ב-is_ — ברור שהיא מחזירה True או False. הבדיקה של הספרה הופרדה לפונקציה קטנה משלה.
סיכום
- Clean Code = קוד שאחרים (ואתם בעוד חודש) מבינים ומשנים בקלות. קוד נקרא הרבה יותר משנכתב.
- שמות:
snake_caseלמשתנים ופונקציות,UPPER_SNAKE_CASEלקבועים. פונקציה = פועל, בוליאני = שאלה (is_...). - בלי מספרי קסם. ערך עם משמעות מקבל קבוע עם שם.
- פונקציות קטנות עם אחריות אחת (SRP). אם צריך “and” בשם, כנראה צריך לפרק.
- פונקציה שמחשבת מחזירה. ההדפסה היא אחריות נפרדת.
דוקומנטציה רשמית
- PEP 8 — Naming Conventions — הכללים הרשמיים לשמות ב-Python
- PEP 8 — Function and Variable Names — snake_case לפונקציות ומשתנים
- PEP 20 — The Zen of Python — Readability counts