א.8 — רשימות — List

עד כה כל משתנה החזיק ערך אחד. אבל רוב הנתונים שתוכנית מטפלת בהם באים בקבוצות: הציונים של כל תלמידי הכיתה, השמות של השחקנים במשחק, המיקומים של כל האויבים על המסך. משתנה נפרד לכל ערך (score1, score2, score3...) אינו מעשי: לא יודעים מראש כמה ערכים יהיו, ואי אפשר לעבור עליהם בלולאה. לשם כך צריך מבנה שמחזיק אוסף ערכים תחת שם אחד.

רשימה (List) שומרת כמה ערכים ברצף אחד, למשל את הציונים של תלמידי הכיתה. כמו במערך, לכל איבר יש מיקום שנקרא אינדקס, ואפשר לקרוא או לשנות את האיבר באמצעותו; האינדקס הראשון הוא 0.

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

פתיחת המחברת ב־Colab · השיעור וההרצאה באתר הקורס

מחברות האוניברסיטה הפתוחה — 5-רשימות; 6-שוויון וזהות

יצירת רשימה

כדי ליצור רשימה כותבים את האיברים בתוך סוגריים מרובעים [], ומפרידים ביניהם בפסיקים. את הרשימה שומרים במשתנה כדי להשתמש בה בהמשך. סדר האיברים נשמר, ואותו ערך יכול להופיע יותר מפעם אחת.

רשימה של מספרים

למשל, אפשר לשמור כמה ציונים ברשימה אחת. המספרים נכתבים ללא מירכאות; הרשימה יכולה להכיל מספרים שלמים וגם מספרים עשרוניים.

scores = [80, 92.5, 100, 80]
print(scores)

פלט

[80, 92.5, 100, 80]

ברשימה ארבעה איברים. הציון 80 מופיע פעמיים — כל הופעה היא איבר בפני עצמו.

רשימה של מחרוזות

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

names = ["Dana", "Noam", "Yael"]
print(names)

פלט

['Dana', 'Noam', 'Yael']

כל שם שלם הוא איבר אחד ברשימה, גם אם הוא מכיל כמה תווים.

רשימה מעורבת

אפשר לשלב באותה רשימה ערכים מטיפוסים שונים:

student = ["Dana", 17, 92.5, True]
print(student)

פלט

['Dana', 17, 92.5, True]

כאן האיברים הם מחרוזת, מספר שלם, מספר עשרוני וערך בוליאני. אין צורך להצהיר מראש על טיפוס משותף לכל האיברים.

רשימה ריקה

לפעמים עדיין אין נתונים לשמור, למשל לפני שמתחילים לקלוט ציונים. כותבים [] — סוגריים מרובעים ללא איברים ביניהם — כדי ליצור רשימה ריקה.

scores = []
print(scores)
print(len(scores))

פלט

[]
0

המשתנה scores כבר מוגדר ומחזיק רשימה, אך כרגע אין בה אף איבר. הפונקציה len מחזירה את מספר האיברים, ולכן התוצאה היא 0. בהמשך אפשר להוסיף אליה ציונים. רשימה ריקה שונה מ־[0]: ברשימה [0] יש איבר אחד שערכו אפס.

יצירת רשימה באמצעות list()

דרך נוספת ליצור רשימה היא להשתמש ב־list(), באותיות קטנות. קריאה ללא ערכים בסוגריים יוצרת רשימה ריקה, כמו []. כאשר מוסרים ל־list רצף שאפשר לעבור על איבריו, היא יוצרת ממנו רשימה.

empty = list()
letters = list("abc")
numbers = list(range(2, 10, 2))
print(empty)
print(letters)
print(numbers)

פלט

[]
['a', 'b', 'c']
[2, 4, 6, 8]

ב־list("abc") כל תו הופך לאיבר נפרד. ב־list(range(2, 10, 2)) נוצרת רשימה מהמספרים שמספק range: מתחילים ב־2, מתקדמים בצעדים של 2 ועוצרים לפני 10.

גישה לאיבר ברשימה

אחרי שיצרנו רשימה, נרצה לעיתים לקרוא מתוכה ערך מסוים — למשל את הציון של התלמיד השני. לכל איבר יש מספר מיקום שנקרא אינדקס. הספירה מתחילה ב־0: לאיבר הראשון אינדקס 0, לשני אינדקס 1 ולשלישי אינדקס 2.

כדי לגשת לאיבר, כותבים את שם הרשימה ואחריו את האינדקס בתוך סוגריים מרובעים: scores[1]. הביטוי מחזיר את הערך שבמיקום הזה, ואפשר להדפיס אותו, לשמור אותו במשתנה או להשתמש בו בחישוב.

