# Slicing, Mutable מול Immutable, ו-is מול ==<no value>

## מה נלמד?

- **Slicing** — איך חותכים חלק מרשימה, tuple או מחרוזת.
- **Mutable** מול **Immutable** — מה ניתן לשינוי ומה לא, ולמה זה משנה.
- **הפניות** (References) — מה קורה כשכותבים `b = a`.
- **`is` מול `==`** — "אותו ערך" מול "אותו אובייקט בדיוק".

## למה צריך את זה?

- **Slicing** נמצא בכל מקום: 10 התוצאות הראשונות, 3 התווים האחרונים של מספר כרטיס (`****1234`), "עמוד 2" של רשימה.
- **Mutable** הוא מקור לאחד הבאגים המבלבלים ביותר ב-Python: "שיניתי רשימה אחת, ופתאום השתנתה גם השנייה!". הבנה של הנושא הזה מונעת שעות של דיבאג. ובנושא OOP, אובייקטים של מחלקות מתנהגים בדיוק כך.

## הסבר מהבסיס

### Slicing

**Slicing** (חיתוך) יוצר **רצף חדש** מחלק של רצף קיים:

```text
sequence[start:stop:step]
```

- **start** — מאיפה מתחילים (כולל). ברירת מחדל: מההתחלה.
- **stop** — איפה עוצרים (**לא כולל!**, כמו ב-`range`). ברירת מחדל: עד הסוף.
- **step** — קפיצה. ברירת מחדל: 1. צעד **שלילי** = מהסוף להתחלה.

```text
letters = ["a", "b", "c", "d", "e", "f"]
index:      0    1    2    3    4    5
negative:  -6   -5   -4   -3   -2   -1
```

Slicing **לא זורק `IndexError`** גם אם חורגים מהטווח. הוא פשוט מחזיר את מה שיש.

### Mutable מול Immutable

| Immutable (לא ניתן לשינוי) | Mutable (ניתן לשינוי) |
|---|---|
| `int`, `float`, `bool`, `str`, `tuple` | `list`, `dict`, `set` |

**Immutable** = אי אפשר לשנות את האובייקט **עצמו**. כל "שינוי" יוצר אובייקט **חדש**:

```python
name = "dana"
name.upper()      # returns a NEW string "DANA"; name is still "dana"
name = name.upper()   # now name points to the new string
```

**Mutable** = אפשר לשנות את האובייקט **במקום** (`append`, `d[k] = v`), והוא נשאר אותו אובייקט.

### משתנה = הפניה (Reference)

זוכרים את אנלוגיית המדבקה? **משתנה הוא מדבקה עם שם, שמודבקת על אובייקט.** הוא לא "מכיל" את האובייקט, הוא **מצביע** עליו.

```python
a = [1, 2, 3]
b = a          # NOT a copy! b is a second sticker on the SAME list
```

```text
a ──┐
    ├──► [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:`.

{{< box "analogy" "אנלוגיה: תאומים" >}}
שני תאומים זהים: **`==`** ‏←‏ `True` (נראים אותו דבר). **`is`** ‏←‏ `False` (שני אנשים שונים). לעומת זאת, "דנה" ו"הבת של רותי" הם **אותו אדם** (`is` ‏←‏ `True`): שני שמות לאותו אחד. אם דנה מסתפרת, גם "הבת של רותי" מסופרת.
{{< /box >}}

## תחביר

```python
# 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`:

```python
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:])
```

**פלט:**

```text
['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`:

```python
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)
```

**פלט:**

```text
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 כפרמטר לפונקציה

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

```python
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` כדי "לגבות" רשימה לפני שינוי.**

```python
backup = cart
cart.clear()
print(backup)   # [] - the "backup" is empty too!
```

תיקון: `backup = cart.copy()`.

**הטעות 2: `is` להשוואת ערכים.**

```python
a = [1, 2, 3]
b = [1, 2, 3]
if a is b:
    print("same")
