• בלוג
  • תיקון שימוש מסורבל בפונקציה sorted בפייתון

תיקון שימוש מסורבל בפונקציה sorted בפייתון

07/05/2023

הפונקציה sorted בפייתון מקבלת רשימה ומחזירה עותק ממוין של אותה רשימה. לא מזמן נתקלתי בקטע קוד שדומה לקוד הזה:

import wikipedia

def k(i):
    """ returns the number of words the value i has in wikipedia """
    return len(wikipedia.summary(i))


words = ["wikipedia", "Python (programming language)", "Ruby (programming language)", "Rust (programming language)"]

print([
    w[0] for w in
    sorted([(w, k(w)) for w in words], key=lambda i: i[1], reverse=True)])

הקוד לוקח רשימה של ערכים בויקיפדיה ומדפיס אותם ממוינים לפי מספר המילים בכל ערך. אבל, למה הקוד כל כך מסורבל? והאם אפשר לפשט אותו?

1. נקרא לאט את החלק המסורבל

כשאנחנו רואים קטע קוד מסורבל שאנחנו לא מבינים כדאי לעצור ולקרוא אותו לאט. הלולאה המסובכת בקוד היא:

[
    w[0] for w in
    sorted([(w, k(w)) for w in words], key=lambda i: i[1], reverse=True)
]

נפרק את השורה לחלקים ונפריד לכמה שורות:

list_1 = [(w, k(w)) for w in words]
list_2 = sorted(list_1, key=lambda i: i[1], reverse=True)

final_list = [ w[0] for w in temp_list]

עכשיו אפשר לתת שמות טובים יותר למשתנים: המשתנה list_1 מכיל רשימה של tuples, בכל tuple המשתנה הראשון הוא ערך מתוך מערך המילים והערך השני הוא תוצאת הפונקציה k עליו. הרשימה השניה היא עותק ממוין של הראשונה לפי הערך השני ב tuple, כלומר לפי מספר המילים בויקיפדיה של הערך, והרשימה השלישית היא רשימת האיבר הראשון בלבד מתוך ה tuples. סך הכל עם שמות מתוקנים יש לי:

value_with_wordcount = [(w, k(w)) for w in words]
sorted_by_wordcount = sorted(list_1, key=lambda i: i[1], reverse=True)

sorted_values = [ w[0] for w in temp_list]

ועכשיו זה ברור - הקוד רוצה למיין את רשימת הערכים לפי אורך מאמרי הויקיפדיה שלהם. אבל למה צריך את כל הרשימות באמצע? למה לא להשתמש רק ב sorted?

אני לא בטוח בתשובה האמיתית אבל יכול רק לשער, שמי שכתב את הקוד חשב שהפונקציה key עשויה להיקרא מספר פעמים על אותו ערך, למשל אם מנגנון המיון יצטרך לחשב את המפתח כמה פעמים, ולכן sorted תעבוד טוב יותר אם הפונקציה key תחזור כמה שיותר מהר. בגלל שחיפוש המילים בויקיפדיה יכול להיות איטי, יש פה ניסיון לשפר ביצועים באמצעות שמירת התוצאה של k של כל ערך בצד, ושימוש בערך השמור במהלך המיון.

זו תיאוריה יפה אבל לשמחתנו לא נכונה בכלל. הפונקציה sorted של פייתון ממילא תפעיל את key רק פעם אחת על כל איבר ואחרי זה מנגנון המיון ישתמש במפתחות שהוא חישוב ושמר לעצמו בצד. בעצם פייתון לבד עושה את העבודה שפה נעשתה בצורה מפורשת (כך לפי התיעוד וגם אפשר לבדוק ולהשתכנע).

2. עדכון הקוד לגירסה הפשוטה

אחרי שקראנו לאט את הקוד והבנו את הטעות, אנחנו יכולים לתקן אותו לגירסה הפשוטה יותר:

import wikipedia

def k(i):
    """ returns the number of words the value i has in wikipedia """
    return len(wikipedia.summary(i))


words = ["wikipedia", "Python (programming language)", "Ruby (programming language)", "Rust (programming language)"]
print(sorted(words, key=k, reverse=True))

ונכון - מבחינת מהירות ריצה או תוצאה אין הבדל בין שתי הגירסאות. הייתרון של הגירסה הפשוטה הוא במילת הקסם "פשוטה", קוד שקל יותר לקרוא, להבין ולתחזק.