Срезы, изменяемые и неизменяемые типы, is и ==
- Нарезать списки и строки срезами (Slicing), включая отрицательный шаг для разворота
- Относить типы к изменяемым (Mutable) или неизменяемым (Immutable) и объяснять разницу
- Понимать, что происходит, когда две переменные указывают на один объект, и как создать копию
- Различать
==(одно и то же значение) иis(один и тот же объект)
Что мы изучим?
- Срезы (Slicing) — как вырезать часть списка, tuple или строки.
- Изменяемые (Mutable) и неизменяемые (Immutable) типы — что можно изменить, а что нельзя, и почему это важно.
- Ссылки (references) — что происходит, когда вы пишете
b = a. isи==— «одно и то же значение» и «абсолютно тот же объект».
Зачем это нужно?
- Срезы встречаются повсюду: первые 10 результатов, последние 3 символа номера карты (
****1234), «страница 2» списка. - Изменяемость — источник одного из самых запутанных багов в Python: «Я изменил один список, и вдруг изменился и второй!». Понимание этой темы экономит часы отладки. А в теме ООП объекты классов ведут себя именно так.
Объяснение с основ
Срезы
Срез (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 -1Срез не выбрасывает IndexError, даже если выйти за границы. Он просто возвращает то, что есть.
Изменяемые и неизменяемые
| 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), и он остаётся тем же объектом.
Переменная = ссылка
Помните аналогию с наклейкой? Переменная — это наклейка с именем, приклеенная к объекту. Она не «содержит» объект, она указывает на него.
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 ──┘Если список изменяемый и вы меняете его через 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 | Срезы работают и для строк |
"*" * 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")— мы изменили сам список (он изменяемый), поэтомуoriginalтоже «изменился». На самом деле он не менялся отдельно — это один и тот же список.copy = original.copy()— новый список с теми же элементами, на который изменение не влияет.copy == original→False, потому что после append значения уже разные. До append былоTrue, ноisвсё равно былоFalse.- Строки неизменяемы:
other.upper()создал новую строку, иotherприкрепился к ней.nameне затронут. С неизменяемыми типами такой баг невозможен.
Изменяемый объект как параметр функции
Ровно тот же принцип действует для функций. Параметр — ещё одна «наклейка» на тот же объект:
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 "Строка неизменяема, а новый результат был выброшен. Исправление: email = email.strip().lower().
Ошибка 4: думать, что stop включается. items[1:3] возвращает 2 элемента (индексы 1 и 2), а не 3.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Срезы
letters = ["a", "b", "c", "d", "e", "f"]Используя только срезы, напечатайте: первые три, последние два, каждый второй элемент и список в обратном порядке.
Ожидаемый вывод:
['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"))************1234Срезы работают и для строк: 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: Изменяемый объект как параметр
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- Список изменяемый:
appendизменил тот же список, на который указываетmy_grades. - Строка неизменяемая:
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']Срез не падает, когда stop больше длины списка, — он просто возвращает то, что есть. Поэтому последняя страница возвращает ['e'].
Итоги
- Срез
[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.