Уроки курса
1 Введение в Python и философия дзен
30 мин
2 Переменные и динамическая типизация
30 мин
3 Базовые типы данных: числа, строки и булевы значения
30 мин
4 Изменяемые и неизменяемые объекты (Mutable vs Immutable)
30 мин
5 Форматирование строк и f-строки
30 мин
6 Углубленная работа со списками
30 мин
7 Кортежи и их особенности
30 мин
8 Словари под капотом
30 мин
9 Множества и математические операции
30 мин
10 Генераторы списков (List Comprehensions)
30 мин
11 Генераторы словарей и множеств
30 мин
12 Встроенные функции для коллекций
30 мин
13 Условные операторы и логические выражения
30 мин
14 Циклы while и управление потоком
30 мин
15 Итерация с циклом for
30 мин
16 Конструкции for...else и while...else
30 мин
17 Функции enumerate и zip
30 мин
18 Создание собственных функций (def)
30 мин
19 Позиционные и именованные аргументы
30 мин
20 Проблема изменяемых аргументов по умолчанию
30 мин
21 Произвольное число аргументов (*args и **kwargs)
30 мин
22 Область видимости переменных (LEGB)
30 мин
23 Анонимные функции (lambda)
30 мин
24 Функции высшего порядка
30 мин
25 Замыкания (Closures)
30 мин
26 Введение в объектно-ориентированное программирование
30 мин
27 Атрибуты классов и экземпляров
30 мин
28 Магический метод __init__
30 мин
29 Методы экземпляра
30 мин
30 Инкапсуляция и сокрытие данных
30 мин
31 Декоратор @property
30 мин
32 Наследование классов
30 мин
33 Переопределение методов и функция super()
30 мин
34 Полиморфизм в Python
30 мин
35 Магические методы строк (__str__ и __repr__)
30 мин
36 Обработка исключений (try-except)
30 мин
37 Блоки else и finally
30 мин
38 Генерация собственных исключений (raise)
30 мин
39 Открытие и чтение файлов
30 мин
40 Запись данных в файлы
30 мин
41 Контекстные менеджеры (with)
30 мин
42 Работа с форматом JSON
30 мин
43 Модули и импорты
30 мин
44 Полезные модули стандартной библиотеки
30 мин
45 Модуль datetime
30 мин
46 Модуль collections
30 мин
47 Виртуальные окружения (venv)
30 мин
48 Установка сторонних пакетов через pip
30 мин
49 Организация структуры Python-проекта
30 мин
50 Финальный проект: создание приложения
30 мин

Углубленная работа со списками

Изучение продвинутых срезов (slices), а также методов добавления, удаления и сортировки элементов.

Прогресс урока: 0%

Добро пожаловать в шестой урок курса! Сегодня мы переходим от базового понимания списков к уровню Intermediate. Вы уже знаете, что список (list) в Python — это изменяемая, упорядоченная коллекция элементов. Однако на практике для создания эффективных и быстрых программ этого знания недостаточно. Нам предстоит заглянуть "под капот" интерпретатора CPython и понять, как именно списки хранятся в памяти, почему одни операции выполняются мгновенно, а другие заставляют программу "тормозить".

Внутреннее устройство списков

В Python списки реализованы не как традиционные связные списки из информатики, а как массивы переменной длины (динамические массивы указателей). Это означает, что сам список хранит не сами объекты (числа, строки), а лишь ссылки (адреса памяти) на эти объекты. Именно благодаря этой архитектурной особенности списки в Python могут содержать элементы абсолютно разных типов данных одновременно: одно целое число, одну строку, один вложенный словарь и так далее. Указателю всё равно, на какой тип данных он ссылается, его размер всегда фиксирован.

Когда вы создаете пустой список, Python выделяет в оперативной памяти небольшой блок под этот массив указателей. По мере добавления новых элементов (например, через метод append()), массив заполняется. Когда место заканчивается, Python не просто добавляет одну ячейку. Он применяет стратегию Over-allocation (сверхвыделение): выделяет новый, значительно больший блок памяти, копирует туда старые указатели и добавляет новый. Это позволяет сделать операцию добавления элемента в конец списка очень быстрой (амортизированная сложность O(1)).

