Примеры идут от базового добавления/удаления элементов к сортировке по ключу и агрегатам. Весь вывод в комментариях получен реальным запуском кода, а не подобран по памяти.
Пример 1. append(), extend(), insert() — три способа что-то добавить
Три метода добавления решают разные задачи: один объект в конец, несколько элементов в конец, один объект в середину. Разница особенно заметна на append() со списком внутри — он не «разворачивает» аргумент.
numbers = [1, 2, 3]
numbers.append(4)
print(numbers) # [1, 2, 3, 4]
numbers.extend([5, 6])
print(numbers) # [1, 2, 3, 4, 5, 6]
numbers.insert(1, "new")
print(numbers) # [1, 'new', 2, 3, 4, 5, 6]
nested = [1, 2, 3]
nested.append([4, 5])
print(nested) # [1, 2, 3, [4, 5]]
Что происходит: append(4) добавляет ровно один объект. extend([5, 6]) перебирает список-аргумент и добавляет его элементы по одному. А вот nested.append([4, 5]) кладёт список [4, 5] как единый вложенный элемент — это не ошибка, а прямое следствие того, что append() никогда не разбирает аргумент на части.
Пример 2. Ловушка: append() вместо extend() на практике
Самая частая путаница — попытка «дописать список заказов» через append(). Код не падает, но результат и длина списка оказываются не такими, как ожидалось.
orders = [("apple", 3), ("banana", 5)]
new_orders = [("cherry", 2), ("date", 7)]
# неправильно: append кладёт весь список одним элементом
wrong = list(orders)
wrong.append(new_orders)
print(wrong)
# [('apple', 3), ('banana', 5), [('cherry', 2), ('date', 7)]]
print(len(wrong)) # 3 — а не 4!
# правильно: extend добавляет элементы по одному
right = list(orders)
right.extend(new_orders)
print(right)
# [('apple', 3), ('banana', 5), ('cherry', 2), ('date', 7)]
print(len(right)) # 4
Разбор: после append(new_orders) в списке 3 элемента, а не 4 — третий элемент сам оказался списком. Проверка простая: если после добавления ожидаете рост длины ровно на количество новых записей — нужен extend(); если нужен один новый «пункт» (даже если это список) — append().
Пример 3. insert() с отрицательным и «слишком большим» индексом
Индекс в insert() не обязан попадать в границы списка — Python аккуратно приводит его к ближайшей допустимой позиции вместо ошибки.
letters = ["a", "b", "c", "d"]
letters.insert(-1, "X")
print(letters) # ['a', 'b', 'c', 'X', 'd'] — перед последним элементом
letters.insert(100, "END")
print(letters) # ['a', 'b', 'c', 'X', 'd', 'END'] — индекс > len -> в конец
letters.insert(-100, "START")
print(letters) # ['START', 'a', 'b', 'c', 'X', 'd', 'END'] — индекс < -len -> в начало
Что важно: insert(-1, x) вставляет перед последним элементом, а не после — это частая ошибка при попытке «дописать в конец». Индексы за пределами диапазона не вызывают IndexError: слишком большой уводит вставку в конец списка, слишком маленький (по модулю) — в начало.
Пример 4. remove(), pop(), clear() — что удаляется и что возвращается
Три метода удаления отличаются тем, что именно они принимают — значение или индекс — и что возвращают.
fruits = ["apple", "banana", "cherry", "banana"]
fruits.remove("banana")
print(fruits) # ['apple', 'cherry', 'banana']
last_fruit = fruits.pop()
print(last_fruit) # banana
print(fruits) # ['apple', 'cherry']
try:
fruits.remove("kiwi")
except ValueError as error:
print("ValueError:", error)
# ValueError: list.remove(x): x not in list
fruits.clear()
print(fruits) # []
Что происходит: remove("banana") удаляет только первое совпадение по значению. pop() без аргумента забирает последний элемент и возвращает его — это удобно, когда удалённое значение нужно использовать дальше. Оба метода возвращают None, кроме pop(); и оба падают, если элемента/индекса нет.
Пример 5. remove() vs pop() vs del vs срез — четыре способа убрать элемент
В реальном коде удаление элемента можно записать четырьмя разными способами. У них разный интерфейс и разный побочный эффект на исходный список.
source = ["a", "b", "c", "d", "e"]
by_remove = list(source)
by_remove.remove("c")
print(by_remove) # ['a', 'b', 'd', 'e'] — по значению
by_pop = list(source)
removed = by_pop.pop(2)
print(by_pop, removed) # ['a', 'b', 'd', 'e'] c — по индексу, элемент возвращается
by_del = list(source)
del by_del[2]
print(by_del) # ['a', 'b', 'd', 'e'] — по индексу, ничего не возвращает
by_slice = list(source)
tail = by_slice[3:]
print(tail) # ['d', 'e'] — срез создаёт НОВЫЙ список
print(by_slice) # ['a', 'b', 'c', 'd', 'e'] — оригинал не тронут
Разбор: remove() ищет по значению и падает с ValueError, если его нет. pop(i) и del list[i] работают по индексу и падают с IndexError, но только pop() отдаёт удалённый элемент. Срез [3:] вообще ничего не удаляет — он строит новый список из части старого, поэтому by_slice остался нетронутым.
del не входит в материалы этого урока (там разбирались только методы списка), но часто встречается рядом с ними в реальном коде. Синтаксис и поведение сверьте по разделу The del statement.
Пример 6. index() и count() — поиск и подсчёт
Оба метода читают список, не изменяя его. index() ищет позицию, count() — количество совпадений.
fruits = ["apple", "banana", "cherry", "banana", "cherry"]
print(fruits.index("banana")) # 1
print(fruits.index("banana", 2)) # 3 — поиск начиная с индекса 2
print(fruits.count("cherry")) # 2
print(fruits.count("orange")) # 0 — count не падает на отсутствующем значении
try:
fruits.index("kiwi")
except ValueError as error:
print("ValueError:", error)
# ValueError: 'kiwi' is not in list
Что происходит: у "banana" два вхождения — на индексах 1 и 3. Второй вызов index("banana", 2) ищет только начиная с индекса 2 и поэтому находит второе вхождение. count(), в отличие от index(), никогда не падает — на отсутствующем значении просто возвращает 0.
Пример 7. sort() меняет список на месте, sorted() — нет: почему это важно
Разница между «на месте» и «новый список» становится критичной внутри функций: функция, которая незаметно мутирует переданный ей список, — источник трудноуловимых багов в остальном коде.
def top_three(items):
# sorted() не трогает items вызывающего кода
return sorted(items, reverse=True)[:3]
scores = [4, 9, 1, 7, 3]
best = top_three(scores)
print(best) # [9, 7, 4]
print(scores) # [4, 9, 1, 7, 3] — исходный список не изменился
numbers = [4, 1, 7, 2, 9]
numbers.sort()
print(numbers) # [1, 2, 4, 7, 9]
result = numbers.sort()
print(result) # None — sort() ничего не возвращает
Что важно: top_three() использует sorted() именно потому, что не должна портить список, который ей передали, — вызывающий код по-прежнему видит scores в исходном порядке. А numbers.sort() возвращает None: если написать numbers = numbers.sort(), список будет потерян.
Пример 8. key= для сортировки: строки без учёта регистра, словари, кортежи
key — функция, которая вычисляет значение для сравнения для каждого элемента. Один и тот же параметр работает и для простых строк, и для пар «ключ — значение» словаря.
words = ["Banana", "apple", "kiwi", "Cherry"]
print(sorted(words)) # ['Banana', 'Cherry', 'apple', 'kiwi'] — по Unicode
print(sorted(words, key=str.lower))
# ['apple', 'Banana', 'Cherry', 'kiwi'] — без учёта регистра
prices = {"apple": 120, "banana": 45, "cherry": 300}
print(sorted(prices.items(), key=lambda item: item[1]))
# [('banana', 45), ('apple', 120), ('cherry', 300)]
people = [("Alice", 25), ("Bob", 20), ("Carol", 30)]
print(sorted(people, key=lambda person: person[1]))
# [('Bob', 20), ('Alice', 25), ('Carol', 30)]
Разбор: без key заглавные буквы сортируются раньше строчных (Unicode-порядок), поэтому "Banana" оказывается перед "apple". key=str.lower убирает эту зависимость от регистра. prices.items() превращает словарь в список кортежей (ключ, значение), и key=lambda item: item[1] сортирует именно по значению, а не по ключу.
Пример 9. Устойчивость сортировки и сортировка по нескольким ключам
Сортировка в Python устойчива (stable): элементы с одинаковым ключом сохраняют исходный взаимный порядок. На этом свойстве строится сортировка по нескольким критериям через кортеж.
students = [("Bob", 4), ("Alice", 5), ("Ann", 4), ("Zoe", 5)]
by_grade = sorted(students, key=lambda s: s[1], reverse=True)
print(by_grade)
# [('Alice', 5), ('Zoe', 5), ('Bob', 4), ('Ann', 4)]
# внутри одинаковой оценки порядок из исходного списка сохранён
orders = [("banana", 2, 45), ("apple", 1, 120), ("apple", 3, 120), ("banana", 1, 45)]
by_name_then_qty = sorted(orders, key=lambda o: (o[0], -o[1]))
print(by_name_then_qty)
# [('apple', 3, 120), ('apple', 1, 120), ('banana', 2, 45), ('banana', 1, 45)]
# сначала по названию, внутри одного названия — по количеству по убыванию
Что происходит: у Alice и Zoe одинаковая оценка 5, но Alice шла раньше в исходном списке — и осталась раньше в результате. Во втором примере key возвращает кортеж (название, -количество): сортировка идёт сначала по первому полю, а при совпадении — по второму; минус переворачивает направление только для количества, не трогая порядок по названию.
Пример 10. min(), max() с key= и default=
min() и max() принимают тот же параметр key, что и сортировка, а параметр default спасает от падения на пустой коллекции.
words = ["apple", "banana", "kiwi", "grape"]
print(min(words, key=len)) # kiwi
print(max(words, key=len)) # banana
empty = []
print(min(empty, default="список пуст")) # список пуст
try:
min(empty)
except ValueError as error:
print("ValueError:", error)
# ValueError: min() arg is an empty sequence
tuples = [(3, 6), (1, 7, 9), (12, 5), (1, 3, 7)]
tuples.sort(key=max, reverse=True)
print(tuples)
# [(12, 5), (1, 7, 9), (1, 3, 7), (3, 6)]
Что происходит: без default вызов min()/max() на пустой коллекции — это ValueError, а не None и не 0. В последнем примере key=max — необычный, но рабочий приём: ключом сортировки становится максимальный элемент каждого вложенного кортежа.
Пример 11. sum() со start и ограничение по типам
sum() складывает элементы итерируемого объекта, начиная со значения start (по умолчанию 0). Работает с числами, но не со строками.
scores = [4, 1, 7, 2, 9]
print(sum(scores)) # 23
print(sum(scores, 1000)) # 1023 — start прибавляется к сумме
prices = (19.99, 5.50, 2.25)
print(sum(prices)) # 27.74 — sum() работает и с кортежами
try:
sum(["a", "b", "c"])
except TypeError as error:
print("TypeError:", error)
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
Разбор: start=1000 — не порог и не ограничение, а именно начальное значение, к которому прибавляются элементы. Для строк sum() не работает — Python явно требует "".join(...) вместо сложения строк через + в цикле сложения.
Пример 12. reversed() как итератор и способы скопировать список
reversed(), в отличие от .reverse(), не трогает исходный список и возвращает итератор — его нужно обернуть в list() или "".join(), чтобы увидеть элементы.
letters = "Python"
reversed_obj = reversed(letters)
print(reversed_obj) # <reversed object at 0x...>
print("".join(reversed_obj)) # nohtyP
numbers = [1, 2, 3, 4]
print(list(reversed(numbers))) # [4, 3, 2, 1]
print(numbers) # [1, 2, 3, 4] — оригинал не тронут
original = [1, 2, 3]
copy_a = original.copy()
copy_b = original[:]
copy_a.append(4)
copy_b.append(5)
print(original) # [1, 2, 3] — независим от копий
print(copy_a) # [1, 2, 3, 4]
print(copy_b) # [1, 2, 3, 5]
Что важно: list.copy() и срез [:] — два равнозначных способа получить независимую копию списка: изменения в copy_a и copy_b никак не повлияли на original. Без копирования (copy_a = original) обе переменные указывали бы на один и тот же список.
list.copy() в материалах урока не разбирался. Это поверхностная (shallow) копия — вложенные списки внутри неё всё равно останутся общими. Подробности — в разделе copy — Shallow and deep copy operations.
Что делать дальше
Если методы count()/index() кажутся смутно знакомыми — они уже встречались у кортежей в уроке 24. Списки кортежей из урока 25 — хороший полигон для sort(key=...). Дальше переходите к заданиям; если что-то не запустилось так, как ожидалось — загляните в типичные ошибки или в исходные примеры репозитория.