Срезы, изменяемые и неизменяемые типы, 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, tuplelist, 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 string

Mutable = объект можно изменить на месте (append, d[k] = v), и он остаётся тем же объектом.

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

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

a = [1, 2, 3]
b = a          # NOT a copy! b is a second sticker on the SAME list
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:.

Аналогия: близнецы

Два одинаковых близнеца: == → 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, 34 не включается
[: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)
  1. Что будет напечатано и почему?
  2. Исправьте так, чтобы резервная копия сохранилась.
Показать решение
  1. Напечатается []. backup = cart — не копия: оба имени указывают на один и тот же список. Очистка через cart видна и через backup.
  2. Создайте копию:
cart = ["milk", "bread"]
backup = cart.copy()
cart.clear()
print(backup)
print(backup is cart)
['milk', 'bread']
False

is проверяет, тот же ли это объект, — после 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: Палиндром и страницы

  1. Напишите is_palindrome(text) — читается ли слово одинаково с обеих сторон (без учёта заглавных букв).
  2. Напишите 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.

Официальная документация