Понимание этой механики критически важно для разработчика уровня Intermediate. Если вы будете постоянно вставлять элементы в начало списка или удалять их из середины, интерпретатору придется сдвигать все последующие указатели вправо или влево. При размере списка в несколько миллионов элементов это приведет к катастрофическому падению производительности (сложность O(N)). Мы будем подробно разбирать алгоритмическую сложность каждого метода, чтобы вы могли писать по-настоящему эффективный и профессиональный код, соответствующий стандартам индустрии.

Опасность изменяемых объектов: Mutable Default Arguments

Прежде чем мы перейдем к срезам, давайте разберем классическую ошибку, с которой сталкиваются 90% начинающих разработчиков при работе со списками. Эта ситуация настолько типична, что мы оформим ее в виде симуляции Code Review (обзора кода) между Junior (младшим) и Senior (старшим) разработчиками.

Ситуация: Джуниор написал функцию для добавления студента в список группы. Если список не передан, функция должна создать новый пустой список и добавить студента туда.

Диалог:
Junior: Я создал функцию add_student(name, students=[]). Если я не передаю список, она должна создавать новый. Но почему-то, когда я вызываю её второй раз для другой группы, туда добавляются студенты из первой!
Senior: Это классическая ошибка "Mutable Default Argument" (Изменяемый аргумент по умолчанию). В Python аргументы по умолчанию создаются и вычисляются ровно один раз — в момент определения функции интерпретатором (когда модуль загружается), а не при каждом ее вызове.

Поскольку список — это изменяемый (mutable) объект, функция сохраняет ссылку на этот единственный созданный список. Каждый раз, когда ты вызываешь функцию без второго аргумента, она обращается к одному и тому же участку памяти. Первый студент добавляется туда. Второй студент добавляется к первому, и так далее. Это приводит к утечкам данных и непредсказуемому поведению программы.

Как правильно это исправить? Золотое правило Python: никогда не используйте изменяемые объекты (списки, словари, множества) в качестве значений по умолчанию для аргументов функции. Вместо этого используйте None. Внутри функции проверяйте, равен ли аргумент None, и если да — создавайте новый пустой список локально внутри функции. Таким образом, при каждом вызове без списка будет создаваться абсолютно новый, независимый объект в памяти. Это краеугольный камень безопасного программирования на Python.

python
# ОШИБКА: Изменяемый аргумент по умолчанию
def add_student_bad(name, students=[]):
    students.append(name)
    return students

print(add_student_bad("Alice")) # ['Alice']
print(add_student_bad("Bob"))   # ['Alice', 'Bob'] - ОШИБКА! Ожидалось ['Bob']

# ПРАВИЛЬНОЕ РЕШЕНИЕ
def add_student_good(name, students=None):
    if students is None:
        students = [] # Создаем новый список ПРИ КАЖДОМ вызове
    students.append(name)
    return students

print(add_student_good("Alice")) # ['Alice']
print(add_student_good("Bob"))   # ['Bob'] - Работает корректно!

Почему использование `students=[]` в качестве аргумента по умолчанию вызывает накопление данных при последующих вызовах функции?

Продвинутые срезы (Slices) — это одна из самых элегантных и мощных особенностей языка Python. Срез позволяет не просто извлекать части списка, но и делать это с определенным шагом, разворачивать списки, а также изменять их содержимое на лету. Синтаксис среза выглядит так: my_list[start:stop:step], где start — индекс начала (включительно), stop — индекс конца (не включительно!), а step — шаг выборки.

Анатомия среза и шаг (step)

Большинство новичков используют только start и stop. Например, lst[1:4] вернет элементы с 1 по 3 индекс. Однако параметр step открывает огромные возможности. По умолчанию шаг равен 1. Если задать шаг 2, Python выберет каждый второй элемент. Это невероятно полезно при работе с данными, где значения чередуются (например, список формата: имя, возраст, имя, возраст).

Более того, шаг может быть отрицательным! Если задать step=-1, Python пойдет по списку в обратном направлении. Конструкция lst[::-1] является идиоматическим ("Pythonic") способом развернуть список. В отличие от метода .reverse(), который изменяет оригинальный список на месте (in-place), срез [::-1] создает новый список в памяти, оставляя исходный нетронутым. Это критически важное различие при проектировании функций, которые не должны иметь побочных эффектов (side effects).

