# Срезы, изменяемые и неизменяемые типы, is и ==<no value>

## Что мы изучим?

- **Срезы** (Slicing) — как вырезать часть списка, tuple или строки.
- **Изменяемые** (Mutable) и **неизменяемые** (Immutable) типы — что можно изменить, а что нельзя, и почему это важно.
- **Ссылки** (references) — что происходит, когда вы пишете `b = a`.
- **`is` и `==`** — «одно и то же значение» и «абсолютно тот же объект».

## Зачем это нужно?

- **Срезы** встречаются повсюду: первые 10 результатов, последние 3 символа номера карты (`****1234`), «страница 2» списка.
- **Изменяемость** — источник одного из самых запутанных багов в Python: «Я изменил один список, и вдруг изменился и второй!». Понимание этой темы экономит часы отладки. А в теме ООП объекты классов ведут себя именно так.

## Объяснение с основ

### Срезы

**Срез** (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
```

Срез **не выбрасывает `IndexError`**, даже если выйти за границы. Он просто возвращает то, что есть.

### Изменяемые и неизменяемые

| 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`), и он остаётся тем же объектом.

### Переменная = ссылка

Помните аналогию с наклейкой? **Переменная — это наклейка с именем, приклеенная к объекту.** Она не «содержит» объект, она **указывает** на него.

```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 ──┘
```

Если список **изменяемый** и вы меняете его через `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 | Срезы работают и для **строк** |
| `"*" * 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")` — мы изменили **сам список** (он изменяемый), поэтому `original` тоже «изменился». На самом деле он не менялся отдельно — это один и тот же список.
- `copy = original.copy()` — **новый** список с теми же элементами, на который изменение не влияет.
- `copy == original` → `False`, потому что после append значения уже разные. **До** append было `True`, но `is` всё равно было `False`.
- **Строки** неизменяемы: `other.upper()` создал **новую** строку, и `other` прикрепился к ней. `name` не затронут. С неизменяемыми типами такой баг **невозможен**.

### Изменяемый объект как параметр функции

Ровно тот же принцип действует для функций. Параметр — ещё одна «наклейка» на **тот же** объект:

```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 "
```

Строка неизменяема, а новый результат был выброшен. Исправление: `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: Срезы

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

Используя только срезы, напечатайте: первые три, последние два, каждый второй элемент и список в обратном порядке.

**Ожидаемый вывод:**

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

Срезы работают и для строк: `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: Изменяемый объект как параметр

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

- Список **изменяемый**: `append` изменил **тот же** список, на который указывает `my_grades`.
- Строка **неизменяемая**: `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']
```

Срез не падает, когда `stop` больше длины списка, — он просто возвращает то, что есть. Поэтому последняя страница возвращает `['e']`.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Срез** `[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)
- [Data model — Objects, values and types](https://docs.python.org/3/reference/datamodel.html#objects-values-and-types)
- [Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not)

<!-- docs:end -->
