🧠 Что выведет код
Что выведет каждая строка?
Ответы:
3
True
"a"
3
Если хоть одна строка удивила — читай дальше, тут прячется важное про устройство Python.
Разбор
Ключевой факт: в Python
Внутри
Разберём по строкам:
Третья строка — самая коварная:
Где это реально полезно
Каждое
Где это подводит
🐍Вопросы с собесов -> ProstoPython
print(True + True + True)
print(True == 1)
print(["a", "b", "c"][False])
print(sum([True, False, True, True]))
Что выведет каждая строка?
Ответы:
True
"a"
3
Если хоть одна строка удивила — читай дальше, тут прячется важное про устройство Python.
Разбор
Ключевой факт: в Python
bool — это подкласс `int`. Не «похож на число», а буквально является числом.
isinstance(True, int) # True
True == 1 # True
False == 0 # True
Внутри
True — это 1, False — это 0. Полноценные целые, просто с красивыми именами и печатью.Разберём по строкам:
True + True + True → 1 + 1 + 1 → 3
True == 1 → 1 == 1 → True
lst[False] → lst[0] → "a" (False как индекс = 0!)
sum([T, F, T, T]) → 1 + 0 + 1 + 1 → 3
Третья строка — самая коварная:
False спокойно работает как индекс 0, а True — как 1. lst[True] вернул бы "b".Где это реально полезно
sum булевых значений — это идиома «посчитать, сколько раз условие истинно»:
# сколько чисел больше 10?
count = sum(x > 10 for x in nums)
# сколько строк непустые?
count = sum(bool(s) for s in strings)
Каждое
True добавляет 1, каждое False — 0. Коротко и читаемо, без ручного счётчика в цикле.Где это подводит
bool — подкласс int, поэтому type() == их путает при неаккуратной проверке, а как ключи словаря True и 1 — один и тот же ключ:
d = {1: "один", True: "правда"}
print(d) # {1: 'правда'} — True перезаписал 1 !
print(len(d)) # 1
True и 1 равны и дают одинаковый хеш → для словаря это один ключ.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🎭 Red Flag:
Плохой пример
Выглядит аккуратно: поймали низкоуровневый
Что не так
Когда исключение возникает внутри блока
Формулировка «During handling of the above exception» намекает, что новая ошибка выскочила случайно, во время обработки — как будто в твоём обработчике баг. А ты-то преобразовал ошибку намеренно. Смысл связи потерян.
Хуже, когда исходную ошибку глушат совсем:
Если бы внутри
Как надо
Явно свяжи новое исключение с исходным через
Теперь трейсбек говорит правду о причинно-следственной связи:
«The direct cause» — ты явно сказал: «этот
Когда исходную ошибку стоит подавить
Иногда низкоуровневая причина — шум, и её нужно осознанно скрыть (например, деталь реализации, которую не должен видеть вызывающий). Для этого есть
Это тоже явное решение — «я намеренно обрываю цепочку», в отличие от случайной потери контекста при голом
🐍Вопросы с собесов -> ProstoPython
raise без from — потеря исходной ошибкиПлохой пример
def load_user(user_id):
try:
data = cache[user_id]
except KeyError:
raise UserNotFoundError(f"Юзер {user_id} не найден")
Выглядит аккуратно: поймали низкоуровневый
KeyError, бросили осмысленный доменный UserNotFoundError. Но так ты прячешь улики — и однажды это выйдет боком.Что не так
Когда исключение возникает внутри блока
except, Python по умолчанию цепляет его к исходному. Но в трейсбеке это выглядит запутанно:
KeyError: 42
During handling of the above exception, another exception occurred:
UserNotFoundError: Юзер 42 не найден
Формулировка «During handling of the above exception» намекает, что новая ошибка выскочила случайно, во время обработки — как будто в твоём обработчике баг. А ты-то преобразовал ошибку намеренно. Смысл связи потерян.
Хуже, когда исходную ошибку глушат совсем:
except KeyError:
raise UserNotFoundError(...) # а если тут не KeyError, а TypeError?
Если бы внутри
try была другая, неожиданная ошибка — понять её первопричину по логам станет тяжело.Как надо
Явно свяжи новое исключение с исходным через
raise ... from:
def load_user(user_id):
try:
data = cache[user_id]
except KeyError as e:
raise UserNotFoundError(f"Юзер {user_id} не найден") from e
Теперь трейсбек говорит правду о причинно-следственной связи:
KeyError: 42
The above exception was the direct cause of the following exception:
UserNotFoundError: Юзер 42 не найден
«The direct cause» — ты явно сказал: «этот
UserNotFoundError вызван вот этим `KeyError`». Отладка сохраняет всю цепочку.Когда исходную ошибку стоит подавить
Иногда низкоуровневая причина — шум, и её нужно осознанно скрыть (например, деталь реализации, которую не должен видеть вызывающий). Для этого есть
from None:
except KeyError:
raise UserNotFoundError(f"Юзер {user_id} не найден") from None
Это тоже явное решение — «я намеренно обрываю цепочку», в отличие от случайной потери контекста при голом
raise.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆4
🧠 Interview Thinking: Two Sum
Классика, с которой начинается почти любой собес. И именно на ней видно, отличает ли кандидат «перебор» от «правильной структуры данных».
Задача
Дан массив чисел и
Пример:
Как думает junior
«Переберу все пары, проверю каждую сумму.»
Работает. Но два вложенных цикла — O(n²). На собесе сразу: «А быстрее?»
Как думает сильный кандидат
Инсайт-переворот: я ищу не «две подходящие пары», а для каждого числа — одно конкретное дополнение.
Если сейчас у меня
Иду один раз, по пути запоминаю каждое число и его индекс. Для нового
O(n) время, O(n) память. Один проход.
🐍Вопросы с собесов -> ProstoPython
Классика, с которой начинается почти любой собес. И именно на ней видно, отличает ли кандидат «перебор» от «правильной структуры данных».
Задача
Дан массив чисел и
target. Найди индексы двух элементов, дающих в сумме target.Пример:
nums = [2, 7, 11, 15], target = 9 → [0, 1] (2 + 7 = 9).Как думает junior
«Переберу все пары, проверю каждую сумму.»
def two_sum(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
Работает. Но два вложенных цикла — O(n²). На собесе сразу: «А быстрее?»
Как думает сильный кандидат
Инсайт-переворот: я ищу не «две подходящие пары», а для каждого числа — одно конкретное дополнение.
Если сейчас у меня
x, то мне нужен ровно target − x. Вопрос сужается до: «встречал ли я уже target − x?» А «встречал ли уже» — это O(1) через словарь.Иду один раз, по пути запоминаю каждое число и его индекс. Для нового
x сначала проверяю, не лежит ли уже его дополнение:def two_sum(nums, target):
seen = {} # число → его индекс
for i, x in enumerate(nums):
need = target - x
if need in seen:
return [seen[need], i]
seen[x] = i
O(n) время, O(n) память. Один проход.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
💾 Memory Footprint
Ищем пару с нужной суммой.
Какая сложность по ПАМЯТИ?
A) O(1)
B) O(n)
C) O(n²)
D) O(log n)
Правильный ответ:C
Мало кто замечает: срез — это не «взгляд на кусок массива», а новый список.
Разбор
А делаем мы этот срез на каждой итерации внешнего цикла:
Да, старые копии собирает garbage collector, но пиковая работа с памятью и лишние аллокации никуда не деваются — на каждом шаге мы создаём и выбрасываем целый список. Это бьёт и по памяти, и по скорости (само копирование — тоже O(n) на итерацию).
Как починить — O(1) лишней памяти
Нам не нужна копия хвоста. Нужен лишь индекс, с которого начинать внутренний цикл:
Никаких копий — ходим по оригиналу по индексам.
🐍Вопросы с собесов -> ProstoPython
def has_pair_summing(nums, target):
for i in range(len(nums)):
rest = nums[i + 1:] # «остаток» массива
for x in rest:
if nums[i] + x == target:
return True
return False
Ищем пару с нужной суммой.
Какая сложность по ПАМЯТИ?
A) O(1)
B) O(n)
C) O(n²)
D) O(log n)
Правильный ответ:
Мало кто замечает: срез — это не «взгляд на кусок массива», а новый список.
Разбор
nums[i+1:] выглядит как «просто хвост массива». Но срез списка в Python всегда создаёт копию — выделяет новый список и копирует туда элементы.nums[i+1:] → новый список из (n - i - 1) элементов
(не ссылка на кусок оригинала — КОПИЯ)
А делаем мы этот срез на каждой итерации внешнего цикла:
i=0: копия длины n-1
i=1: копия длины n-2
i=2: копия длины n-3
...
суммарно скопировано: (n-1)+(n-2)+...+1 = O(n²) элементов
Да, старые копии собирает garbage collector, но пиковая работа с памятью и лишние аллокации никуда не деваются — на каждом шаге мы создаём и выбрасываем целый список. Это бьёт и по памяти, и по скорости (само копирование — тоже O(n) на итерацию).
Как починить — O(1) лишней памяти
Нам не нужна копия хвоста. Нужен лишь индекс, с которого начинать внутренний цикл:
def has_pair_summing(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)): # индекс вместо среза
if nums[i] + nums[j] == target:
return True
return False
nums[i+1:]: копия хвоста каждый раз → O(n²) памяти
range(i+1, len): просто числа-индексы → O(1) памяти
Никаких копий — ходим по оригиналу по индексам.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
⚖️ This vs That: sorted() vs list.sort()
Обе сортируют список. Разница выглядит косметической — а стоила многим часов отладки.
Что делает
Сортирует список на месте, меняя оригинал. И возвращает
Что делает
Не трогает оригинал. Создаёт и возвращает новый отсортированный список.
Главное отличие в одной фразе
🐍Вопросы с собесов -> ProstoPython
Обе сортируют список. Разница выглядит косметической — а стоила многим часов отладки.
Что делает
list.sort()Сортирует список на месте, меняя оригинал. И возвращает
None.nums = [3, 1, 2]
nums.sort()
print(nums) # [1, 2, 3] — сам список изменился
Что делает
sorted()Не трогает оригинал. Создаёт и возвращает новый отсортированный список.
nums = [3, 1, 2]
result = sorted(nums)
print(result) # [1, 2, 3]
print(nums) # [3, 1, 2] — оригинал цел
Главное отличие в одной фразе
.sort()меняет список и возвращает
None.
sorted()не трогает оригинал и возвращает новый список.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆4
❌ Rookie Mistakes: мутабельный атрибут на уровне класса
Две разные корзины. Кладём в первую — товар появляется во второй.
Почему это ошибка
Тут кроется вторая тонкость. Почему
Именно поэтому баг с неизменяемыми атрибутами не возникает:
🐍Вопросы с собесов -> ProstoPython
class Cart:
items = [] # список прямо в теле класса
def add(self, item):
self.items.append(item)
a = Cart()
b = Cart()
a.add("яблоко")
print(b.items) # ['яблоко'] ← откуда?!
Две разные корзины. Кладём в первую — товар появляется во второй.
Почему это ошибка
items = [] в теле класса создаёт атрибут класса, а не экземпляра. Список создаётся один раз — при определении класса — и принадлежит самому классу, а не объектам.Cart.items ──► ['яблоко']
▲ ▲
│ │
a b ← оба смотрят на ОДИН список
a.items не находит атрибут в самом объекте a → поднимается к классу → находит там общий список. То же для b. append мутирует этот единственный список — и его видят все экземпляры.Тут кроется вторая тонкость. Почему
self.items.append(...) не создал атрибут у объекта? Потому что append — это мутация, а не присваивание. Присваивание бы создало:self.items = [] # создаёт СВОЙ атрибут у экземпляра
self.items.append(x) # мутирует ОБЩИЙ список класса
Именно поэтому баг с неизменяемыми атрибутами не возникает:
count = 0 в классе безопасен, ведь self.count += 1 — это присваивание, и оно создаст личный атрибут.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
⏱️ Big O Breakdown
Какие сложности у A и B?
A) A: O(n), B: O(n)
B) A: O(2n), B: O(n²)
C) A: O(n), B: O(n²)
D) A: O(n²), B: O(n²)
Правильный ответ: C — A: O(n), B: O(n²)
Вопрос кажется тривиальным. Но ловушек тут две, и на обеих спотыкаются.
Разбор
Ловушка 1: циклы ПОДРЯД складываются, а не умножаются.
Это O(n), а не O(n²). Ключевое слово — «подряд»: второй цикл начинается после того, как первый полностью закончился. Работа складывается.
А вложенные циклы умножаются: внутренний прогоняется целиком на каждой итерации внешнего.
Ловушка 2: «O(2n)» — так не пишут.
Вариант B в ответах — приманка. Константы в Big O отбрасываются:
Big O описывает скорость роста, а не точное число операций. Удвоишь
Поэтому даже сто циклов подряд — это всё ещё O(n):
Что действительно важно
Смотри не на количество циклов, а на их вложенность:
И если циклы идут подряд, но с разной сложностью — берётся самое дорогое:
🐍Вопросы с собесов -> ProstoPython
# Вариант A
def process_a(items):
for x in items:
handle(x)
for x in items:
report(x)
# Вариант B
def process_b(items):
for x in items:
for y in items:
compare(x, y)
n — длина items. handle, report, compare работают за O(1).Какие сложности у A и B?
A) A: O(n), B: O(n)
B) A: O(2n), B: O(n²)
C) A: O(n), B: O(n²)
D) A: O(n²), B: O(n²)
Правильный ответ: C — A: O(n), B: O(n²)
Вопрос кажется тривиальным. Но ловушек тут две, и на обеих спотыкаются.
Разбор
Ловушка 1: циклы ПОДРЯД складываются, а не умножаются.
цикл 1: n операций
цикл 2: n операций
итого: n + n = 2n
Это O(n), а не O(n²). Ключевое слово — «подряд»: второй цикл начинается после того, как первый полностью закончился. Работа складывается.
А вложенные циклы умножаются: внутренний прогоняется целиком на каждой итерации внешнего.
подряд: n + n = 2n → O(n)
вложенные: n × n = n² → O(n²)
Ловушка 2: «O(2n)» — так не пишут.
Вариант B в ответах — приманка. Константы в Big O отбрасываются:
O(2n) → O(n)
O(n/2) → O(n)
O(3n+5) → O(n)
Big O описывает скорость роста, а не точное число операций. Удвоишь
n — время удвоится, независимо от того, 2n там или 100n. Это принципиально иначе, чем n², где удвоение n даёт четырёхкратный рост.Поэтому даже сто циклов подряд — это всё ещё O(n):
for x in items: ... # ×100 раз
# → O(100n) = O(n)
Что действительно важно
Смотри не на количество циклов, а на их вложенность:
циклы рядом → складывай → берётся максимум
циклы внутри → умножай
И если циклы идут подряд, но с разной сложностью — берётся самое дорогое:
for x in items: # O(n)
...
sorted(items) # O(n log n) ← доминирует
# итого: O(n log n)
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
❤3
🔍 Under the Hood: property — как атрибут превращается в метод
Проблема, которую он решает
Есть класс, у него публичный атрибут:
Код по всему проекту читает
Наивный путь — превратить в метод:
Но тогда весь код в проекте ломается: везде надо переписать
Что делает
Позволяет подменить обращение к атрибуту вызовом метода — не меняя синтаксис снаружи.
Снаружи ничего не изменилось:
Обычное присваивание, а под ним — код с проверками.
Второе применение: вычисляемые атрибуты
Значение, которое зависит от других полей и не должно храниться отдельно:
Меняешь радиус — площадь автоматически «обновляется», потому что она не хранится, а вычисляется при каждом обращении. Никакого рассинхрона между
🐍Вопросы с собесов -> ProstoPython
Проблема, которую он решает
Есть класс, у него публичный атрибут:
class Circle:
def __init__(self, radius):
self.radius = radius
Код по всему проекту читает
c.radius. А теперь надо добавить валидацию: радиус не может быть отрицательным.Наивный путь — превратить в метод:
def get_radius(self): ...
def set_radius(self, value): ...
Но тогда весь код в проекте ломается: везде надо переписать
c.radius на c.get_radius(). Именно поэтому в Java/C++ геттеры пишут заранее, «на всякий случай».Что делает
propertyПозволяет подменить обращение к атрибуту вызовом метода — не меняя синтаксис снаружи.
class Circle:
def __init__(self, radius):
self.radius = radius # это уже пойдёт через сеттер!
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Радиус не может быть отрицательным")
self._radius = value
Снаружи ничего не изменилось:
c = Circle(5)
c.radius # 5 → незаметно вызвался геттер
c.radius = 10 # ок → незаметно вызвался сеттер
c.radius = -1 # ValueError !
c.radius → Python видит property → вызывает метод-геттер
c.radius = x → Python видит property → вызывает метод-сеттер
Обычное присваивание, а под ним — код с проверками.
Второе применение: вычисляемые атрибуты
Значение, которое зависит от других полей и не должно храниться отдельно:
class Circle:
@property
def area(self):
return 3.14159 * self._radius ** 2
c.area # считается на лету, скобок нет
Меняешь радиус — площадь автоматически «обновляется», потому что она не хранится, а вычисляется при каждом обращении. Никакого рассинхрона между
radius и area.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
📈 From O(n²) to O(n)
Задача
Дан массив чисел (могут быть отрицательные). Найди максимальную сумму непрерывного подмассива.
Пример:
Наивное решение
«Переберу все подмассивы, посчитаю сумму каждого.»
Корректно, O(n²). Каждый старт
Проблема
Мы заново рассматриваем все возможные начала. Но интуиция подсказывает: если накопленная сумма ушла в минус, тащить её дальше бессмысленно — она только испортит любое продолжение.
Оптимизированное решение (алгоритм Кадане)
Ключевой вопрос на каждом элементе:
Ответ прост: если накопленная сумма отрицательна, она мешает. Отбрасываем её и стартуем заново.
O(n) время, O(1) память. Один проход, две переменные.
Объяснение
Трассировка
Смотри на
В этом вся суть: отрицательный префикс никогда не помогает.
🐍Вопросы с собесов -> ProstoPython
Задача
Дан массив чисел (могут быть отрицательные). Найди максимальную сумму непрерывного подмассива.
Пример:
[-2, 1, -3, 4, -1, 2, 1, -5, 4] → 6 (это [4, -1, 2, 1]).Наивное решение
«Переберу все подмассивы, посчитаю сумму каждого.»
def max_subarray(nums):
best = float("-inf")
for i in range(len(nums)):
total = 0
for j in range(i, len(nums)):
total += nums[j]
best = max(best, total)
return best
Корректно, O(n²). Каждый старт
i — новый проход вправо.Проблема
Мы заново рассматриваем все возможные начала. Но интуиция подсказывает: если накопленная сумма ушла в минус, тащить её дальше бессмысленно — она только испортит любое продолжение.
Оптимизированное решение (алгоритм Кадане)
Ключевой вопрос на каждом элементе:
«Продолжить предыдущий подмассив — или начать новый прямо с этого элемента?»
Ответ прост: если накопленная сумма отрицательна, она мешает. Отбрасываем её и стартуем заново.
def max_subarray(nums):
best = current = nums[0]
for x in nums[1:]:
current = max(x, current + x) # начать заново или продолжить?
best = max(best, current)
return best
O(n) время, O(1) память. Один проход, две переменные.
Объяснение
current — лучшая сумма подмассива, заканчивающегося на текущем элементе. best — лучший ответ среди всех, что видели.Трассировка
[-2, 1, -3, 4, -1, 2, 1, -5, 4]:x: -2 1 -3 4 -1 2 1 -5 4
current: -2 1 -2 4 3 5 6 1 5
best: -2 1 1 4 4 5 6 6 6
↑ ↑ ↑
-2+1=-1 < 1 -2+4=2 < 4 максимум!
начинаем заново начинаем заново
Смотри на
x = 1 (второй элемент): current + x дало бы −1, а сам x — это 1. Тянуть отрицательный хвост невыгодно → отбрасываем прошлое, начинаем новый подмассив.В этом вся суть: отрицательный префикс никогда не помогает.
🐍Вопросы с собесов -> ProstoPython
🔥4
🧠 Что выведет код
Что выведет каждая строка?
Ответы:
Ни одного
Разбор
Четвёртая строка — самая показательная.
Это короткое замыкание (short-circuit): вычисление обрывается, как только результат ясен.
🐍Вопросы с собесов -> ProstoPython
print("" or "default")
print(0 or [])
print("hello" and 42)
print(0 and 1/0)Что выведет каждая строка?
Ответы:
"default"
[]
42
0
Ни одного
True/False. Многие ждут булев результат — а получают сами операнды.Разбор
and и or в Python не возвращают bool. Они возвращают один из операндов — тот, на котором остановились.or — возвращает первый истинный операнд. Если все ложные — последний:"" or "default" → "" ложь → идём дальше → "default"
0 or [] → 0 ложь → идём дальше → [] (последний, хоть и ложный)
and — возвращает первый ложный операнд. Если все истинные — последний:"hello" and 42 → "hello" истина → идём дальше → 42
0 and 1/0 → 0 ложь → СРАЗУ возвращаем 0
Четвёртая строка — самая показательная.
1/0 — это ZeroDivisionError. Но ошибки нет! Потому что Python до неё не дошёл:0 and 1/0
↑
ложь → результат уже известен → правую часть НЕ вычисляем
Это короткое замыкание (short-circuit): вычисление обрывается, как только результат ясен.
🐍Вопросы с собесов -> ProstoPython
👍4
💾 Memory Footprint: рекурсия и стек вызовов
Суммируем список рекурсивно. Ни одного нового списка, ни словаря, ни множества — только числа.
Какая сложность по ПАМЯТИ?
A) O(1) — мы же не создаём структур данных
B) O(log n)
C) O(n)
D) O(n²)
Правильный ответ:C
Память тратится там, где её не видно в коде.
Разбор
Каждый вызов функции кладёт на стек вызовов новый фрейм: локальные переменные, аргументы, адрес возврата. Фрейм живёт, пока функция не вернула результат.
А тут ни один вызов не может вернуться, пока не вернулся вложенный:
В момент самого глубокого вызова на стеке лежат все
И это не абстракция — Python упадёт
У стека есть жёсткий лимит (по умолчанию ~1000 вызовов):
На списке в 10 000 элементов код падает. Не из-за времени — из-за глубины стека.
Как починить
Разверни рекурсию в цикл — тогда фрейм один, а память O(1):
Важный нюанс: не всякая рекурсия — это O(n)
Смотри на глубину, а не на число вызовов:
Именно поэтому у рекурсивного merge sort память O(n) (плюс буферы), а у бинарного поиска — O(log n): дерево вызовов глубокое или мелкое.
🐍Вопросы с собесов -> ProstoPython
def sum_list(nums, i=0):
if i == len(nums):
return 0
return nums[i] + sum_list(nums, i + 1)
Суммируем список рекурсивно. Ни одного нового списка, ни словаря, ни множества — только числа.
n — длина списка.Какая сложность по ПАМЯТИ?
A) O(1) — мы же не создаём структур данных
B) O(log n)
C) O(n)
D) O(n²)
Правильный ответ:
Память тратится там, где её не видно в коде.
Разбор
Каждый вызов функции кладёт на стек вызовов новый фрейм: локальные переменные, аргументы, адрес возврата. Фрейм живёт, пока функция не вернула результат.
А тут ни один вызов не может вернуться, пока не вернулся вложенный:
sum_list(nums, 0) ждёт →
sum_list(nums, 1) ждёт →
sum_list(nums, 2) ждёт →
...
sum_list(nums, n) → 0
В момент самого глубокого вызова на стеке лежат все
n фреймов одновременно. Это O(n) памяти — просто не в куче, а на стеке.итеративный цикл: один фрейм, переменные перезаписываются → O(1)
рекурсия глубины n: n фреймов ждут друг друга → O(n)
И это не абстракция — Python упадёт
У стека есть жёсткий лимит (по умолчанию ~1000 вызовов):
sum_list(list(range(10_000)))
# RecursionError: maximum recursion depth exceeded
На списке в 10 000 элементов код падает. Не из-за времени — из-за глубины стека.
Как починить
Разверни рекурсию в цикл — тогда фрейм один, а память O(1):
def sum_list(nums):
total = 0
for x in nums:
total += x
return total
Важный нюанс: не всякая рекурсия — это O(n)
Смотри на глубину, а не на число вызовов:
линейная рекурсия (сумма списка): глубина n → O(n) памяти
бинарный поиск рекурсивно: глубина log n → O(log n)
обход сбалансированного дерева: глубина log n → O(log n)
обход вырожденного дерева-цепочки: глубина n → O(n)
Именно поэтому у рекурсивного merge sort память O(n) (плюс буферы), а у бинарного поиска — O(log n): дерево вызовов глубокое или мелкое.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🧠 Interview Thinking
Задача
Проверь, является ли строка палиндромом — читается одинаково слева направо и справа налево. Учитываем только буквы и цифры, регистр игнорируем.
Пример:
Как думает junior
«Почищу строку, разверну и сравню.»
Работает, и это O(n) по времени. Отличный старт — на многих собесах этого хватит.
Но интервьюер добавит: «А без дополнительной памяти?»
И тут junior обычно теряется — ведь по времени-то уже линия, куда быстрее?
Как думает сильный кандидат
Сначала слышит правильный вопрос. Просят не ускорить время, а убрать память: сейчас мы создаём копию строки (
Инсайт: не нужно ничего копировать. Палиндром — это про симметрию. Иду с двух концов навстречу и сравниваю пары символов на месте.
O(n) время, O(1) память. Ни одной копии.
🐍Вопросы с собесов -> ProstoPython
Задача
Проверь, является ли строка палиндромом — читается одинаково слева направо и справа налево. Учитываем только буквы и цифры, регистр игнорируем.
Пример:
"A man, a plan, a canal: Panama" → True.Как думает junior
«Почищу строку, разверну и сравню.»
def is_palindrome(s):
cleaned = [ch.lower() for ch in s if ch.isalnum()]
return cleaned == cleaned[::-1]
Работает, и это O(n) по времени. Отличный старт — на многих собесах этого хватит.
Но интервьюер добавит: «А без дополнительной памяти?»
И тут junior обычно теряется — ведь по времени-то уже линия, куда быстрее?
Как думает сильный кандидат
Сначала слышит правильный вопрос. Просят не ускорить время, а убрать память: сейчас мы создаём копию строки (
cleaned) плюс её разворот — это O(n) лишней памяти.Инсайт: не нужно ничего копировать. Палиндром — это про симметрию. Иду с двух концов навстречу и сравниваю пары символов на месте.
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1 # пропускаем мусор слева
while left < right and not s[right].isalnum():
right -= 1 # пропускаем мусор справа
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
O(n) время, O(1) память. Ни одной копии.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
❤🔥4
⏱️ Big O Breakdown
Считаем, сколько пользователей в бан-листе.
Какая общая сложность по времени?
A) O(n)
B) O(n + m)
C) O(n · m)
D) O(n log m)
Правильный ответ:C
Цикл один, и кажется, что O(n). Но
Разбор
Всё зависит от того, что такое
И эту проверку мы делаем на каждом из
Один видимый цикл
Магия одной строки
Меняем
Для
Важный нюанс — когда set не окупается
Построение множества стоит
Множество окупается, когда по одной и той же коллекции проверяешь принадлежность многократно.
🐍Вопросы с собесов -> ProstoPython
def count_common(users, banned):
count = 0
for user in users:
if user in banned: # banned — это список
count += 1
return count
Считаем, сколько пользователей в бан-листе.
n — число users, m — размер banned.Какая общая сложность по времени?
A) O(n)
B) O(n + m)
C) O(n · m)
D) O(n log m)
Правильный ответ:
Цикл один, и кажется, что O(n). Но
in по списку прячет второй цикл внутри.Разбор
Всё зависит от того, что такое
banned. Если это список — user in banned делает линейный поиск: перебирает элементы по одному, пока не найдёт (или не дойдёт до конца).user in banned (список) → до m сравнений
И эту проверку мы делаем на каждом из
n пользователей:n пользователей × до m сравнений каждый = O(n · m)
Один видимый цикл
for, но in по списку — это скрытый второй цикл.Магия одной строки
Меняем
banned со списка на множество — и сложность падает:def count_common(users, banned):
banned = set(banned) # ← превращаем в множество
count = 0
for user in users:
if user in banned: # теперь O(1)
count += 1
return count
in по множеству — это O(1) (хеш-таблица: вычисляем хеш, прыгаем в ячейку, не перебирая). Общая сложность становится:построение set: O(m) (один раз)
n проверок × O(1): O(n)
итого: O(n + m)
banned — список: n × O(m) = O(n·m)
banned — множество: O(m) + n × O(1) = O(n + m)
Для
n = m = 10 000 разница между 10⁸ и 2·10⁴ операций — это секунды против мгновения.Важный нюанс — когда set не окупается
Построение множества стоит
O(m). Если проверок мало — скажем, всего одна — конвертировать нет смысла: O(m) на построение съест всю экономию.одна проверка: list → O(m), set → O(m) на построение + O(1) = O(m)
(одинаково, конвертация не нужна)
много проверок (n): list → O(n·m), set → O(m + n)
(set выигрывает тем сильнее, чем больше n)
Множество окупается, когда по одной и той же коллекции проверяешь принадлежность многократно.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🔍 Under the Hood: MRO и почему super() — это не «родитель»
Почти все думают:
Загадка
Интуиция говорит
Что происходит
У каждого класса есть MRO — линейный список, в каком порядке искать методы:
Ключевой факт:
Зачем так — проблема ромба
Порядок строит алгоритм C3-линеаризации: потомки идут раньше предков, порядок базовых классов из объявления сохраняется. Если согласованного порядка нет — Python падает с
🐍Вопросы с собесов -> ProstoPython
Почти все думают:
super() вызывает родительский класс. При простом наследовании — да. При множественном — нет.Загадка
class A:
def greet(self):
print("A")
class B(A):
def greet(self):
print("B")
super().greet()
class C(A):
def greet(self):
print("C")
super().greet()
class D(B, C):
def greet(self):
print("D")
super().greet()
D().greet()
Интуиция говорит
D → B → A. А на деле:D
B
C ← откуда C?! B же не наследует C
A
Что происходит
У каждого класса есть MRO — линейный список, в каком порядке искать методы:
D.__mro__
# (D, B, C, A, object)
Ключевой факт:
super()вызывает не родителя, а следующий класс в MRO относительно текущего.
MRO: D → B → C → A → object
D.greet → super → B
B.greet → super → C ← вот почему C
C.greet → super → A
A.greet → стоп
super() в B смотрит не на «своего родителя A», а на позицию B в MRO этого объекта — а там дальше C.Зачем так — проблема ромба
A
/ \
B C
\ /
D
D наследует и B, и C, оба — от A. Если бы каждый звал своего родителя, A.greet вызвался бы дважды. MRO выстраивает всех в линию так, что каждый предок посещается ровно один раз — A в самом конце.Порядок строит алгоритм C3-линеаризации: потомки идут раньше предков, порядок базовых классов из объявления сохраняется. Если согласованного порядка нет — Python падает с
TypeError при определении класса.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
🧠 Что выведет код
Варианты:
A)
B)
C)
D)
Правильный ответ:B
Одна функция «меняет» список, другая — нет. Почему?
Разбор
Python передаёт аргументы не по значению и не по ссылке, а по модели «передача ссылки на объект» (pass by object reference). Параметр функции — это новое имя, указывающее на тот же объект.
Дальше всё решает: ты переприсваиваешь имя или мутируешь объект.
Присваивание меняет имя, а не объект. Наружу это не видно.
Суть в одной строке
Функция может изменить переданный объект только через мутацию. Присваивание нового значения параметру наружу не протекает — оно лишь перевешивает локальное имя.
🐍Вопросы с собесов -> ProstoPython
x = [1, 2, 3]
def modify(lst):
lst = lst + [4]
def mutate(lst):
lst.append(4)
modify(x)
print(x)
mutate(x)
print(x)
Варианты:
A)
[1, 2, 3, 4] и [1, 2, 3, 4] B)
[1, 2, 3] и [1, 2, 3, 4] C)
[1, 2, 3, 4] и [1, 2, 3, 4, 4] D)
[1, 2, 3] и [1, 2, 3]Правильный ответ:
Одна функция «меняет» список, другая — нет. Почему?
Разбор
Python передаёт аргументы не по значению и не по ссылке, а по модели «передача ссылки на объект» (pass by object reference). Параметр функции — это новое имя, указывающее на тот же объект.
x ──► [1, 2, 3]
▲
lst ────┘ ← lst и x смотрят на ОДИН объект
Дальше всё решает: ты переприсваиваешь имя или мутируешь объект.
modify: lst = lst + [4]lst + [4] создаёт новый список. Присваивание lst = ... перевешивает локальное имя lst на этот новый объект. Связь с x рвётся. Оригинал не тронут.lst = lst + [4]
x ──► [1, 2, 3] ← оригинал цел
lst ──► [1, 2, 3, 4] ← новый объект, только внутри функции
Присваивание меняет имя, а не объект. Наружу это не видно.
mutate: lst.append(4)append мутирует сам объект — тот, на который смотрит и lst, и x. Имя не трогаем — меняем то, на что оно указывает.lst.append(4)
x ──► [1, 2, 3, 4] ← тот же объект, изменён
lst ──┘
Суть в одной строке
lst = ... → переприсваивание ИМЕНИ → оригинал не виден снаружи
lst.append(...) → мутация ОБЪЕКТА → видно везде, где есть ссылка
Функция может изменить переданный объект только через мутацию. Присваивание нового значения параметру наружу не протекает — оно лишь перевешивает локальное имя.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥3
🧰 Code Cleanup: ручная проверка ключа → dict.get и setdefault
Плохой код
Работает. Но каждое обращение к словарю — это четыре строки на «а вдруг ключа нет». Проверка
Чистый вариант
Объяснение
Важная тонкость про
🐍Вопросы с собесов -> ProstoPython
Плохой код
# достать значение с дефолтом
if "timeout" in config:
timeout = config["timeout"]
else:
timeout = 30
# накопить в словарь списков
if key in groups:
groups[key].append(value)
else:
groups[key] = [value]
Работает. Но каждое обращение к словарю — это четыре строки на «а вдруг ключа нет». Проверка
in, потом снова доступ по тому же ключу — дублирование.Чистый вариант
# дефолт в одну строку
timeout = config.get("timeout", 30)
# накопление без ветвлений
groups.setdefault(key, []).append(value)
Объяснение
dict.get(key, default) возвращает значение по ключу, а если ключа нет — default. Никакого if in, никакого второго обращения:if key in d: x = d[key] else: x = default
↓
x = d.get(key, default)
dict.setdefault(key, default) хитрее: если ключа нет — вставляет его со значением default и возвращает это значение; если есть — просто возвращает существующее. Поэтому setdefault(key, []).append(value) работает в обоих случаях:ключа нет: вставили [], вернули его, добавили value → [value]
ключ есть: вернули текущий список, добавили value → [..., value]
Важная тонкость про
get — не путай «ключа нет» и «значение ложное»:config = {"retries": 0}
config.get("retries", 3) # 0 — ключ есть, вернётся его значение
config.get("retries") or 3 # 3 — ой! 0 ложен, or его отбросил
get с дефолтом смотрит на наличие ключа, а or — на истинность значения. Для 0, "", [] это разные вещи. Нужен именно «ключ отсутствует» — бери get(key, default), а не get(key) or default.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🧠 Interview Thinking
Похоже на классику про акции, но правило одно меняет всё — и на этом ловят.
Задача
Дан массив
Пример:
Как думает junior
«Много сделок… надо найти лучшие моменты входа и выхода, перебрать комбинации покупок и продаж.»
И тут начинается: вложенные циклы, попытки в динамику, состояния «держу / не держу». Решение раздувается, появляются баги на границах. O(n²) или хуже, и легко ошибиться.
Задача кажется сложнее первой версии — ведь сделок много.
Как думает сильный кандидат
Сначала — разбор задачи на бумаге, до кода. Что значит «неограниченно много сделок»?
Ключевое наблюдение: раз ограничений на число сделок нет, я могу «поймать» каждый участок роста. А любой рост с
Значит, задача сводится к простому: сложить все положительные разницы между соседними днями.
O(n) время, O(1) память. Одна строка по сути.
Смысл: «покупай перед каждым днём роста, продавай после». Все восходящие отрезки собираются автоматически.
Трассировка
Отрицательные разницы (падения) просто игнорируем — в эти дни мы акцию не держим.
🐍Вопросы с собесов -> ProstoPython
Похоже на классику про акции, но правило одно меняет всё — и на этом ловят.
Задача
Дан массив
prices — цена акции по дням. Теперь можно совершать сколько угодно сделок: покупать и продавать много раз (но держать не больше одной акции одновременно). Максимизируй суммарную прибыль.Пример:
[7, 1, 5, 3, 6, 4] → 7.Как думает junior
«Много сделок… надо найти лучшие моменты входа и выхода, перебрать комбинации покупок и продаж.»
И тут начинается: вложенные циклы, попытки в динамику, состояния «держу / не держу». Решение раздувается, появляются баги на границах. O(n²) или хуже, и легко ошибиться.
Задача кажется сложнее первой версии — ведь сделок много.
Как думает сильный кандидат
Сначала — разбор задачи на бумаге, до кода. Что значит «неограниченно много сделок»?
Ключевое наблюдение: раз ограничений на число сделок нет, я могу «поймать» каждый участок роста. А любой рост с
A до B через промежуточные точки можно разложить на сумму дневных приростов:цена 1 → 5: прибыль 4
это то же, что: (2−1) + (3−2) + (4−3) + (5−4) = 4
Значит, задача сводится к простому: сложить все положительные разницы между соседними днями.
def max_profit(prices):
return sum(
prices[i] - prices[i - 1]
for i in range(1, len(prices))
if prices[i] > prices[i - 1]
)
O(n) время, O(1) память. Одна строка по сути.
Смысл: «покупай перед каждым днём роста, продавай после». Все восходящие отрезки собираются автоматически.
Трассировка
[7, 1, 5, 3, 6, 4]:день: 7 1 5 3 6 4
разница: -6 +4 -2 +3 -1
берём: — 4 — 3 —
сумма положительных: 4 + 3 = 7
Отрицательные разницы (падения) просто игнорируем — в эти дни мы акцию не держим.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆3
❌ Rookie Mistakes: except ловит не то, что кажется
Логика ясная: если в конфиге нет
Почему это ошибка
Если
Суть ошибки:
Исправленный вариант
Оборачивай в
Теперь
А для «достать значение с дефолтом» вообще не нужен
Общее правило узкого
Держи под
🐍Вопросы с собесов -> ProstoPython
try:
config = load_config()
value = config["timeout"]
result = process(value)
except KeyError:
print("В конфиге нет ключа timeout")
result = default_result()
Логика ясная: если в конфиге нет
timeout — берём дефолт. Но однажды process внутри себя тоже кинет KeyError — по совсем другой причине. И этот блок его проглотит, напечатав неверное сообщение.Почему это ошибка
except KeyError ловит любой KeyError, возникший где угодно в блоке try — не только тот, что ты имел в виду.try:
config = load_config()
value = config["timeout"] # ← ждём KeyError отсюда
result = process(value) # ← а он прилетит ОТСЮДА
except KeyError: # ловит оба, не различая
...
Если
process внутри обратится к несуществующему ключу словаря — вылетит KeyError, но не про timeout. А обработчик уверенно скажет «нет ключа timeout» и подсунет дефолт. Настоящий баг в process замаскирован, сообщение врёт, отладка превращается в ад.Суть ошибки:
try обнимает слишком много кода. Чем больше строк под ним, тем выше шанс, что тип исключения совпадёт случайно, не по той причине.Исправленный вариант
Оборачивай в
try ровно ту строку, чьё исключение ты ждёшь:config = load_config()
try:
value = config["timeout"] # только эта строка
except KeyError:
print("В конфиге нет ключа timeout")
value = 30
result = process(value) # его ошибки НЕ ловятся здесь
Теперь
KeyError из process полетит наверх — как и должен, с честным трейсбеком.А для «достать значение с дефолтом» вообще не нужен
try:value = config.get("timeout", 30) # ключа нет → 30, без исключений
result = process(value)Общее правило узкого
tryширокий try: try { A; B; C } except E ← E от A, B или C — не различить
узкий try: try { B } except E ← точно знаем: это про BДержи под
try минимум строк — только те, где ожидаешь конкретную ошибку. Всё остальное выноси наружу.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
⚖️ This vs That:
«Кортеж — это неизменяемый список» — так отвечают почти все. Верно, но неполно: разница глубже, чем возможность менять.
Что делает
Изменяемая последовательность. Можно добавлять, удалять, менять элементы:
Что делает
Неизменяемая последовательность. После создания — только чтение:
Главное отличие в одной фразе
Следствие 1: хешируемость
Кортеж можно использовать как ключ словаря и элемент множества. Список — нельзя:
Причина: хеш должен быть стабильным. Изменяемый объект менял бы хеш после мутации — и ключ бы «терялся» в хеш-таблице. Поэтому мутабельные типы нехешируемы по определению.
Это главная практическая причина брать кортеж — координаты, составные ключи, пары в
Следствие 2: смысл, а не только форма
Есть негласная семантическая конвенция:
Поэтому функции возвращают кортежи:
Следствие 3: память и скорость
Кортеж знает свой размер навсегда — ему не нужен запас под рост:
Отсюда кортеж компактнее и чуть быстрее в создании/обходе. На миллионах мелких записей разница заметна.
🐍Вопросы с собесов -> ProstoPython
list vs tuple«Кортеж — это неизменяемый список» — так отвечают почти все. Верно, но неполно: разница глубже, чем возможность менять.
Что делает
listИзменяемая последовательность. Можно добавлять, удалять, менять элементы:
items = [1, 2, 3]
items.append(4)
items[0] = 99 # ок
Что делает
tupleНеизменяемая последовательность. После создания — только чтение:
point = (1, 2, 3)
point[0] = 99 # TypeError: 'tuple' object does not support item assignment
Главное отличие в одной фразе
list— изменяемый,
tuple— нет. Но следствия из этого важнее самого факта.
Следствие 1: хешируемость
Кортеж можно использовать как ключ словаря и элемент множества. Список — нельзя:
cache = {(1, 2): "результат"} # ок
cache = {[1, 2]: "результат"} # TypeError: unhashable type: 'list'Причина: хеш должен быть стабильным. Изменяемый объект менял бы хеш после мутации — и ключ бы «терялся» в хеш-таблице. Поэтому мутабельные типы нехешируемы по определению.
Это главная практическая причина брать кортеж — координаты, составные ключи, пары в
set.Следствие 2: смысл, а не только форма
Есть негласная семантическая конвенция:
list → однородная коллекция ПЕРЕМЕННОЙ длины
[user1, user2, user3] — «много одинакового»
tuple → фиксированная структура, где важна ПОЗИЦИЯ
(x, y) — координата; (name, age, city) — запись
Поэтому функции возвращают кортежи:
return name, age — это одна структура из разных по смыслу полей, а не «список из двух штук».Следствие 3: память и скорость
Кортеж знает свой размер навсегда — ему не нужен запас под рост:
list: выделяет память С ЗАПАСОМ под будущие append
tuple: ровно под n элементов
Отсюда кортеж компактнее и чуть быстрее в создании/обходе. На миллионах мелких записей разница заметна.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
📈 From O(n log n) to O(n)
Задача
Дан массив из
Наивное решение
«Отсортирую и возьму хвост.»
Коротко и корректно. Но сложность — O(n log n): мы упорядочили весь массив, хотя нужны всего
Проблема
Мы делаем гораздо больше работы, чем требует задача. Порядок остальных
Оптимизированное решение
Куча (heap) размера
Сложность: O(n log k). Когда
По памяти — O(k) вместо O(n) на копию для сортировки. Важно, если данные идут потоком и не влезают в память целиком.
Объяснение
Min-heap — структура, где минимум всегда на вершине и доступен за O(1). Это ровно то, что нужно: «самый слабый в топе» — кандидат на вылет.
Трассировка
Куча в любой момент содержит
В стандартной библиотеке это уже завёрнуто:
🐍Вопросы с собесов -> ProstoPython
Задача
Дан массив из
n чисел. Верни k наибольших. Обычно k сильно меньше n (топ-10 из миллиона).Наивное решение
«Отсортирую и возьму хвост.»
def top_k(nums, k):
return sorted(nums, reverse=True)[:k]
Коротко и корректно. Но сложность — O(n log n): мы упорядочили весь массив, хотя нужны всего
k элементов.Проблема
Мы делаем гораздо больше работы, чем требует задача. Порядок остальных
n − k элементов нас не интересует вообще — а мы за него заплатили.Оптимизированное решение
Куча (heap) размера
k. Держим min-heap ровно из k элементов — текущего топа. Новый элемент сравниваем с минимумом кучи: если он больше — минимум вылетает, новый заходит.import heapq
def top_k(nums, k):
heap = nums[:k]
heapq.heapify(heap) # O(k)
for x in nums[k:]:
if x > heap[0]: # heap[0] — минимум кучи, O(1)
heapq.heapreplace(heap, x) # выкинуть min, вставить x: O(log k)
return heap
Сложность: O(n log k). Когда
k мало и почти константа — это фактически O(n).По памяти — O(k) вместо O(n) на копию для сортировки. Важно, если данные идут потоком и не влезают в память целиком.
Объяснение
Min-heap — структура, где минимум всегда на вершине и доступен за O(1). Это ровно то, что нужно: «самый слабый в топе» — кандидат на вылет.
Трассировка
nums = [3, 1, 5, 12, 2, 11], k = 3:куча из первых 3: [1, 3, 5] min = 1
x=12: 12 > 1 → выкидываем 1 → [3, 12, 5] min = 3
x=2: 2 < 3 → пропускаем → [3, 12, 5]
x=11: 11 > 3 → выкидываем 3 → [5, 12, 11] min = 5
топ-3: [5, 12, 11]
Куча в любой момент содержит
k лучших из просмотренного. Элементы меньше текущего минимума отбрасываются мгновенно, за одно сравнение.В стандартной библиотеке это уже завёрнуто:
heapq.nlargest(k, nums) # то же самое, готовое
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🎭 Red Flag: изменение списка, по которому идёт цикл
Плохой пример
Код выглядит очевидно правильным. Но неактивный
Что не так
Итератор списка ходит по индексам, а не по элементам «логически». Он помнит: «я на позиции
Удалил элемент → сосед занял его место → итератор перешагнул через соседа. Каждое удаление «прячет» следующий элемент от проверки.
Со словарём и множеством такое хотя бы честно падает с
Как надо
Не мутируй то, что перебираешь. Построй новый список из того, что нужно оставить:
Если по контракту надо изменить тот же объект (на него есть ссылки снаружи) — перезапиши его содержимое срезом:
Ещё вариант, если очень нужен цикл, — итерируйся по копии, а меняй оригинал:
Когда исходный подход не сломается
Практически никогда для удаления. Единственный безопасный способ менять список в цикле — идти с конца по индексам, тогда сдвиг не задевает ещё не пройденные позиции:
Но это менее читаемо, чем comprehension. Comprehension почти всегда лучше.
🐍Вопросы с собесов -> ProstoPython
Плохой пример
def remove_inactive(users):
for user in users:
if not user.is_active:
users.remove(user) # удаляем прямо во время итерации
users = [alice, bob, carol, dave] # bob и carol неактивны
remove_inactive(users)
print([u.name for u in users]) # ['alice', 'carol', 'dave'] ← carol выжила!
Код выглядит очевидно правильным. Но неактивный
carol остался в списке. И — что коварно — никакой ошибки не было. Просто тихо неверный результат.Что не так
Итератор списка ходит по индексам, а не по элементам «логически». Он помнит: «я на позиции
i». Когда ты удаляешь элемент — все, что правее, сдвигаются влево, и следующий элемент проскакивает мимо.users: [alice, bob, carol, dave]
i=0 i=1 i=2 i=3
i=1: bob неактивен → remove(bob)
список сдвинулся:
[alice, carol, dave]
↑
а итератор идёт на i=2 → это dave, НЕ carol
carol пропущена!
Удалил элемент → сосед занял его место → итератор перешагнул через соседа. Каждое удаление «прячет» следующий элемент от проверки.
Со словарём и множеством такое хотя бы честно падает с
RuntimeError. А список молчит и возвращает мусор — это опаснее всего: баг проходит тесты на «удобных» данных и стреляет в проде.Как надо
Не мутируй то, что перебираешь. Построй новый список из того, что нужно оставить:
def remove_inactive(users):
return [user for user in users if user.is_active]
Если по контракту надо изменить тот же объект (на него есть ссылки снаружи) — перезапиши его содержимое срезом:
def remove_inactive(users):
users[:] = [user for user in users if user.is_active] # мутируем на месте, но безопасно
users[:] = ... заменяет содержимое существующего списка — ссылки снаружи увидят изменение, но итерации по живому списку во время удаления нет.Ещё вариант, если очень нужен цикл, — итерируйся по копии, а меняй оригинал:
for user in list(users): # копия ключей/элементов
if not user.is_active:
users.remove(user)
Когда исходный подход не сломается
Практически никогда для удаления. Единственный безопасный способ менять список в цикле — идти с конца по индексам, тогда сдвиг не задевает ещё не пройденные позиции:
for i in range(len(users) - 1, -1, -1):
if not users[i].is_active:
del users[i]
Но это менее читаемо, чем comprehension. Comprehension почти всегда лучше.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
❤3