При использовании отрицательного шага логика start и stop меняется местами. Если вы пишете lst[5:1:-1], вы говорите интерпретатору: "Начни с индекса 5 и иди назад до индекса 1 (не включая его)". Понимание этих нюансов позволяет писать очень компактный и читаемый код, избавляя вас от необходимости писать громоздкие циклы for или while для обхода элементов в нестандартном порядке. Запомните: срезы всегда создают поверхностную копию (shallow copy) извлекаемых элементов.

python
data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 1. Извлечение каждого второго элемента (четные числа из этого списка)
evens = data[0:10:2] # или просто data[::2]
print(f"Четные: {evens}") # [0, 2, 4, 6, 8]

# 2. Извлечение с нечетных позиций
odds = data[1::2]
print(f"Нечетные: {odds}") # [1, 3, 5, 7, 9]

# 3. Разворот списка (создается копия)
reversed_data = data[::-1]
print(f"Развернутый: {reversed_data}") # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# 4. Сложный срез с отрицательным шагом
# От индекса 7 до индекса 2 (не включительно) в обратном порядке
complex_slice = data[7:2:-1]
print(f"Сложный срез: {complex_slice}") # [7, 6, 5, 4, 3]

Дан список `nums = [10, 20, 30, 40, 50]`. Что вернет выражение `nums[3:0:-1]`?

Синтаксис среза Описание действия Пример результата для [0,1,2,3,4]
lst[:] Создает полную поверхностную копию всего списка. [0, 1, 2, 3, 4]
lst[2:] От индекса 2 до самого конца. [2, 3, 4]
lst[:3] От начала до индекса 3 (не включительно). [0, 1, 2]
lst[-2:] Последние два элемента (с конца). [3, 4]

Присваивание через срезы (Slice Assignment) — это магия Python, которая позволяет заменять, удалять или вставлять целые последовательности элементов в список одной строкой кода. Это действие изменяет исходный список (работает in-place), что делает его мощным инструментом для массовой модификации данных. При этом размер заменяемого среза не обязательно должен совпадать с размером новой последовательности.

Замена и удаление частей списка

Если вы присваиваете значение срезу, Python удаляет старые элементы, попавшие в срез, и вставляет на их место элементы из новой итерируемой последовательности. Например, если у вас есть lst = [1, 2, 3, 4], и вы пишете lst[1:3] = [8, 9, 10], элементы 2, 3 будут удалены, а на их место встанут 8, 9, 10. Результатом станет список [1, 8, 9, 10, 4]. Как видите, список увеличился в размере. Это кардинально отличается от замены одного элемента по индексу (lst[1] = 8).

Точно так же можно массово удалять элементы. Если присвоить срезу пустой список [], элементы просто исчезнут. Выражение lst[1:3] = [] удалит элементы с индексами 1 и 2. Это работает быстрее и выглядит элегантнее, чем вызов оператора del для каждого элемента в цикле. Более того, с помощью срезов можно вставлять элементы, не удаляя старые. Для этого нужно указать срез нулевой длины. Например, lst[1:1] = ['A', 'B'] вставит 'A' и 'B' начиная с индекса 1, сдвинув все остальные элементы вправо.

Важное предостережение: Если вы используете срез с шагом, отличным от 1 (например, lst[::2] = [10, 20]), то количество элементов в заменяемом срезе обязано строго совпадать с количеством элементов в новой последовательности. Если они не совпадут, интерпретатор Python выбросит исключение ValueError: attempt to assign sequence of size X to extended slice of size Y. Это связано с тем, что при расширенном срезе элементы не идут подряд, и Python не может произвольно сдвигать части списка, сохраняя логику чередования.

python
inventory = ['меч', 'щит', 'зелье', 'карта', 'факел']

# 1. Замена нескольких элементов (длины могут не совпадать)
inventory[1:3] = ['броня', 'шлем', 'кольцо']
print(inventory) 
# ['меч', 'броня', 'шлем', 'кольцо', 'карта', 'факел']

# 2. Вставка элементов без удаления (срез нулевой длины)
inventory[0:0] = ['магия'] 
print(inventory)
# ['магия', 'меч', 'броня', 'шлем', 'кольцо', 'карта', 'факел']