קריאת איבר לפי האינדקס

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

scores = [80, 90, 100]
print(scores[0])
second_score = scores[1]
print(second_score)

פלט

80
90

הביטוי scores[0] מחזיר את הציון הראשון, 80. הביטוי scores[1] מחזיר את הציון השני, 90, שאותו שומרים במשתנה second_score. האינדקס מתאר את המיקום; הערך הוא הנתון שנמצא באותו מיקום. קריאת איבר אינה מוציאה אותו מהרשימה.

אותו תחביר משמש גם ברשימה של מחרוזות:

names = ["Dana", "Noam", "Yael"]
print(names[1])

פלט

Noam

קיבלנו את השם השלם "Noam", משום שהוא האיבר השני ברשימה.

גישה מהסוף

אפשר לספור גם מסוף הרשימה באמצעות אינדקס שלילי: -1 מציין את האיבר האחרון, -2 את האיבר שלפניו, וכן הלאה.

scores = [80, 90, 100]
print(scores[-1])
print(scores[-2])

פלט

100
90

כך אפשר לקרוא את האיבר האחרון בלי לחשב מראש את האינדקס שלו מתחילת הרשימה.

שינוי ערך של איבר

כדי להחליף ערך במיקום קיים, כותבים את הגישה לאיבר משמאל לסימן ההשמה =, ואת הערך החדש מימין.

scores = [80, 90, 100]
scores[1] = 95
print(scores)

פלט

[80, 95, 100]

האיבר השני השתנה מ־90 ל־95. שאר האיברים נשארו כפי שהיו, ואורך הרשימה עדיין שלושה איברים.

גישה למיקום שאינו קיים

ברשימה באורך 3, האינדקסים מתחילת הרשימה הם 0, 1 ו־2. ניסיון לקרוא את scores[3] גורם לשגיאת IndexError, משום שאין איבר רביעי.

scores = [80, 90, 100]
print(scores[3])

פלט

IndexError: list index out of range

מוצגת השורה האחרונה של הודעת השגיאה. בכלליות, האינדקס האחרון מתחילת רשימה שאינה ריקה הוא len(scores) - 1. ברשימה ריקה אין אינדקס תקין — גם גישה ל־scores[0] תיכשל. השמה לאינדקס שאינו קיים אינה מוסיפה איבר; הוספת איברים תוסבר בהמשך.

הגודל של הרשימה משתנה

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

scores = [80, 90]
print(len(scores))
scores.append(100)
print(scores)
print(len(scores))
del scores[0]
print(scores)
print(len(scores))

פלט

2
[80, 90, 100]
3
[90, 100]
2

append מוסיפה איבר לסוף הרשימה, ו־del scores[0] מוחקת את האיבר במיקום הראשון. האורך השתנה מ־2 ל־3 ושוב ל־2; לאחר המחיקה, הערך 90 נמצא באינדקס 0.

הוספה והסרה

כדי לעדכן את אוסף הנתונים, אפשר להוסיף איברים לרשימה ולהסיר ממנה איברים קיימים. פעולה על רשימה נכתבת אחרי נקודה, למשל scores.append(5): שם הרשימה, נקודה, שם הפעולה וסוגריים עם הערכים שמעבירים לה. משתמשים כאן בפעולות המוכנות של הרשימה; יצירת מחלקות משלנו תילמד בהמשך.

append מוסיפה לסוף, insert מוסיפה לפני אינדקס נתון. remove מסירה לפי ערך; pop מסירה לפי אינדקס ומחזירה את האיבר שהוסר. ללא אינדקס, pop מסירה את האחרון.

colors = ["blue", "red", "white"]
colors.append("yellow")
colors.insert(1, "green")
colors.remove("red")
last = colors.pop()
print(colors)
print(last)

פלט

['blue', 'green', 'white']
yellow

remove מסירה רק את ההופעה הראשונה. clear() מרוקנת את הרשימה. הסרת ערך שאינו קיים או פנייה לאינדקס שאינו קיים גורמות לשגיאה.

חיתוך — slicing

לעיתים לא צריך את כל הרשימה אלא רק קטע ממנה: שלושת הציונים הראשונים, או כל האיברים מהאמצע ועד הסוף. חיתוך (slicing) מאפשר לבחור חלק מאיברי הרשימה ולקבל אותם כרשימה חדשה. התחביר הוא sequence[start:stop:step]. אינדקס ההתחלה נכלל, אינדקס הסיום אינו נכלל. אפשר להשמיט גבולות ולהשתמש בצעד ברירת המחדל 1.