```

לא מודפס כלום. שתי רשימות שוות **בערך** הן שני אובייקטים נפרדים. **לערכים, תמיד `==`.**

(עם מספרים קטנים ומחרוזות קצרות, `is` "עובד במקרה", כי Python ממחזרת אובייקטים קטנים. עם מספרים גדולים זה כבר לא קורה. **אל תסמכו על זה.**)

**הטעות 3: לשכוח שמתודות של str לא משנות.**

```python
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

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

### תרגיל 1: Slicing

```python
letters = ["a", "b", "c", "d", "e", "f"]
```

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

**פלט צפוי:**

```text
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
```

{{< solution >}}
```python
letters = ["a", "b", "c", "d", "e", "f"]

print(letters[:3])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
```

```text
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
```

`[start:stop:step]` — ה-`stop` **לא נכלל**. `[::-1]` מחזיר **עותק** הפוך — המקור לא השתנה.
{{< /solution >}}

### תרגיל 2: להסתיר מספר כרטיס

כתבו פונקציה `mask_card(number)` שמקבלת מספר כרטיס כמחרוזת, ומחזירה כוכביות במקום כל הספרות חוץ מארבע האחרונות.

```python
print(mask_card("4580123498761234"))
```

**פלט צפוי:**

```text
************1234
```

{{< solution >}}
```python
def mask_card(number):
    hidden = len(number) - 4
    return "*" * hidden + number[-4:]


print(mask_card("4580123498761234"))
```

```text
************1234
```

Slicing עובד גם על מחרוזות: `number[-4:]` הן ארבע הספרות האחרונות.
{{< /solution >}}

### תרגיל 3: הגיבוי שהתרוקן

```python
cart = ["milk", "bread"]
backup = cart
cart.clear()
print(backup)
```

1. מה יודפס, ולמה?
2. תקנו כך שהגיבוי יישמר.

{{< solution >}}
1. יודפס `[]`. **`backup = cart` אינו העתקה** — שני השמות מצביעים על **אותה** רשימה. ריקון דרך `cart` נראה גם דרך `backup`.
2. יוצרים עותק:

```python
cart = ["milk", "bread"]
backup = cart.copy()
cart.clear()
print(backup)
print(backup is cart)
```

```text
['milk', 'bread']
False
```

`is` בודק אם זה **אותו אובייקט** — אחרי `copy()` כבר לא.
{{< /solution >}}

### תרגיל 4: Mutable כפרמטר

```python
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)
```

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

{{< solution >}}
```python
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)
```

```text
[80, 90, 100]
dana
```

- רשימה היא **Mutable**: `append` שינה את **אותה** רשימה ש-`my_grades` מצביע עליה.
- מחרוזת היא **Immutable**: `name.upper()` יצר מחרוזת **חדשה**, וההשמה רק שינתה לאן `name` המקומי מצביע. `my_name` לא הושפע.
{{< /solution >}}

### תרגיל 5: פלינדרום ועמודים

1. כתבו `is_palindrome(text)` — האם המילה זהה מימין ומשמאל (בלי להתחשב באותיות גדולות).
2. כתבו `paginate(items, page, page_size)` שמחזירה את הפריטים של עמוד מסוים (העמוד הראשון הוא 1).

```python
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))
```

**פלט צפוי:**

```text
True
False
['c', 'd']
['e']
```

{{< solution >}}
```python
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))
```

```text
True
False
['c', 'd']
['e']
```

Slicing לא נכשל כשה-`stop` גדול מאורך הרשימה — הוא פשוט מחזיר את מה שיש. לכן העמוד האחרון מחזיר `['e']`.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **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`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [Common Sequence Operations](https://docs.python.org/3/builtins/stdtypes.html#common-sequence-operations) — Slicing: s[i:j:k]
- [Data model — Objects, values and types](https://docs.python.org/3/reference/datamodel.html#objects-values-and-types) — Mutable מול Immutable
- [Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not) — is מול ==

<!-- docs:end -->