# 3. Удаление части списка путем присвоения пустого списка
inventory[-2:] = [] # Удаляем последние 2 элемента
print(inventory)
# ['магия', 'меч', 'броня', 'шлем', 'кольцо']

# 4. Ошибка при расширенном срезе (раскомментируйте для проверки)
nums = [1, 2, 3, 4, 5]
# nums[::2] = [100, 200] # Вызовет ValueError, т.к. мест 3 (индексы 0, 2, 4), а элементов 2

Что произойдет при выполнении кода: `lst = [1, 2, 3]; lst[1:1] = [99]`?

Методы добавления элементов: append, extend и insert. Для расширения списков в Python предусмотрено несколько специализированных методов. Выбор правильного метода критически важен как для читаемости кода, так и для его производительности (скорости выполнения).

Сравнение методов: append() vs extend()

Метод append(element) берет переданный ему аргумент и добавляет его в конец списка как единый объект. Независимо от того, передали вы число, строку или другой список, длина исходного списка увеличится ровно на 1. Например, если к списку [1, 2] применить .append([3, 4]), мы получим вложенную структуру: [1, 2, [3, 4]]. Как мы уже обсуждали, эта операция выполняется за O(1) времени.

Метод extend(iterable) работает принципиально иначе. Он ожидает на вход итерируемый объект (список, кортеж, строку, множество) и "распаковывает" его, добавляя каждый элемент по отдельности в конец текущего списка. Если мы применим .extend([3, 4]) к [1, 2], мы получим плоский список [1, 2, 3, 4]. Сложность этой операции O(K), где K — количество элементов в добавляемой коллекции. Использование extend() всегда работает быстрее, чем добавление элементов в цикле for с помощью append(), так как extend() реализован на уровне языка C и оптимизирует выделение памяти.

Метод insert() и операторы сложения

Метод insert(index, element) вставляет объект перед указанным индексом. Как упоминалось ранее, эта операция очень "дорогая" с точки зрения ресурсов (O(N)), если вы вставляете элемент в начало или середину длинного списка. Прибегайте к insert() только тогда, когда это абсолютно необходимо и списки имеют небольшой размер.

Также списки можно объединять с помощью оператора +. Однако важно понимать разницу между lst1 + lst2 и lst1 += lst2. Оператор сложения + создает в памяти совершенно новый список, оставляя исходные неизменными. Это требует выделения нового блока памяти и копирования всех элементов из обоих списков. В отличие от этого, оператор += для списков работает в точности как метод extend(): он модифицирует левый список in-place (на месте), что значительно эффективнее по памяти и скорости работы.

python
# Демонстрация разницы между append, extend и insert
base_list_1 = [1, 2, 3]
base_list_1.append([4, 5])
print("После append:", base_list_1) # [1, 2, 3, [4, 5]] - длина 4

base_list_2 = [1, 2, 3]
base_list_2.extend([4, 5])
print("После extend:", base_list_2) # [1, 2, 3, 4, 5] - длина 5

base_list_3 = ['A', 'C', 'D']
# Вставка 'B' на индекс 1 (между 'A' и 'C')
base_list_3.insert(1, 'B')
print("После insert:", base_list_3) # ['A', 'B', 'C', 'D']

# Оператор + vs +=
lst_a = [10, 20]
lst_b = [30, 40]
new_lst = lst_a + lst_b # lst_a не изменился, создан новый объект
print(new_lst) # [10, 20, 30, 40]

lst_a += [99] # Аналог extend. lst_a изменился in-place
print(lst_a) # [10, 20, 99]

Напишите название метода, который используется для вставки элемента в строго определенную позицию (по индексу) внутри списка.

Методы удаления элементов: remove, pop, del и clear. Как и в случае с добавлением, Python предоставляет богатый арсенал для очистки списков. Каждый инструмент предназначен для своей конкретной задачи, и неправильный выбор может привести к трудноуловимым багам (ошибкам) в вашем приложении.

Удаление по значению: метод remove()

Метод remove(value) ищет первое совпадение указанного значения в списке и удаляет его. Важно: он удаляет только первое найденное совпадение, а не все элементы с таким значением. Алгоритмическая сложность этой операции — O(N), так как интерпретатору нужно сначала найти элемент линейным поиском, а затем сдвинуть все элементы справа от него, чтобы заполнить образовавшуюся пустоту. Если элемента с таким значением в списке нет, метод remove() выбросит исключение ValueError. Поэтому перед удалением профессиональные разработчики часто проверяют наличие элемента с помощью оператора in, либо оборачивают код в блок try-except.