scores = [10, 20, 30, 40, 50]
print(scores[1:4])
print(scores[:3])
print(scores[3:])
print(scores[::2])
print(scores[::-1])

פלט

[20, 30, 40]
[10, 20, 30]
[40, 50]
[10, 30, 50]
[50, 40, 30, 20, 10]

scores[1:4] מחזיר את האיברים באינדקסים 1, 2 ו־3 (האינדקס 4 אינו נכלל). כשמשמיטים את ההתחלה, החיתוך מתחיל מהאיבר הראשון; כשמשמיטים את הסיום, הוא נמשך עד הסוף. הצעד 2 בוחר כל איבר שני, וצעד שלילי -1 עובר על הרשימה מהסוף להתחלה. חיתוך של רשימה יוצר רשימה חדשה. חיתוך עובד גם על מחרוזות.

סריקה לפי ערך

פעולה שכיחה מאוד היא לעבור על כל איברי הרשימה, למשל כדי להדפיס את כל הציונים או לחשב את הממוצע שלהם. לשם כך משתמשים בלולאת for שלמדנו בפרק הלולאות, הפעם על רשימה במקום על range. בכל סיבוב של הלולאה, המשתנה score מקבל את הערך של האיבר הבא ברשימה. כך סורקים כאשר צריך לקרוא את הערכים, בלי להשתמש במיקומם.

scores = [10, 20, 30]
for score in scores:
    print(score)

פלט

10
20
30

סריקה לפי אינדקס

לפעמים לא מספיק לדעת מהו הערך; צריך לדעת גם היכן הוא נמצא, למשל כדי לשנות אותו ברשימה. במקרה כזה הלולאה עוברת על האינדקסים במקום על הערכים. בכל סיבוב, המשתנה i מקבל אינדקס. len(scores) מחזירה את אורך הרשימה, ו־range(len(scores)) נותנת את האינדקסים מ־0 ועד האורך פחות אחד. כדי להגיע לערך משתמשים ב־scores[i].

scores = [10, 20, 30]
for i in range(len(scores)):
    print(i, scores[i])

פלט

0 10
1 20
2 30

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

scores = [10, 20, 30]
for i in range(len(scores)):
    scores[i] += 1
print(scores)

פלט

[11, 21, 31]

בשתי הדרכים עוברים על אותם איברים ובאותו סדר. בסריקה לפי ערך מקבלים את האיבר; בסריקה לפי אינדקס מקבלים את מיקומו. השמה למשתנה score בדוגמת המספרים אינה מעדכנת את הרשימה; ההשמה scores[i] = ... משנה את האיבר ברשימה.

צירוף והעתקה

צירוף מאפשר לאחד איברים משתי רשימות: אפשר ליצור רשימה חדשה או להרחיב רשימה קיימת.

scores = [10, 20, 30]
combined = scores + [40, 50]
scores.extend([60, 70])
print(combined)
print(scores)

פלט

[10, 20, 30, 40, 50]
[10, 20, 30, 60, 70]

+ יוצרת רשימה חדשה; extend משנה את הרשימה הקיימת. append מוסיפה ערך אחד, ואילו extend מוסיפה בנפרד כל איבר מהרצף שנמסר לה. פעולות כמו append ו־extend משנות את הרשימה ומחזירות None, ולכן לא כותבים scores = scores.append(5).

לעיתים רוצים עותק של רשימה, למשל כדי לשמור את מצב הלוח לפני מהלך ולנסות עליו שינויים בלי לפגוע במקור. השמה רגילה אינה מספיקה לכך, כפי שמראה הדוגמה הבאה: same ו־original הם שני שמות לאותה רשימה, ואילו copied היא רשימה נפרדת.

original = [10, 20]
same = original
copied = original.copy()
original.append(30)
print(same)
print(copied)

פלט

[10, 20, 30]
[10, 20]

השמה אינה מעתיקה רשימה. ההוספה ל־original מופיעה גם ב־same, כי זו אותה רשימה; copied נשארה כפי שהייתה. copy() יוצרת העתקה שטחית: הרשימה החיצונית חדשה, אך רשימות פנימיות, אם קיימות, עדיין משותפות. להעתקה נפרדת גם של מבנים מקוננים משתמשים ב־deepcopy.

בלוח הבא כל איבר הוא רשימה שמייצגת שורה. board[0][0] פונה לאיבר הראשון בשורה הראשונה. הפקודה from copy import deepcopy מייבאת את פעולת ההעתקה העמוקה מהמודול copy:

from copy import deepcopy

