• בלוג
  • חיפוש אלמנט ב List ו Dictionary בשפת Python

חיפוש אלמנט ב List ו Dictionary בשפת Python

02/05/2019

הפקודה in של פייתון מאפשרת לחפש אלמנט בתוך מבנה נתונים. למעשה כל מה שהיא עושה זה להפעיל את הפונקציה __contains__ של מבנה הנתונים, או בקוד זה אומר ש:

5 in [10, 20, 30, 5, 9]

בעצם מפעיל את:

[10, 20, 30, 5, 9].__contains__(5)

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

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

1. חיפוש אלמנט במילון

הקוד הבא מייצר מילון של אלף ערכים אקראיים ומחפש בו את המפתח 5:

import random
import timeit

setup = """
import random
d = {}
for _ in range(10_000):
  d[random.randint(1, 1_000)] = random.randint(1, 1_000)
"""

print(timeit.timeit('5 in d', setup=setup))

בהפעלה אצלי על המחשב מתקבל זמן הריצה:

0.031794708

2. חיפוש ערך במפתחות של מילון

הפקודה keys של מילון מחזירה את אוסף המפתחות של אותו המילון. ב Python2 הפקודה החזירה רשימה רגילה, אבל ב Python3 פקודה זו מחזירה מבנה נתונים חדש שנקרא dict_keys. מבנה זה הוא בסך הכל Pointer למפתחות המילון ולכן יצירתו יחסית מהירה, אבל עדיין בבדיקה אנחנו יכולים לראות שזה לוקח קצת יותר זמן מאשר חיפוש ישיר במילון (כנראה בגלל שלוקח לפייתון קצת זמן לקרוא לפונקציה ולייצר את המבנה). חשוב להדגיש - אין העתקה של מפתחות המילון ולכן משך פעולה זו לא תלוי בגודל המילון.

הקוד הבא מייצר dict_keys ומחפש בו ערך. בשביל להבדיל בין הזמן שלוקח לייצר את ה dict_keys לזמן שלוקח לחפש במבנה כזה יצרתי שתי בדיקות, בראשונה יצירת ה dict_keys מתרחשת ב setup כך שאינה נספרת בזמן הבדיקה, ובשניה הבדיקה כוללת גם את יצירת ה dict_keys:

import random
import timeit

setup = """
import random
d = {}
for _ in range(10_000):
  d[random.randint(1, 1_000)] = random.randint(1, 1_000)

keys = d.keys()
"""

print(timeit.timeit('5 in d', setup=setup))
print(timeit.timeit('5 in keys', setup=setup))
print(timeit.timeit('5 in d.keys()', setup=setup))

תוצאת ההרצה אצלי על המחשב נראית כך:

0.031886886999999996
0.03299745899999999
0.10683095899999998

קל לראות ששני החיפושים הראשונים מאוד קרובים, והחיפוש השלישי שכולל גם את יצירת מבנה ה dict_keys לוקח קצת יותר זמן. זה אומר שבתוכנית אמיתית כשנרצה לחפש במילון כדאי לזכור להשתמש ב:

d = {'a': 10, 'b': 20}

if 'a' in d:
    pass

ולא בגירסא הארוכה יותר:

d = {'a': 10, 'b': 20}

if 'a' in d.keys():
    pass

למרות שגם אם כן בחרתם בגירסא השניה פער הזמנים לא יהיה יותר מדי מורגש.

3. חיפוש ערך ברשימה

מצד שני ברגע שאנחנו עוזבים את המילון ועוברים לרשימה מימוש הפונקציה __contains__ משתנה: ברשימה חיפוש אלמנט מחייב לסרוק את הרשימה אלמנט אחרי אלמנט וזה כבר תהליך שיכול לקחת זמן. התאמתי את תוכנית הבדיקה כדי שתהפוך את ה dict_keys שלנו לרשימה ואז תנסה לחפש שם את 5:

import random
import timeit

setup = """
import random
d = {}
for _ in range(10_000):
  d[random.randint(1, 1_000)] = random.randint(1, 1_000)

keys = d.keys()
keys_as_list = list(keys)
"""

print(timeit.timeit('5 in d', setup=setup))
print(timeit.timeit('5 in keys', setup=setup))
print(timeit.timeit('5 in d.keys()', setup=setup))
print(timeit.timeit('5 in keys_as_list', setup=setup))

ותוצאת זמני הריצה לא הפתיעה:

0.035458299000000006
0.03386534499999999
0.10725996600000004
5.69748644

חיפוש ברשימה לוקח משמעותית יותר זמן מחיפוש במילון, בגלל שהפונקציה __contains__ של רשימה עושה משהו שדורש הרבה יותר מאמץ מאשר הפונקציה המקבילה במילון. בעבודה עם פייתון3 אני חושב שיחסית קשה להמיר את המידע בטעות ממילון לרשימה לצורך החיפוש (בזכות התוספת של dict_keys), ועדיין שווה להכיר את ההבדלים האלה כשאתם בוחרים באיזה מבנה נתונים להשתמש ולבחור את מבנה הנתונים שיתאים בצורה הטובה ביותר לסיטואציה שלכם.