Удаление по индексу: метод pop() и оператор del

Если вы знаете точную позицию элемента, который нужно удалить, используйте метод pop(index). Его уникальная особенность в том, что он не просто удаляет элемент, но и возвращает его значение. Это позволяет сразу сохранить удаленный элемент в переменную (например, при реализации структуры данных "стек" или "очередь"). Если не передавать индекс, pop() удалит и вернет последний элемент списка. В этом случае сложность операции составит O(1), что делает её идеальной для высоконагруженных систем. Если же передать индекс 0 (pop(0)), сложность возрастет до O(N).

Оператор del (сокращение от delete) — это инструкция самого языка Python. Он не возвращает значение, но позволяет удалять как отдельные элементы по индексу (del lst[2]), так и целые срезы (del lst[1:4]), и даже переменные целиком. Это очень мощный инструмент управления памятью. Наконец, если вам нужно полностью обнулить список, сохранив сам объект в памяти, используйте метод clear(). Это эквивалентно записи del lst[:] или lst[:] = [].

python
tasks = ['купить хлеб', 'помыть посуду', 'вынести мусор', 'помыть посуду']

# 1. remove() - удаляет ПЕРВОЕ совпадение
tasks.remove('помыть посуду')
print(tasks) # ['купить хлеб', 'вынести мусор', 'помыть посуду']
# Если написать tasks.remove('полить цветы') -> ValueError!

# Безопасное удаление:
if 'полить цветы' in tasks:
    tasks.remove('полить цветы')

# 2. pop() - удаляет и ВОЗВРАЩАЕТ элемент
last_task = tasks.pop() # по умолчанию удаляет последний
print(f"Завершена задача: {last_task}") # Завершена задача: помыть посуду
print(tasks) # ['купить хлеб', 'вынести мусор']

# 3. del - удаление по индексу или срезу
users = ['Admin', 'Moderator', 'User1', 'User2']
del users[1] # Удаляем 'Moderator'
print(users) # ['Admin', 'User1', 'User2']

# 4. clear() - очистка всего списка
users.clear()
print(users) # []

Какая операция удаления из списка будет выполнена быстрее всего (имеет сложность O(1))?

Частая ошибка новичков: изменение списка во время итерации. Давайте обсудим один из самых коварных подводных камней, который часто встречается на собеседованиях на позицию Junior Python Developer. Представьте задачу: у вас есть список чисел, и вам нужно удалить из него все четные числа. Очевидным (но в корне неверным) решением кажется создание цикла for, который пройдет по списку и вызовет метод remove() или del для четных элементов.

Почему нельзя удалять элементы в цикле for?

Цикл for в Python использует внутренний счетчик (итератор), который отслеживает текущий индекс. Когда вы удаляете элемент, размер списка уменьшается, и все оставшиеся элементы сдвигаются влево на одну позицию. Однако итератор об этом не знает! Он просто переходит к следующему индексу. В результате элемент, который находился сразу после удаленного, смещается на текущий индекс, а итератор шагает дальше, тем самым пропуская элемент.

Рассмотрим пример: список [2, 4, 6]. На первой итерации (индекс 0) мы видим 2. Это четное число, мы его удаляем. Список становится [4, 6]. На следующей итерации итератор переходит к индексу 1. Но на индексе 1 теперь находится число 6! Число 4 было полностью пропущено циклом. В результате код отработает без ошибок синтаксиса, но выдаст абсолютно неверный результат. Это классический пример логической ошибки.

Как правильно удалять элементы пачками?

Существует три правильных подхода (Pythonic way) к решению этой проблемы:
1. Итерация по копии списка. Вы можете перебирать элементы копии (созданной через срез lst[:]), а удалять элементы из оригинала. Так как итерация идет по неизменяемой копии, сдвиг элементов в оригинале не сломает цикл.
2. Итерация с конца. Если использовать цикл while или for с отрицательным шагом (с конца в начало), удаление элемента сместит только те элементы, которые мы уже прошли. Текущий индекс не собьется.
3. Создание нового списка (List Comprehension). Это самый предпочитаемый, современный и быстрый способ. Вместо того чтобы мучительно удалять ненужное из старого списка, мы просто создаем новый список, в который включаем только нужные элементы. Об этом мощном инструменте мы поговорим в следующем блоке.

