🧰 Code Cleanup: «Убери дублирование условия»
❌ Плохой код
✅ Улучшенный код
💥 Что изменилось?
В исходном коде
Сначала разбираемся с исключением:
⚡ Правило
Если одно условие повторяется в нескольких ветках — попробуй вынести его раньше и сделать ранний
Меньше вложенности → меньше кода → проще читать
🐍Вопросы с собесов -> ProstoPython
❌ Плохой код
def get_discount(user):
if user.is_premium and user.age >= 18:
return 20
elif user.is_premium and user.age < 18:
return 10
else:
return 0
✅ Улучшенный код
def get_discount(user):
if not user.is_premium:
return 0
return 20 if user.age >= 18 else 10
💥 Что изменилось?
В исходном коде
user.is_premium проверяется дважды.Сначала разбираемся с исключением:
if not user.is_premium:
return 0
После этого можно спокойно работать только с премиум-пользователем.⚡ Правило
Если одно условие повторяется в нескольких ветках — попробуй вынести его раньше и сделать ранний
return.Меньше вложенности → меньше кода → проще читать
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
📈 From O(n²) to O(n)
📌 Задача
Найти, есть ли в двух списках хотя бы один общий элемент.
❌ Наивное решение
Сложность:
Каждый элемент
✅ Оптимизированное решение
Сложность:
💡 Главный инсайт
Мы не ускорили вложенный цикл.
Мы вообще от него избавились.
Вместо:
делаем:
🐍Вопросы с собесов -> ProstoPython
📌 Задача
Найти, есть ли в двух списках хотя бы один общий элемент.
a = [10, 20, 30, 40]
b = [5, 30, 70, 90]
❌ Наивное решение
def has_common(a, b):
for x in a:
for y in b:
if x == y:
return True
return False
Сложность:
O(n × m)Каждый элемент
a сравнивается со всеми элементами b.✅ Оптимизированное решение
def has_common(a, b):
values = set(b)
return any(x in values for x in a)
Сложность:
O(n + m)💡 Главный инсайт
Мы не ускорили вложенный цикл.
Мы вообще от него избавились.
Вместо:
«Сравни этот элемент со всеми»
делаем:
«Проверь, встречался ли он в
set».
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
🧠 Interview Thinking
📌 Задача
Есть список:
Найти второй по величине уникальный элемент.
Ответ:
👶 Как думает junior
Сразу сортирует:
Работает
Но сильный кандидат сначала спросит:
Если нельзя — это решение уже не подходит.
🧠 Как думает сильный кандидат
Проходит список один раз и хранит только два значения:
Сложность:
🐍Вопросы с собесов -> ProstoPython
📌 Задача
Есть список:
nums = [3, 1, 4, 1, 5, 9]
Найти второй по величине уникальный элемент.
Ответ:
5
👶 Как думает junior
Сразу сортирует:
def second_max(nums):
return sorted(set(nums))[-2]
Работает
Но сильный кандидат сначала спросит:
«Можно ли использовать дополнительную память?»
Если нельзя — это решение уже не подходит.
🧠 Как думает сильный кандидат
Проходит список один раз и хранит только два значения:
def second_max(nums):
first = second = float("-inf")
for num in nums:
if num > first:
second = first
first = num
elif first > num > second:
second = num
return second
Сложность:
O(n) по времени и O(1) по памяти🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
📈 From O(n²) to O(n): Подсчёт частоты элементов
📌 Задача
Найти элемент, который встречается чаще всего в списке.
❌ Наивное решение
Сложность:
✅ Оптимизированное решение
Сложность:
💡 Что изменилось?
Когда он вызывается внутри цикла, список проходится снова и снова.
🐍Вопросы с собесов -> ProstoPython
📌 Задача
Найти элемент, который встречается чаще всего в списке.
❌ Наивное решение
def most_frequent(nums):
max_count = 0
result = None
for num in nums:
count = nums.count(num)
if count > max_count:
max_count = count
result = num
return result
Сложность:
O(n²)✅ Оптимизированное решение
from collections import Counter
def most_frequent(nums):
return Counter(nums).most_common(1)[0][0]
Сложность:
O(n)💡 Что изменилось?
count() проходит по всему списку.Когда он вызывается внутри цикла, список проходится снова и снова.
Counter считает частоты за один проход, после чего нужный элемент находится мгновенно.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
🧠 Interview Thinking
📌 Задача
Проверить, содержит ли список дубликаты.
👶 Как думает junior
Работает. На этом объяснение заканчивается.
🧠 Как думает сильный кандидат
И сразу объясняет:
🎯 Что хочет интервьюер
Не просто увидеть
Он хочет понять, почему ты выбрал именно его, а не список, словарь или сортировку
🐍Вопросы с собесов -> ProstoPython
📌 Задача
Проверить, содержит ли список дубликаты.
👶 Как думает junior
def has_duplicates(nums):
return len(nums) != len(set(nums))
Работает. На этом объяснение заканчивается.
🧠 Как думает сильный кандидат
def has_duplicates(nums):
seen = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return False
И сразу объясняет:
«Я использую
set, потому что проверка наличия элемента выполняется в среднем за
O(1). Кроме того, функция завершится сразу после нахождения первого дубликата, не проходя весь список.»
🎯 Что хочет интервьюер
Не просто увидеть
set.Он хочет понять, почему ты выбрал именно его, а не список, словарь или сортировку
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
🧠 Что выведет код
Варианты ответа:
A)
B)
C)
D)
✅ Правильный ответ:C
Разбор
Первая часть,
Проблема — во второй части. Python пытается выполнить
Мутация уже случилась, откат назад никто не делает. Получаем на первый взгляд абсурдную ситуацию: код падает с ошибкой, но результат мутации остаётся.
Вывод
⚡️
🐍Вопросы с собесов -> ProstoPython
t = ([1, 2], [3, 4])
t[0] += [5]
Варианты ответа:
A)
t становится ([1, 2, 5], [3, 4]), ошибок нет B)
TypeError, t не меняется C)
TypeError, но t[0] всё равно становится [1, 2, 5] D)
SyntaxError — так писать нельзя✅ Правильный ответ:
Разбор
t[0] += [5] — это не «магия», а синтаксический сахар для:t[0] = t[0].__iadd__([5])
Первая часть,
t[0].__iadd__([5]), отрабатывает штатно: список — мутируемый объект, __iadd__ меняет его на месте и возвращает ту же ссылку. На этом этапе t[0] уже физически стал [1, 2, 5].Проблема — во второй части. Python пытается выполнить
t[0] = ..., а t — кортеж, и __setitem__ у него просто не существует. Отсюда TypeError: 'tuple' object does not support item assignment.Мутация уже случилась, откат назад никто не делает. Получаем на первый взгляд абсурдную ситуацию: код падает с ошибкой, но результат мутации остаётся.
Вывод
⚡️
+= на изменяемом объекте внутри неизменяемой структуры — это два разных действия под одной строкой: in-place мутация и попытка присваивания. Первое может пройти успешно, второе — упасть. На собеседовании это отличный способ проверить, понимает ли кандидат разницу между __iadd__ и обычным __add__, а не просто заучил, что кортежи «неизменяемые».🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆3
🧰 Code Cleanup
Ручная мемоизация через словарь-аргумент
❌ Плохой код
✅ Улучшенный код
💥 Краткое объяснение
Есть нюанс:
🐍Вопросы с собесов -> ProstoPython
Ручная мемоизация через словарь-аргумент
❌ Плохой код
def fib(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
return memo[n]✅ Улучшенный код
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
💥 Краткое объяснение
memo={} — mutable default argument, тот самый классический баг-магнит: словарь создаётся один раз при определении функции и живёт между всеми вызовами. Работает это здесь случайно, а не потому что так задумано — стоит кому-то вызвать fib(5, {}) явно, и кэш перестанет работать без ошибки, просто молча.lru_cache решает ту же задачу правильно: кэш живёт в самом декораторе, изолирован от сигнатуры функции и не тянется в аргументы, где ему не место. Плюс lru_cache умеет ограничивать размер кэша (maxsize) и даёт .cache_info() для отладки — попробуй получить это от самодельного словаря без лишнего кода.Есть нюанс:
lru_cache требует, чтобы аргументы были хешируемыми. Если функция принимает списки или словари — сначала понадобится обёртка или functools.cache не подойдёт вовсе, и это стоит держать в голове.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
❤🔥3
📈 From O(n²) to O(n)
Задача
Дан список чисел и target. Нужно найти индексы двух элементов, сумма которых равна target. Гарантируется, что решение ровно одно.
❌ Наивное решение
Сложность: O(n²) по времени, O(1) по памяти.
✅ Оптимизированное решение
Сложность: O(n) по времени, O(n) по памяти.
💡 Что изменилось
Наивное решение на каждом шаге спрашивает «а есть ли где-то ещё число, дополняющее меня до target?» — и каждый раз отвечает на этот вопрос заново, перебором. Отсюда квадрат: n элементов × n элементов проверки.
Ключевая мысль оптимизации — не искать ответ, а помнить его. Вместо того чтобы на шаге
Это общий паттерн: если задача сводится к вопросу «видел ли я это (или что-то связанное с этим) раньше», почти всегда можно заменить вложенный цикл на один проход с dict или set. Цена — дополнительная память, но на собеседовании это почти всегда приемлемый trade-off, и его стоит проговорить вслух.
🐍Вопросы с собесов -> ProstoPython
Задача
Дан список чисел и target. Нужно найти индексы двух элементов, сумма которых равна target. Гарантируется, что решение ровно одно.
❌ Наивное решение
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²) по времени, O(1) по памяти.
✅ Оптимизированное решение
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return seen[complement], i
seen[num] = i
Сложность: O(n) по времени, O(n) по памяти.
💡 Что изменилось
Наивное решение на каждом шаге спрашивает «а есть ли где-то ещё число, дополняющее меня до target?» — и каждый раз отвечает на этот вопрос заново, перебором. Отсюда квадрат: n элементов × n элементов проверки.
Ключевая мысль оптимизации — не искать ответ, а помнить его. Вместо того чтобы на шаге
i заново сканировать весь массив в поисках target - nums[i], мы один раз проходим по массиву и складываем уже увиденные числа в словарь. Тогда вопрос «встречалось ли нужное дополнение раньше?» превращается из линейного поиска в O(1) обращение к hash-таблице.Это общий паттерн: если задача сводится к вопросу «видел ли я это (или что-то связанное с этим) раньше», почти всегда можно заменить вложенный цикл на один проход с dict или set. Цена — дополнительная память, но на собеседовании это почти всегда приемлемый trade-off, и его стоит проговорить вслух.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
❌ Rookie Mistakes
Сортировка, которая возвращает None
❌ Код с ошибкой
🤔 Что ожидает новичок
Что
💥 Что происходит на самом деле
🧠 Почему
Это осознанное архитектурное решение Python, а не недосмотр. Методы, которые мутируют объект in-place (
Отсюда правило: функция либо мутирует и возвращает
✅ Исправление
🐍Вопросы с собесов -> ProstoPython
Сортировка, которая возвращает None
❌ Код с ошибкой
def get_top_scores(scores):
result = scores.sort(reverse=True)
return result[:3]
🤔 Что ожидает новичок
Что
sort() отсортирует список и вернёт его — как будто это просто ещё один способ вызвать sorted(). Логика понятная: метод называется «сортировка», значит должен отдать отсортированные данные.💥 Что происходит на самом деле
TypeError: 'NoneType' object is not subscriptable
scores.sort(reverse=True) действительно сортирует список — но на месте, мутируя сам scores. А возвращает при этом None. В result попадает не список, а None, и result[:3] падает.🧠 Почему
Это осознанное архитектурное решение Python, а не недосмотр. Методы, которые мутируют объект in-place (
list.sort(), list.append(), list.reverse(), dict.update()), по конвенции возвращают None — это сигнал «я меняю существующий объект, а не создаю новый». Если бы sort() заодно и возвращал список, легко было бы перепутать: непонятно, работаешь ты с оригиналом или с копией.Отсюда правило: функция либо мутирует и возвращает
None, либо ничего не мутирует и возвращает новый объект. Смешивать эти два поведения в Python считается плохим тоном, и стандартная библиотека этому правилу следует последовательно.✅ Исправление
def get_top_scores(scores):
return sorted(scores, reverse=True)[:3]
sorted() — функция, а не метод списка. Она не трогает исходный scores и возвращает новый отсортированный список, который сразу можно использовать дальше.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥4
📈 From O(n²) to O(n)
📌 Задача
Проверить, являются ли две строки анаграммами.
❌ Наивное решение
Сложность:
✅ Оптимизированное решение
Сложность:
💡 Что изменилось?
Главная проблема —
Каждый его вызов проходит по всей строке. Когда он вызывается внутри цикла, сложность становится квадратичной.
🐍Вопросы с собесов -> ProstoPython
📌 Задача
Проверить, являются ли две строки анаграммами.
❌ Наивное решение
def is_anagram(s1, s2):
if len(s1) != len(s2):
return False
for char in s1:
if s1.count(char) != s2.count(char):
return False
return True
Сложность:
O(n²)✅ Оптимизированное решение
from collections import Counter
def is_anagram(s1, s2):
return Counter(s1) == Counter(s2)
Сложность:
O(n)💡 Что изменилось?
Главная проблема —
count().Каждый его вызов проходит по всей строке. Когда он вызывается внутри цикла, сложность становится квадратичной.
Counter подсчитывает частоту символов за один проход, после чего остаётся лишь сравнить два словаря🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍4
🧠 Interview Thinking
📌 Задача
Найти максимальную сумму двух элементов массива
👶 Как думает junior
Работает за O(n log n).
🧠 Как думает сильный кандидат
Работает за O(n)
🐍Вопросы с собесов -> ProstoPython
📌 Задача
Найти максимальную сумму двух элементов массива
nums = [8, 3, 10, 5, 7]
👶 Как думает junior
«Нужно отсортировать массив.»
def max_pair_sum(nums):
nums.sort()
return nums[-1] + nums[-2]
Работает за O(n log n).
🧠 Как думает сильный кандидат
«Мне не нужен полностью отсортированный массив. Мне нужны только два максимальных элемента.»
def max_pair_sum(nums):
first = second = float("-inf")
for num in nums:
if num > first:
second = first
first = num
elif num > second:
second = num
return first + second
Работает за O(n)
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥3
📈 От O(n log n) до O(n log k)
Задача: дан список из миллиона слов, нужно найти 5 самых частых.
Простая на вид задача, но именно на ней часто ловят кандидатов, которые злоупотребляют
❌ Наивное решение
Сложность: O(n log n) — сортируем вообще все уникальные слова, хотя нужны только 5.
✅ Оптимизированное решение
Сложность: O(n log k) — куча размером k вместо полной сортировки.
💡 Что изменилось
Разница в том, что мы спрашиваем: «мне нужны все элементы отсортированными или только первые k?».
На больших данных (n = 10⁶, k = 5) наивное решение делает ~20 млн операций сравнения, куча — около 6.6 млн.
⚡️ Вывод
Как только видишь в задаче "top-K" — это сигнал think heap, а не sort. Спроси себя: нужен ли полный порядок или только первые k элементов. Если k << n, куча выигрывает почти всегда.
🐍Вопросы с собесов -> ProstoPython
Задача: дан список из миллиона слов, нужно найти 5 самых частых.
Простая на вид задача, но именно на ней часто ловят кандидатов, которые злоупотребляют
sorted().❌ Наивное решение
from collections import Counter
def top_k_frequent(words: list[str], k: int) -> list[str]:
counts = Counter(words)
ranked = sorted(counts.items(), key=lambda x: x[1], reverse=True)
return [word for word, _ in ranked[:k]]
Сложность: O(n log n) — сортируем вообще все уникальные слова, хотя нужны только 5.
✅ Оптимизированное решение
import heapq
from collections import Counter
def top_k_frequent(words: list[str], k: int) -> list[str]:
counts = Counter(words)
return [word for word, _ in heapq.nlargest(k, counts.items(), key=lambda x: x[1])]
Сложность: O(n log k) — куча размером k вместо полной сортировки.
💡 Что изменилось
Разница в том, что мы спрашиваем: «мне нужны все элементы отсортированными или только первые k?».
sorted() — это решение первой задачи, а heap заточен именно под вторую.heapq.nlargest внутри поддерживает кучу размером k: новый элемент либо выбрасывается сразу, либо вытесняет минимум из кучи. Если k мало по сравнению с n (а на практике почти всегда так), выигрыш ощутимый — сортировка миллиона элементов ради top-5 просто не нужна.На больших данных (n = 10⁶, k = 5) наивное решение делает ~20 млн операций сравнения, куча — около 6.6 млн.
⚡️ Вывод
Как только видишь в задаче "top-K" — это сигнал think heap, а не sort. Спроси себя: нужен ли полный порядок или только первые k элементов. Если k << n, куча выигрывает почти всегда.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
🧰 Group by без боли: defaultdict вместо ручных проверок
Классический код, который встречается почти в каждом втором проекте — группировка списка объектов по ключу.
❌ Плохой код
✅ Улучшенный код
💥 Краткое объяснение
Проблема первого варианта не в том, что он не работает, а в том, что он тратит логику на несуществующую задачу: «а вдруг ключа ещё нет».
Важный нюанс:
⚡️ Одно правило
Используй
🐍Вопросы с собесов -> ProstoPython
Классический код, который встречается почти в каждом втором проекте — группировка списка объектов по ключу.
❌ Плохой код
def group_orders_by_user(orders: list[dict]) -> dict[str, list[dict]]:
grouped = {}
for order in orders:
user_id = order["user_id"]
if user_id not in grouped:
grouped[user_id] = []
grouped[user_id].append(order)
return grouped
✅ Улучшенный код
from collections import defaultdict
def group_orders_by_user(orders: list[dict]) -> dict[str, list[dict]]:
grouped = defaultdict(list)
for order in orders:
grouped[order["user_id"]].append(order)
return dict(grouped)
💥 Краткое объяснение
Проблема первого варианта не в том, что он не работает, а в том, что он тратит логику на несуществующую задачу: «а вдруг ключа ещё нет».
defaultdict берёт эту проверку на себя — при первом обращении к отсутствующему ключу он молча создаёт значение по умолчанию (в нашем случае пустой список) и продолжает работу.Важный нюанс:
defaultdict — это не просто синтаксический сахар, это отдельный тип со своим поведением при доступе через []. Если в конце функции ты возвращаешь grouped наружу как есть, у вызывающего кода могут появиться сюрпризы — любое обращение по несуществующему ключу тихо создаст новую пустую запись вместо KeyError. Поэтому в примере выше на выходе стоит dict(grouped) — это обрезает "magic"-поведение и отдаёт обычный dict.⚡️ Одно правило
Используй
defaultdict внутри функции для накопления данных, но на границе (return, экспорт наружу) всегда приводи к обычному dict — иначе поведение по умолчанию просочится туда, где его никто не ждёт.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥3
🧠 Что выведет код
Варианты ответа:
A)
B)
C)
D)
✅ Правильный ответ:B
Разбор
Интуитивно кажется, что каждая лямбда «запоминает» своё значение
Все три лямбды в списке
Это называется late binding — тело функции обращается к переменной по имени в момент вызова, а не в момент определения.
Как исправить, если нужно зафиксировать значение на каждой итерации:
Аргумент по умолчанию вычисляется один раз, в момент определения лямбды — поэтому такой трюк «замораживает» текущее значение
Вывод
Замыкания в Python захватывают переменные, а не их значения. Это всплывает везде, где создаются функции внутри циклов — от лямбд до обработчиков событий в GUI и async-калбэков. Если интервьюер спрашивает «что выведет этот код» с циклом и лямбдой — это почти всегда проверка именно на понимание late binding.
🐍Вопросы с собесов -> ProstoPython
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs])
Варианты ответа:
A)
[0, 1, 2]B)
[2, 2, 2]C)
[0, 0, 0]D)
RuntimeError✅ Правильный ответ:
Разбор
Интуитивно кажется, что каждая лямбда «запоминает» своё значение
i в момент создания. Но это не так — лямбда не копирует значение, она захватывает саму переменную i по ссылке на её область видимости.Все три лямбды в списке
funcs смотрят на одну и ту же ячейку памяти — переменную i из окружающей функции (замыкание, closure). Цикл for не создаёт новую переменную на каждой итерации, он просто переиспользует одну и ту же i и меняет её значение. К моменту, когда мы реально вызываем f() в списковом включении, цикл уже завершён, и i равна последнему значению — 2.Это называется late binding — тело функции обращается к переменной по имени в момент вызова, а не в момент определения.
Как исправить, если нужно зафиксировать значение на каждой итерации:
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # значение по умолчанию фиксируется сразу
print([f() for f in funcs]) # [0, 1, 2]
Аргумент по умолчанию вычисляется один раз, в момент определения лямбды — поэтому такой трюк «замораживает» текущее значение
i.Вывод
Замыкания в Python захватывают переменные, а не их значения. Это всплывает везде, где создаются функции внутри циклов — от лямбд до обработчиков событий в GUI и async-калбэков. Если интервьюер спрашивает «что выведет этот код» с циклом и лямбдой — это почти всегда проверка именно на понимание late binding.
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍3
📈 От O(n²) до O(n)
Задача: дан список чисел, нужно проверить, есть ли в нём два элемента, сумма которых равна
Вопрос настолько классический, что его знают почти все — но многие всё равно скатываются в перебор, потому что «так проще написать».
❌ Наивное решение
Сложность: O(n²) — для каждого элемента перебираем все последующие.
✅ Оптимизированное решение
Сложность: O(n) — один проход, поиск в множестве.
💡 Что изменилось
Ключевая идея — развернуть вопрос. Вместо «есть ли пара, дающая в сумме target» спрашиваем на каждом шаге: «я уже видел число, которое в паре с текущим даст target?». Это
Проверка
Обрати внимание на порядок действий: сначала проверяем
⚡️ Вывод
Если в задаче фигурирует «найти пару/подмножество с определённой суммой» — это почти всегда сигнал заменить вложенный цикл на set или dict. Ищи не «как перебрать все пары», а «что нужно было увидеть раньше, чтобы текущий элемент завершил пару».
🐍Вопросы с собесов -> ProstoPython
Задача: дан список чисел, нужно проверить, есть ли в нём два элемента, сумма которых равна
target.Вопрос настолько классический, что его знают почти все — но многие всё равно скатываются в перебор, потому что «так проще написать».
❌ Наивное решение
def has_pair_with_sum(nums: list[int], target: int) -> bool:
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return True
return False
Сложность: O(n²) — для каждого элемента перебираем все последующие.
✅ Оптимизированное решение
def has_pair_with_sum(nums: list[int], target: int) -> bool:
seen = set()
for num in nums:
if target - num in seen:
return True
seen.add(num)
return False
Сложность: O(n) — один проход, поиск в множестве.
💡 Что изменилось
Ключевая идея — развернуть вопрос. Вместо «есть ли пара, дающая в сумме target» спрашиваем на каждом шаге: «я уже видел число, которое в паре с текущим даст target?». Это
target - num.Проверка
in set работает в среднем за O(1) благодаря хеш-таблице под капотом, а не за O(n), как поиск в списке. Именно поэтому переход с list на set меняет асимптотику всей задачи, а не просто ускоряет константу.Обрати внимание на порядок действий: сначала проверяем
target - num in seen, потом добавляем num. Если поменять местами, для случая target == 2 * num алгоритм ошибочно сочтёт число парой самому себе.⚡️ Вывод
Если в задаче фигурирует «найти пару/подмножество с определённой суммой» — это почти всегда сигнал заменить вложенный цикл на set или dict. Ищи не «как перебрать все пары», а «что нужно было увидеть раньше, чтобы текущий элемент завершил пару».
🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍2
Ребят, запустил Telegram-канал с вакансиями для Python-разработчиков 🐍
Сейчас канал только начинает расти, поэтому хочу собрать первую аудиторию и постепенно привлечь больше работодателей.
Если вы Python-разработчик, подписывайтесь, чтобы не пропустить новые вакансии.
А если вы HR или занимаетесь наймом Python-разработчиков, просто возьмите канал на заметку. Возможно, скоро он вам пригодится 👀
t.me/python_work1
Сейчас канал только начинает расти, поэтому хочу собрать первую аудиторию и постепенно привлечь больше работодателей.
Если вы Python-разработчик, подписывайтесь, чтобы не пропустить новые вакансии.
А если вы HR или занимаетесь наймом Python-разработчиков, просто возьмите канал на заметку. Возможно, скоро он вам пригодится 👀
t.me/python_work1
🔥2
🧰 Читаемые условия: all()/any() вместо ручных флагов
Проверка «выполняется ли условие для всех/хотя бы одного элемента» — то место, где Python-код часто выдаёт бэкграунд разработчика.
❌ Плохой код
✅ Улучшенный код
💥 Краткое объяснение
Первый вариант — это ручная реализация того, что уже есть в стандартной библиотеке, причём реализация не бесплатная: заводится промежуточный флаг, состояние которого нужно отслеживать, плюс явный
Та же логика работает в обратную сторону:
🐍Вопросы с собесов -> ProstoPython
Проверка «выполняется ли условие для всех/хотя бы одного элемента» — то место, где Python-код часто выдаёт бэкграунд разработчика.
❌ Плохой код
def all_users_verified(users: list[dict]) -> bool:
is_all_verified = True
for user in users:
if not user["is_verified"]:
is_all_verified = False
break
return is_all_verified
✅ Улучшенный код
def all_users_verified(users: list[dict]) -> bool:
return all(user["is_verified"] for user in users)
💥 Краткое объяснение
Первый вариант — это ручная реализация того, что уже есть в стандартной библиотеке, причём реализация не бесплатная: заводится промежуточный флаг, состояние которого нужно отслеживать, плюс явный
break. Всё это — шум, который отвлекает от самой сути проверки.all() принимает любой итерируемый объект и возвращает True, только если каждый элемент истинный. Важный нюанс: генератор (user["is_verified"] for user in users) вычисляется лениво — как только all() натыкается на первый False, он немедленно останавливается и не проходит по остальным элементам. То есть по производительности это тот же short-circuit, что и break в ручном варианте, только без явного управления состоянием.Та же логика работает в обратную сторону:
any() возвращает True, если истинен хотя бы один элемент, и тоже останавливается на первом совпадении.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥1👌1
🧠 Interview Thinking: найти первый неповторяющийся символ
Задача: дана строка, нужно найти первый символ, который встречается в ней ровно один раз. Если такого нет — вернуть
👶 Как думает junior
Для каждого символа проверить, сколько раз он встречается в строке — и как только нашли символ с count == 1, вернуть его.
Код рабочий и на маленьких строках даже быстрый. Проблема всплывает, если спросить про сложность:
🧠 Как думает сильный кандидат
Сначала разделить задачу на две части: «посчитать частоту каждого символа» и «найти первый по порядку с частотой 1». Первую часть можно сделать за один проход, вторую — тоже за один, итого O(n) вместо O(n²).
Здесь два прохода по строке — O(2n), что асимптотически всё равно O(n). Но важно не просто написать
🎯 Что хочет интервьюер
Не код с
Хороший вопрос, который стоит задать в ответ: «а какого размера ожидается строка на входе» — если это заведомо короткие строки (пароли, коды), O(n²) может быть абсолютно приемлемым, и оверинжиниринг с
💡 Вывод
Разница между junior и сильным кандидатом здесь не в знании
🐍Вопросы с собесов -> ProstoPython
Задача: дана строка, нужно найти первый символ, который встречается в ней ровно один раз. Если такого нет — вернуть
None.find_first_unique("swiss") # "w"👶 Как думает junior
Для каждого символа проверить, сколько раз он встречается в строке — и как только нашли символ с count == 1, вернуть его.
def find_first_unique(s: str) -> str | None:
for char in s:
if s.count(char) == 1:
return char
return None
Код рабочий и на маленьких строках даже быстрый. Проблема всплывает, если спросить про сложность:
s.count(char) сам по себе O(n), и он вызывается для каждого символа — то есть в худшем случае O(n²). На строке в миллион символов это уже секунды вместо миллисекунд.🧠 Как думает сильный кандидат
Сначала разделить задачу на две части: «посчитать частоту каждого символа» и «найти первый по порядку с частотой 1». Первую часть можно сделать за один проход, вторую — тоже за один, итого O(n) вместо O(n²).
from collections import Counter
def find_first_unique(s: str) -> str | None:
counts = Counter(s)
for char in s:
if counts[char] == 1:
return char
return None
Здесь два прохода по строке — O(2n), что асимптотически всё равно O(n). Но важно не просто написать
Counter, а объяснить, зачем нужен именно второй проход по s, а не по counts: Counter в Python 3.7+ сохраняет порядок вставки, но нам нужен порядок именно исходной строки, а не порядок первого появления в словаре — хотя на практике они совпадают, полагаться на это как на гарантию не стоит без проверки документации под конкретную версию.🎯 Что хочет интервьюер
Не код с
Counter сам по себе — это можно найти в любом решении на LeetCode. Интервьюер хочет услышать, что кандидат сам заметил скрытую стоимость s.count() внутри цикла, назвал её вслух и предложил trade-off: чуть больше памяти (хеш-таблица на все уникальные символы) в обмен на линейное время вместо квадратичного.Хороший вопрос, который стоит задать в ответ: «а какого размера ожидается строка на входе» — если это заведомо короткие строки (пароли, коды), O(n²) может быть абсолютно приемлемым, и оверинжиниринг с
Counter не всегда плюс.💡 Вывод
Разница между junior и сильным кандидатом здесь не в знании
Counter, а в привычке проверять код на скрытые вложенные проходы — count(), in list, index() внутри цикла почти всегда прячут лишнюю степень сложности.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥2
❌ Rookie Mistakes
🤔 Что ожидает новичок
Ожидаемый вывод:
💥 Что происходит на самом деле
Мы поменяли
🧠 Почему
Проверить это легко:
Внешние контейнеры — разные, внутренние — общие.
✅ Исправление
Для настоящей независимой копии вложенной структуры нужен deep copy:
Для простого случая с числами внутри можно обойтись и без импорта
⚡️ Вывод
🐍Вопросы с собесов -> ProstoPython
matrix = [[0, 0, 0] for _ in range(3)]
board = matrix.copy()
board[0][0] = 1
print(matrix[0])
🤔 Что ожидает новичок
board = matrix.copy() создаёт независимую копию — значит, изменение board никак не должно затронуть matrix.Ожидаемый вывод:
[0, 0, 0]
💥 Что происходит на самом деле
[1, 0, 0]
Мы поменяли
board, а изменился и matrix, хотя вроде бы сделали копию.🧠 Почему
.copy() (как и срез matrix[:], и list(matrix)) делает shallow copy — поверхностную копию. Это значит, что копируется только внешний список, а вот его элементы копируются не по значению, а по ссылке.matrix — это список списков. Внешний .copy() создаёт новый внешний контейнер, но каждый элемент внутри — это всё та же ссылка на тот же вложенный список, что и в оригинале. board[0] и matrix[0] — это буквально один и тот же объект в памяти, просто до него можно добраться двумя путями.Проверить это легко:
print(board[0] is matrix[0]) # True
print(board is matrix) # False
Внешние контейнеры — разные, внутренние — общие.
✅ Исправление
Для настоящей независимой копии вложенной структуры нужен deep copy:
import copy
board = copy.deepcopy(matrix)
board[0][0] = 1
print(matrix[0]) # [0, 0, 0] — не изменился
Для простого случая с числами внутри можно обойтись и без импорта
copy, пересобрав вложенные списки вручную:board = [row.copy() for row in matrix]
⚡️ Вывод
.copy() копирует только один уровень вложенности. Если внутри структуры есть изменяемые объекты (списки, словари) — они останутся общими между оригиналом и копией. Для многомерных структур default should be copy.deepcopy(), а не .copy().🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
👍2
📈 От O(n²) до O(n)
Задача: даны два списка чисел, нужно проверить, есть ли между ними хотя бы один общий элемент.
❌ Наивное решение
Для каждого элемента первого списка проверяем, есть ли он во втором.
Сложность: O(n·m) — вложенный цикл, где для каждого элемента первого списка перебирается весь второй.
✅ Оптимизированное решение
Превращаем один из списков в множество и проверяем принадлежность.
Сложность: O(n + m) — построение множества линейно, проверка через
💡 Что изменилось
Здесь смена структуры данных меняет саму природу операции «проверить принадлежность». В списке она стоит O(n), потому что интерпретатор вынужден пройти по элементам один за другим. В множестве та же проверка — O(1) в среднем, потому что под капотом хеш-таблица сразу вычисляет, куда должен попасть элемент, без перебора.
Многие вместо
⚡️ Вывод
Как только видишь «есть ли пересечение / общий элемент между двумя коллекциями» — это сигнал заменить вложенный цикл на set. Отдельно стоит запомнить
🐍Вопросы с собесов -> ProstoPython
Задача: даны два списка чисел, нужно проверить, есть ли между ними хотя бы один общий элемент.
has_common(nums1=[1, 2, 3], nums2=[4, 5, 3]) # True
❌ Наивное решение
Для каждого элемента первого списка проверяем, есть ли он во втором.
def has_common(nums1: list[int], nums2: list[int]) -> bool:
for a in nums1:
for b in nums2:
if a == b:
return True
return False
Сложность: O(n·m) — вложенный цикл, где для каждого элемента первого списка перебирается весь второй.
✅ Оптимизированное решение
Превращаем один из списков в множество и проверяем принадлежность.
def has_common(nums1: list[int], nums2: list[int]) -> bool:
return not set(nums1).isdisjoint(nums2)
Сложность: O(n + m) — построение множества линейно, проверка через
isdisjoint тоже линейна.💡 Что изменилось
Здесь смена структуры данных меняет саму природу операции «проверить принадлежность». В списке она стоит O(n), потому что интерпретатор вынужден пройти по элементам один за другим. В множестве та же проверка — O(1) в среднем, потому что под капотом хеш-таблица сразу вычисляет, куда должен попасть элемент, без перебора.
Многие вместо
isdisjoint пишут set(nums1) & set(nums2) и проверяют результат на пустоту — это тоже правильно по сложности, но isdisjoint быстрее на практике, потому что может остановиться на первом же совпадении и не обязан строить пересечение целиком, если второй аргумент — любой итерируемый объект, а не только множество.⚡️ Вывод
Как только видишь «есть ли пересечение / общий элемент между двумя коллекциями» — это сигнал заменить вложенный цикл на set. Отдельно стоит запомнить
isdisjoint — он часто выразительнее и быстрее, чем ручная проверка пересечения через &.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🏆2
🧰 Code Cleanup: словарь вместо цепочки if/elif
Диспетчеризация по типу действия — то место, где длинная цепочка условий обычно сигнализирует о плохом дизайне.
❌ Плохой код
✅ Улучшенный код
💥 Краткое объяснение
Проблема цепочки
Словарь-диспетчер превращает набор веток в данные. Добавление нового типа события — это добавление одной строки в
Отдельный плюс —
⚡️ Одно правило
Если цепочка
🐍Вопросы с собесов -> ProstoPython
Диспетчеризация по типу действия — то место, где длинная цепочка условий обычно сигнализирует о плохом дизайне.
❌ Плохой код
def handle_event(event_type: str, payload: dict) -> str:
if event_type == "created":
return f"Created: {payload['id']}"
elif event_type == "updated":
return f"Updated: {payload['id']}"
elif event_type == "deleted":
return f"Deleted: {payload['id']}"
elif event_type == "archived":
return f"Archived: {payload['id']}"
else:
return f"Unknown event: {event_type}"
✅ Улучшенный код
HANDLERS = {
"created": lambda p: f"Created: {p['id']}",
"updated": lambda p: f"Updated: {p['id']}",
"deleted": lambda p: f"Deleted: {p['id']}",
"archived": lambda p: f"Archived: {p['id']}",
}
def handle_event(event_type: str, payload: dict) -> str:
handler = HANDLERS.get(event_type)
if handler is None:
return f"Unknown event: {event_type}"
return handler(payload)💥 Краткое объяснение
Проблема цепочки
if/elif не в производительности — на 4-5 условиях разницы почти нет. Проблема в том, что она плохо масштабируется: каждый новый тип события — это ещё одна ветка, вставленная куда-то в середину растущего блока, и с этим неудобно работать при code review, легко случайно сломать порядок проверок.Словарь-диспетчер превращает набор веток в данные. Добавление нового типа события — это добавление одной строки в
HANDLERS, а не правка ветвящейся логики. Поиск по ключу в словаре — O(1), так что при росте количества обработчиков (а не количества вызовов) производительность не деградирует, в отличие от if/elif, где в худшем случае приходится пройти все условия по очереди.Отдельный плюс —
HANDLERS можно тестировать и модифицировать независимо от функции handle_event, например регистрировать обработчики динамически из разных модулей.⚡️ Одно правило
Если цепочка
if/elif проверяет равенство одной и той же переменной разным константам — это структура "ключ → действие", и её почти всегда стоит превратить в словарь.🐍Вопросы с собесов -> ProstoPython
Telegram
Prosto Python | вопросы с собесов
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
🔥2