board = [[0, 0], [0, 0]]
shallow = board.copy()
saved = deepcopy(board)
board[0][0] = 1
print(shallow)
print(saved)

פלט

[[1, 0], [0, 0]]
[[0, 0], [0, 0]]

השינוי בתא מופיע גם ב־shallow, כי השורה הפנימית משותפת. ב־saved נוצרו גם רשימות פנימיות נפרדות. העתקה בתיעוד Python.

אינדקס וערך יחד — enumerate

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

animals = ["Dog", "Cat", "Bird"]
for index, animal in enumerate(animals):
    print(index, animal)

פלט

0 Dog
1 Cat
2 Bird

מרצף טקסט לרשימה

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

print("a,b,c,d,e".split(","))

פלט

['a', 'b', 'c', 'd', 'e']

רשימה של רשימות

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

grades = [[80, 90], [70, 100]]
print(grades[1][0])
for row in grades:
    print(sum(row))

פלט

70
170
170

sum מחברת את המספרים ברשימה. לסריקה לפי מיקום נשתמש בשני אינדקסים:

for i in range(len(grades)):
    for j in range(len(grades[i])):
        print(i, j, grades[i][j])

רשימות פנימיות יכולות להיות באורכים שונים; לכן אורך השורה הוא len(grades[i]). כשמייצגים מטריצה מלבנית, מקפידים שכל השורות באותו אורך.

חישובים ומיון

אפשר לסכם את המספרים ברשימה או לסדר את איבריה לפי ערכם.

len, sum, min ו־max מספקות אורך, סכום וקיצונים. sorted מחזירה רשימה ממוינת חדשה; sort משנה את הרשימה עצמה ומחזירה None.

numbers = [3, 1, 2]
ordered = sorted(numbers)
print(numbers)
print(ordered)
numbers.sort(reverse=True)
print(numbers)

פלט

[3, 1, 2]
[1, 2, 3]
[3, 2, 1]

אחרי sorted, הרשימה numbers נשארה בסדר המקורי והרשימה הממוינת נשמרה ב־ordered. לעומת זאת numbers.sort(reverse=True) מיינה את numbers עצמה, מהגדול לקטן. min ו־max דורשות כאן רשימה שאינה ריקה. בפרק שוויון וזהות נבחין בין שינוי רשימה להעתקתה.

יצירת רשימה בביטוי — list comprehension

לעיתים קרובות בונים רשימה חדשה מתוך רצף קיים: עוברים על האיברים, בוחרים חלק מהם ומחשבים מכל אחד ערך חדש. עד כה עשינו זאת בלולאה: יוצרים רשימה ריקה, ובכל איטרציה מוסיפים אליה ערך באמצעות append. פייתון מציעה תחביר מקוצר לאותה פעולה, list comprehension, שנכתב בתוך סוגריים מרובעים:

new_list = [expression for item in iterable if condition]

קוראים אותו משמאל לימין: עבור כל item ברצף, אם התנאי מתקיים, הכנס לרשימה את ערך הביטוי. חלק התנאי אינו חובה. נתחיל בביטוי בלי תנאי, שמכפיל כל מספר בטווח:

doubles = [x * 2 for x in range(10)]
print(doubles)

פלט

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

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

evens = [x for x in range(20) if x % 2 == 0]
print(evens)

text = "Hello World"
uppercase_letters = [char for char in text if char.isupper()]
print(uppercase_letters)

פלט

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
['H', 'W']

הביטוי [2 * value for value in numbers if value > 0] עושה בשורה אחת את מה שדרש לולאה שלמה: מסנן את החיוביים ומכפיל אותם. כאשר הביטוי נעשה ארוך או מקונן, לולאה רגילה עם append תהיה ברורה יותר.

תרגיל: רשימת המספרים הראשוניים

נכתוב פונקציה is_prime שמחזירה True אם המספר ראשוני, ונשתמש בה כתנאי כדי לבנות את רשימת כל המספרים הראשוניים הקטנים מ־100. מספר ראשוני הוא מספר גדול מ־1 שמתחלק רק ב־1 ובעצמו, ולכן הפונקציה מחזירה False מיד עבור 0 ו־1:

def is_prime(num):
    if num < 2:
        return False
    for x in range(2, num):
        if num % x == 0:
            return False
    return True

primes = [num for num in range(100) if is_prime(num)]
print(primes)

פלט

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

הפונקציה is_prime היא פונקציה רגילה עם לולאה ותנאי, כפי שלמדנו בפרק א.7, והביטוי שבונה את הרשימה משתמש בה כתנאי: כל מספר בטווח נכנס לרשימה רק אם הפונקציה החזירה עבורו True.