python
numbers = [2, 4, 5, 6, 8]

# ПЛОХОЙ СПОСОБ (Пропустит некоторые элементы)
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)
print("Плохой результат:", numbers) # [4, 5, 8] - 4 и 8 остались!

# ХОРОШИЙ СПОСОБ №1: Итерация по копии [:]
numbers_copy = [2, 4, 5, 6, 8]
for n in numbers_copy[:]: # Перебираем копию
    if n % 2 == 0:
        numbers_copy.remove(n) # Удаляем из оригинала
print("Хороший результат 1:", numbers_copy) # [5]

# ИДЕАЛЬНЫЙ СПОСОБ №2: List Comprehension (Списковое включение)
numbers_ideal = [2, 4, 5, 6, 8]
filtered_numbers = [n for n in numbers_ideal if n % 2 != 0]
print("Идеальный результат:", filtered_numbers) # [5]

Генераторы списков (List Comprehensions) и продвинутая фильтрация. Вспомним материалы из сборника практических упражнений уровня Intermediate. Там мы затрагивали концепцию "Pythonic way" — использование встроенных возможностей языка для краткости и скорости выполнения кода. List Comprehension — это идеальный пример такого подхода. По сути, это синтаксический сахар для создания новых списков, который объединяет в себе цикл for и условный оператор if в одну компактную строку.

Синтаксис и преимущества

Базовый синтаксис выглядит так: [выражение for элемент in итерируемый_объект if условие].
Важно понимать, что генератор списков не просто делает код короче (одна строка вместо четырех). Он работает значительно быстрее (на 30-50%), чем эквивалентный цикл for с использованием .append(). Причина кроется во внутреннем устройстве: при выполнении List Comprehension интерпретатор CPython заранее оценивает необходимый размер нового массива и не тратит время на многократные вызовы функции append на каждой итерации, выполняя добавление элементов на уровне байт-кода языка Си.

Практическое применение (Project-Based Learning)

Давайте разберем задание 1.1 из нашего архива: "Продвинутая фильтрация списков". Представьте, что вы разрабатываете бэкенд для интернет-магазина. База данных возвращает вам список словарей, где каждый словарь — это товар с ключами name, price и stock (остаток на складе). Ваша задача: получить список названий товаров, которые стоят дороже 500 рублей и в данный момент есть в наличии (остаток больше нуля).

Если решать эту задачу классическим циклом, код получится громоздким. С помощью List Comprehension мы можем элегантно извлечь нужный ключ p['name'], одновременно проверив два условия: p['price'] > 500 and p['stock'] > 0. Такой код легко читается, быстро выполняется и является стандартом де-факто в профессиональной разработке на Python.

python
# Задача: получить имена дорогих товаров в наличии
products = [
    {"name": "Laptop", "price": 1200, "stock": 5},
    {"name": "Mouse", "price": 400, "stock": 10},
    {"name": "Monitor", "price": 800, "stock": 0}, # Нет в наличии
    {"name": "Keyboard", "price": 600, "stock": 2}
]

# Классический подход (медленно и многословно)
result_classic = []
for p in products:
    if p['price'] > 500 and p['stock'] > 0:
        result_classic.append(p['name'])

# Pythonic way: List Comprehension (быстро и элегантно)
expensive_in_stock = [p['name'] for p in products if p['price'] > 500 and p['stock'] > 0]

print("Результат фильтрации:", expensive_in_stock) 
# Ожидаемый вывод: ['Laptop', 'Keyboard']

Задание

Используя знания о List Comprehension, потренируйтесь в мысленном составлении кода (Active Recall). Ваша задача — создать список квадратов только четных чисел из заданного диапазона от 0 до 9.

  • Определите итерируемый объект: функция range(10) сгенерирует числа от 0 до 9.
  • Сформулируйте условие фильтрации: число должно быть четным (x % 2 == 0).
  • Определите выражение (результат для нового списка): вам нужен квадрат числа (x ** 2).
  • Объедините все в синтаксис List Comprehension: [x**2 for x in range(10) if x % 2 == 0].
