Slicing, Mutable מול Immutable, ו-is מול ==
- לחתוך רשימות ומחרוזות עם Slicing, כולל צעד שלילי להיפוך
- לסווג טיפוסים כ-Mutable או Immutable ולהסביר את ההבדל
- להבין מה קורה כששני משתנים מצביעים על אותו אובייקט, ואיך יוצרים עותק
- להבחין בין
==(אותו ערך) לביןis(אותו אובייקט)
מה נלמד?
- Slicing — איך חותכים חלק מרשימה, tuple או מחרוזת.
- Mutable מול Immutable — מה ניתן לשינוי ומה לא, ולמה זה משנה.
- הפניות (References) — מה קורה כשכותבים
b = a. isמול==— “אותו ערך” מול “אותו אובייקט בדיוק”.
למה צריך את זה?
- Slicing נמצא בכל מקום: 10 התוצאות הראשונות, 3 התווים האחרונים של מספר כרטיס (
****1234), “עמוד 2” של רשימה. - Mutable הוא מקור לאחד הבאגים המבלבלים ביותר ב-Python: “שיניתי רשימה אחת, ופתאום השתנתה גם השנייה!”. הבנה של הנושא הזה מונעת שעות של דיבאג. ובנושא OOP, אובייקטים של מחלקות מתנהגים בדיוק כך.
הסבר מהבסיס
Slicing
Slicing (חיתוך) יוצר רצף חדש מחלק של רצף קיים:
sequence[start:stop:step]- start — מאיפה מתחילים (כולל). ברירת מחדל: מההתחלה.
- stop — איפה עוצרים (לא כולל!, כמו ב-
range). ברירת מחדל: עד הסוף. - step — קפיצה. ברירת מחדל: 1. צעד שלילי = מהסוף להתחלה.
letters = ["a", "b", "c", "d", "e", "f"]
index: 0 1 2 3 4 5
negative: -6 -5 -4 -3 -2 -1Slicing לא זורק IndexError גם אם חורגים מהטווח. הוא פשוט מחזיר את מה שיש.
Mutable מול Immutable
| Immutable (לא ניתן לשינוי) | Mutable (ניתן לשינוי) |
|---|---|
int, float, bool, str, tuple | list, dict, set |
Immutable = אי אפשר לשנות את האובייקט עצמו. כל “שינוי” יוצר אובייקט חדש:
name = "dana"
name.upper() # returns a NEW string "DANA"; name is still "dana"
name = name.upper() # now name points to the new stringMutable = אפשר לשנות את האובייקט במקום (append, d[k] = v), והוא נשאר אותו אובייקט.
משתנה = הפניה (Reference)
זוכרים את אנלוגיית המדבקה? משתנה הוא מדבקה עם שם, שמודבקת על אובייקט. הוא לא “מכיל” את האובייקט, הוא מצביע עליו.
a = [1, 2, 3]
b = a # NOT a copy! b is a second sticker on the SAME lista ──┐
├──► [1, 2, 3] one list in memory, two names
b ──┘אם ה-list Mutable ומשנים אותה דרך b, השינוי נראה גם דרך a. זה אותו אובייקט.
== מול is
| אופרטור | שואל | דוגמה |
|---|---|---|
== | האם הערכים שווים? | [1, 2] == [1, 2] → True |
is | האם זה אותו אובייקט בדיוק בזיכרון? | [1, 2] is [1, 2] → False (שתי רשימות נפרדות) |
id(x) מחזירה מזהה ייחודי של אובייקט. a is b שקול ל-id(a) == id(b).
כלל: משווים ערכים עם ==. את is משתמשים כמעט רק להשוואה ל-None: if result is None:.
שני תאומים זהים: == ← True (נראים אותו דבר). is ← False (שני אנשים שונים). לעומת זאת, “דנה” ו"הבת של רותי" הם אותו אדם (is ← True): שני שמות לאותו אחד. אם דנה מסתפרת, גם “הבת של רותי” מסופרת.
תחביר
# slicing
items[2:5] # indexes 2, 3, 4
items[:3] # first 3
items[-3:] # last 3
items[::2] # every second item
items[::-1] # reversed copy
items[:] # a full (shallow) copy
# copies of mutable objects
copy1 = items[:]
copy2 = items.copy()
copy3 = list(items)
d_copy = my_dict.copy()
# identity vs equality
a == b # same value?
a is b # same object?
x is None # the correct way to check for None
id(a) # the object's unique identity numberדוגמת קוד ראשונה
week-2/slicing_demo.py:
letters = ["a", "b", "c", "d", "e", "f"]
print(letters[1:4])
print(letters[:2])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
print(letters[2:100])
card_number = "4580123498761234"
print("Card ending in", card_number[-4:])
print("Masked:", "*" * 12 + card_number[-4:])פלט:
['b', 'c', 'd']
['a', 'b']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
['c', 'd', 'e', 'f']
Card ending in 1234
Masked: ************1234פירוק הקוד שורה אחר שורה
| ביטוי | אינדקסים | הסבר |
|---|---|---|
[1:4] | 1, 2, 3 | 4 לא כולל |
[:2] | 0, 1 | מההתחלה, 2 איברים |
[-2:] | -2, -1 | שני האחרונים |
[::2] | 0, 2, 4 | כל איבר שני |
[::-1] | 5 → 0 | צעד שלילי: הפוך |
[2:100] | 2 עד הסוף | חריגה מהטווח לא זורקת שגיאה |
card_number[-4:] | 4 אחרונים | Slicing עובד גם על מחרוזות |
"*" * 12 | — | חזרה על טקסט 12 פעמים |
דוגמה נוספת
הבאג המפורסם — שני שמות לאותה רשימה. week-2/aliasing.py:
original = ["milk", "bread"]
same = original
copy = original.copy()
same.append("eggs")
print("original:", original)
print("same: ", same)
print("copy: ", copy)
print("same == original:", same == original)
print("same is original:", same is original)
print("copy == original:", copy == original)
print("copy is original:", copy is original)
name = "dana"
other = name
other = other.upper()
print("name:", name, "| other:", other)פלט:
original: ['milk', 'bread', 'eggs']
same: ['milk', 'bread', 'eggs']
copy: ['milk', 'bread']
same == original: True
same is original: True
copy == original: False
copy is original: False
name: dana | other: DANAפירוק:
same = original— לא נוצרה רשימה חדשה. יש עכשיו שני שמות לאותה רשימה.same.append("eggs")— שינינו את הרשימה עצמה (היא Mutable), ולכן גםoriginal“השתנתה”. בעצם היא לא השתנתה בנפרד, זו אותה רשימה.copy = original.copy()— רשימה חדשה עם אותם איברים, שלא מושפעת מהשינוי.copy == original→False, כי אחרי ה-append הערכים כבר שונים. לפני ה-append זה היהTrue, אבלisעדיין היהFalse.- מחרוזות הן Immutable:
other.upper()יצרה מחרוזת חדשה, ו-otherהוצמדה אליה.nameלא הושפעה. עם Immutable, הבאג הזה לא יכול לקרות.
Mutable כפרמטר לפונקציה
אותו עיקרון בדיוק חל על פונקציות. הפרמטר הוא עוד “מדבקה” על אותו אובייקט:
def add_bonus(grades):
grades.append(100)
my_grades = [80, 90]
add_bonus(my_grades)
print(my_grades) # [80, 90, 100] - the caller's list changed!לפעמים זה רצוי, ולפעמים זה באג. אם פונקציה לא אמורה לשנות את הקלט, עובדים על עותק: grades = grades.copy(), או בונים רשימה חדשה ומחזירים אותה.
טעות נפוצה
הטעות 1: b = a כדי “לגבות” רשימה לפני שינוי.
backup = cart
cart.clear()
print(backup) # [] - the "backup" is empty too!תיקון: backup = cart.copy().
הטעות 2: is להשוואת ערכים.
a = [1, 2, 3]
b = [1, 2, 3]
if a is b:
print("same")לא מודפס כלום. שתי רשימות שוות בערך הן שני אובייקטים נפרדים. לערכים, תמיד ==.
(עם מספרים קטנים ומחרוזות קצרות, is “עובד במקרה”, כי Python ממחזרת אובייקטים קטנים. עם מספרים גדולים זה כבר לא קורה. אל תסמכו על זה.)
הטעות 3: לשכוח שמתודות של str לא משנות.
email = " Dana@Mail.COM "
email.strip().lower()
print(email) # still " Dana@Mail.COM "מחרוזת Immutable, והתוצאה החדשה נזרקה. התיקון: email = email.strip().lower().
הטעות 4: stop נכלל, כביכול. items[1:3] מחזיר 2 איברים (אינדקסים 1 ו-2), לא 3.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: Slicing
letters = ["a", "b", "c", "d", "e", "f"]בעזרת Slicing בלבד, הדפיסו: את שלושת הראשונים, את שני האחרונים, כל איבר שני, ואת הרשימה הפוכה.
פלט צפוי:
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']הצג פתרון
letters = ["a", "b", "c", "d", "e", "f"]
print(letters[:3])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a'][start:stop:step] — ה-stop לא נכלל. [::-1] מחזיר עותק הפוך — המקור לא השתנה.
תרגיל 2: להסתיר מספר כרטיס
כתבו פונקציה mask_card(number) שמקבלת מספר כרטיס כמחרוזת, ומחזירה כוכביות במקום כל הספרות חוץ מארבע האחרונות.
print(mask_card("4580123498761234"))פלט צפוי:
************1234הצג פתרון
def mask_card(number):
hidden = len(number) - 4
return "*" * hidden + number[-4:]
print(mask_card("4580123498761234"))************1234Slicing עובד גם על מחרוזות: number[-4:] הן ארבע הספרות האחרונות.
תרגיל 3: הגיבוי שהתרוקן
cart = ["milk", "bread"]
backup = cart
cart.clear()
print(backup)- מה יודפס, ולמה?
- תקנו כך שהגיבוי יישמר.
הצג פתרון
- יודפס
[].backup = cartאינו העתקה — שני השמות מצביעים על אותה רשימה. ריקון דרךcartנראה גם דרךbackup. - יוצרים עותק:
cart = ["milk", "bread"]
backup = cart.copy()
cart.clear()
print(backup)
print(backup is cart)['milk', 'bread']
Falseis בודק אם זה אותו אובייקט — אחרי copy() כבר לא.
תרגיל 4: Mutable כפרמטר
def add_bonus(grades):
grades.append(100)
def shout(name):
name = name.upper()
my_grades = [80, 90]
my_name = "dana"
add_bonus(my_grades)
shout(my_name)
print(my_grades)
print(my_name)חזו מה יודפס, והסבירו את ההבדל בין שתי הפונקציות.
הצג פתרון
def add_bonus(grades):
grades.append(100)
def shout(name):
name = name.upper()
my_grades = [80, 90]
my_name = "dana"
add_bonus(my_grades)
shout(my_name)
print(my_grades)
print(my_name)[80, 90, 100]
dana- רשימה היא Mutable:
appendשינה את אותה רשימה ש-my_gradesמצביע עליה. - מחרוזת היא Immutable:
name.upper()יצר מחרוזת חדשה, וההשמה רק שינתה לאןnameהמקומי מצביע.my_nameלא הושפע.
תרגיל 5: פלינדרום ועמודים
- כתבו
is_palindrome(text)— האם המילה זהה מימין ומשמאל (בלי להתחשב באותיות גדולות). - כתבו
paginate(items, page, page_size)שמחזירה את הפריטים של עמוד מסוים (העמוד הראשון הוא 1).
print(is_palindrome("Level"))
print(is_palindrome("hello"))
print(paginate(["a", "b", "c", "d", "e"], 2, 2))
print(paginate(["a", "b", "c", "d", "e"], 3, 2))פלט צפוי:
True
False
['c', 'd']
['e']הצג פתרון
def is_palindrome(text):
cleaned = text.lower()
return cleaned == cleaned[::-1]
def paginate(items, page, page_size):
start = (page - 1) * page_size
return items[start:start + page_size]
print(is_palindrome("Level"))
print(is_palindrome("hello"))
print(paginate(["a", "b", "c", "d", "e"], 2, 2))
print(paginate(["a", "b", "c", "d", "e"], 3, 2))True
False
['c', 'd']
['e']Slicing לא נכשל כשה-stop גדול מאורך הרשימה — הוא פשוט מחזיר את מה שיש. לכן העמוד האחרון מחזיר ['e'].
סיכום
- Slicing
[start:stop:step]מחזיר רצף חדש.stopלא כולל,[::-1]הופך, ואיןIndexError. - Immutable (
int,float,str,tuple,bool): כל “שינוי” יוצר אובייקט חדש. Mutable (list,dict,set): משתנה במקום. - משתנה הוא הפניה.
b = a= שני שמות לאותו אובייקט, לא עותק. עותק:.copy()או[:]. - פונקציה שמקבלת list/dict יכולה לשנות את המקור של מי שקרא לה.
=== אותו ערך.is= אותו אובייקט. ל-Noneמשתמשים ב-is None.
דוקומנטציה רשמית
- Common Sequence Operations — Slicing: s[i:j:k]
- Data model — Objects, values and types — Mutable מול Immutable
- Identity comparisons — is מול ==