10 баллов

Продвинутая сортировка: sort() vs sorted() и lambda-функции. Умение правильно и быстро сортировать данные — критический навык программиста. В Python алгоритм сортировки называется Timsort (назван в честь создателя Тима Питерса). Это гибридный, стабильный алгоритм, который комбинирует сортировку слиянием (Merge Sort) и сортировку вставками (Insertion Sort). Он невероятно эффективен для данных, содержащих уже отсортированные подмассивы (что часто бывает в реальной жизни), и имеет сложность O(N log N) в худшем случае, и O(N) в лучшем.

Метод sort() и встроенная функция sorted()

Первое, что нужно усвоить: разницу между списковым методом .sort() и встроенной функцией sorted().
Метод list.sort() сортирует список in-place (на месте). Он изменяет сам объект списка и возвращает None. Это работает быстро, так как не требует выделения памяти под новый список. Однако, если вам нужно сохранить исходный порядок данных, этот метод не подойдет.

Функция sorted(iterable) принимает любую итерируемую последовательность (не только список, но и кортеж, строку, словарь), сортирует её и возвращает абсолютно новый список. Исходные данные остаются нетронутыми. За эту безопасность приходится платить памятью. Обе эти функции принимают два важных именованных аргумента: reverse=True (для сортировки по убыванию) и key (функция для кастомной сортировки).

Аргумент key и Lambda-функции

Самое интересное начинается при использовании параметра key. Этот параметр принимает функцию, которая будет применена к каждому элементу списка перед тем, как произойдет сравнение. Возвращаемое значение этой функции станет "весом" элемента при сортировке.

В задании 2.2 нашего архива требовалось отсортировать список кортежей: [(1, 'Z'), (2, 'A'), (3, 'M')]. По умолчанию Python сравнивает кортежи поэлементно: сначала первые элементы (1, 2, 3), затем вторые. Но что если нам нужно отсортировать их по второму элементу (буквам)? Здесь на помощь приходят анонимные lambda-функции. Lambda — это короткая, одноразовая функция, состоящая из одного выражения. Запись key=lambda x: x[1] означает: "возьми элемент списка (в нашем случае кортеж) и используй для сравнения его элемент с индексом 1". Timsort отсортирует кортежи в алфавитном порядке их вторых элементов: 'A', 'M', 'Z'.

python
# 1. Базовая сортировка in-place и создание нового списка
nums = [4, 1, 3, 2]
new_sorted_nums = sorted(nums) # nums не изменился!
nums.sort(reverse=True) # nums изменился, сортировка по убыванию
print("sorted():", new_sorted_nums) # [1, 2, 3, 4]
print("sort():", nums) # [4, 3, 2, 1]

# 2. Продвинутая сортировка с key и lambda (Задание 2.2)
data = [(1, 'Z', 100), (2, 'A', 50), (3, 'M', 200)]

# Сортировка по второму элементу кортежа (буква, индекс 1)
sorted_by_letter = sorted(data, key=lambda item: item[1])
print("По букве:", sorted_by_letter) 
# [(2, 'A', 50), (3, 'M', 200), (1, 'Z', 100)]

# Сортировка по третьему элементу (число, индекс 2) по убыванию
sorted_by_value = sorted(data, key=lambda item: item[2], reverse=True)
print("По значению (убывание):", sorted_by_value)
# [(3, 'M', 200), (1, 'Z', 100), (2, 'A', 50)]

# 3. Сортировка строк по их длине (без lambda, используем встроенную функцию len)
words = ["яблоко", "киви", "банан", "арбуз"]
words.sort(key=len)
print("По длине слова:", words)
# ['киви', 'банан', 'арбуз', 'яблоко']

Что делает код `sorted(['bb', 'a', 'ccc'], key=len, reverse=True)`?

Завершающий проект: Управление Инвентарем (Project-Based Learning). Настало время собрать все полученные знания воедино. Проектное обучение (PBL) доказывает, что навыки закрепляются лучше всего, когда они применяются к реальным, комплексным задачам, а не к изолированным примерам. Мы смоделируем небольшую систему управления инвентарем (складом) персонажа в RPG-игре.

Сценарий

У нас есть персонаж, чей инвентарь представлен списком словарей. Каждый словарь содержит item_id (уникальный идентификатор), name (название предмета), weight (вес предмета) и rarity (редкость: от 1 до 5). Нам нужно реализовать логику, которая:
1. Удаляет все предметы с редкостью 1 (мусор).
2. Добавляет новый лут (добычу), используя эффективный метод объединения списков.
3. Сортирует финальный инвентарь по весу предметов (от самых тяжелых к самым легким), чтобы игрок знал, что выкинуть при перегрузе.
4. Извлекает названия трех самых тяжелых предметов с помощью срезов для отображения на экране.

В следующем блоке кода будет представлено готовое решение. Ваша задача — внимательно изучить его, обращая внимание на то, как избегается ошибка изменения списка во время итерации (используется List Comprehension), как применяется extend для массового добавления, как lambda помогает сортировать словари по ключу, и как срезы элегантно решают задачу выборки Top-N элементов. Это квинтэссенция Pythonic-кода, которую ожидают увидеть от разработчика уровня Intermediate на собеседованиях и ревью.

python
# Исходный инвентарь персонажа
inventory = [
    {"id": 1, "name": "Ржавый меч", "weight": 5.0, "rarity": 1},
    {"id": 2, "name": "Зелье лечения", "weight": 0.5, "rarity": 3},
    {"id": 3, "name": "Старые сапоги", "weight": 1.2, "rarity": 1},
    {"id": 4, "name": "Щит отражения", "weight": 8.0, "rarity": 4}
]

# Новая добыча из сундука
new_loot = [
    {"id": 5, "name": "Золотое кольцо", "weight": 0.1, "rarity": 5},
    {"id": 6, "name": "Тяжелый молот", "weight": 15.0, "rarity": 3}
]

# ШАГ 1: Избавляемся от мусора (rarity == 1) через List Comprehension
# Создаем новый чистый список
inventory = [item for item in inventory if item["rarity"] > 1]

# ШАГ 2: Добавляем новый лут эффективно (extend)
inventory.extend(new_loot)

# ШАГ 3: Сортируем инвентарь по весу по убыванию (in-place сортировка)
# Используем lambda для извлечения ключа 'weight'
inventory.sort(key=lambda x: x["weight"], reverse=True)

# ШАГ 4: Получаем названия 3-х самых тяжелых предметов
# Сначала делаем срез первых трех элементов, затем вытаскиваем 'name'
heaviest_items = [item["name"] for item in inventory[:3]]

print("Топ-3 тяжелых предмета:", heaviest_items)
# Вывод: Топ-3 тяжелых предмета: ['Тяжелый молот', 'Щит отражения', 'Зелье лечения']

Подведение итогов урока. Вы проделали огромную работу, перейдя от базового понимания списков к глубокому осознанию их внутреннего устройства. Давайте закрепим ключевые тезисы (Active Recall):

  • Память и архитектура: Списки в Python — это массивы ссылок (указателей). Благодаря сверхвыделению памяти (over-allocation), добавление в конец (append) работает за O(1). Вставка в начало (insert(0, val)) и удаление первого элемента — дорогие операции (O(N)), которых следует избегать в больших коллекциях.
  • Срезы (Slices): Это не только способ чтения (lst[start:stop:step]), но и мощный инструмент для замены и удаления фрагментов списка на месте (lst[1:3] = [9, 9]). Срезы всегда создают поверхностную копию (shallow copy) данных.
  • Ошибки и Мутации: Использование изменяемых объектов (списков) в качестве дефолтных аргументов функций — путь к утечкам данных. Изменение списка во время итерации по нему через цикл for приведет к пропуску элементов. Используйте List Comprehension.
  • Добавление и Удаление: extend() и += работают in-place и значительно быстрее цикла с append() при объединении коллекций. pop() позволяет не только удалить, но и вернуть элемент.
  • Сортировка: Алгоритм Timsort обеспечивает высокую производительность. Используйте .sort() для мутации оригинала и sorted() для создания отсортированной копии. Параметр key в сочетании с lambda-функциями позволяет создавать кастомные правила сортировки любой сложности.

Эти знания — надежный фундамент для написания чистого, оптимизированного и "питонячьего" кода. В следующих уроках мы применим похожий глубокий подход к словарям и множествам, где концепция хеширования откроет нам еще более быстрые способы работы с данными.