Prosto Python | вопросы с собесов
363 subscribers
184 photos
1 video
2 files
559 links
🚀 Python-собесы без сюрпризов! Разбираем реальные вопросы, ошибки кандидатов и лайфхаки, которые помогают пройти интервью. Джун → мидл → сеньор — прокачивайся и разнеси следующий собес! 🔥
Download Telegram
📈 From O(n²) to O(n): поиск двух чисел с заданной суммой

Задача: дан список чисел и целевая сумма target. Найти индексы двух чисел, которые в сумме дают target.

nums = [2, 7, 11, 15]
target = 9
# ответ: [0, 1], потому что nums[0] + nums[1] == 9


Наивное решение — O(n²) по времени, O(1) по памяти

def two_sum_naive(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]


Два вложенных цикла. Для каждого элемента проверяем все остальные.

При n = 10 000 это ~50 000 000 операций. При n = 100 000 — уже ~5 000 000 000.

Оптимальное решение — O(n) по времени, O(n) по памяти

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 # запоминаем что видели


Один проход. Для каждого числа спрашиваем: "а его пара уже встречалась?"

При n = 10 000 это 10 000 операций. При n = 100 000 — 100 000.

Что изменилось под капотом

Наивное решение ищет пару перебором. Оптимальное превращает задачу поиска в задачу проверки — а in для словаря это O(1).

Мы платим памятью O(n) за словарь и получаем скорость O(n) вместо O(n²).

Trade-off явный: если память критична и n небольшое — наивный вариант может быть лучше. Об этом стоит сказать на собесе сам, не дожидаясь вопроса.

Edge cases которые нельзя забыть:

# Один и тот же элемент дважды?
nums = [3, 3], target = 6 # должно вернуть [0, 1] — работает корректно


🐍Вопросы с собесов -> ProstoPython
👍4
Rookie Mistakes: изменяемый аргумент по умолчанию

Этот баг живёт в продакшене чаще, чем хочется признавать.

def add_item(item, items=[]):
items.append(item)
return items

print(add_item("a")) # ["a"]
print(add_item("b")) # ["a", "b"] -- ожидали ["b"]
print(add_item("c")) # ["a", "b", "c"] -- ожидали ["c"]


Функция ведёт себя как будто помнит предыдущие вызовы. Но откуда?

Почему это происходит

Аргументы по умолчанию вычисляются один раз — в момент определения функции, а не при каждом вызове.

Список [] создаётся ровно один раз и живёт внутри объекта функции. Каждый вызов без аргумента получает ссылку на один и тот же список.

Проверить можно напрямую:

print(add_item.__defaults__)  # (["a", "b", "c"],)


Список накапливается прямо там.

Правильный вариант

def add_item(item, items=None):
if items is None:
items = [] # новый список при каждом вызове
items.append(item)
return items

print(add_item("a")) # ["a"]
print(add_item("b")) # ["b"]


None — это иммутабельный синглтон. Он не накапливает состояние. Проверка if items is None создаёт свежий список при каждом вызове без аргумента.

🐍Вопросы с собесов -> ProstoPython
👍3
🧩 Code Cleanup

Protocol — утиная типизация со статической проверкой

Типичная ситуация — хочешь принимать любой объект с нужным методом:

class Duck:
def quack(self):
return "quack"

class Person:
def quack(self):
return "I'm quacking like a duck"

def make_it_quack(obj):
print(obj.quack())


Работает — Python не проверяет тип, только наличие метода. Но IDE не подскажет ошибку если передать неправильный объект. И из сигнатуры непонятно что ожидается.

После — Protocol:

from typing import Protocol

class Quackable(Protocol):
def quack(self) -> str:
...

def make_it_quack(obj: Quackable) -> None:
print(obj.quack())


Теперь mypy и IDE знают что ожидается. Duck и Person подходят автоматически — без наследования.

make_it_quack(Duck())    # OK
make_it_quack(Person()) # OK
make_it_quack("string") # mypy: ошибка — нет метода quack


Чем отличается от ABC:

# ABC — явное наследование обязательно
class Animal(ABC):
@abstractmethod
def speak(self): ...

class Dog(Animal): # должен явно наследовать
def speak(self): return "woof"

# Protocol — наследование не нужно
class Speakable(Protocol):
def speak(self) -> str: ...

class Dog: # ничего не наследует
def speak(self): return "woof" # автоматически подходит


Protocol — структурная типизация. ABC — номинальная.

Реальный кейс:

from typing import Protocol

class Serializable(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...

def save(obj: Serializable, path: str) -> None:
data = obj.to_dict()
...


Любой класс с этими методами подходит — без изменения его кода.

🐍Вопросы с собесов -> ProstoPython
🏆3
📈 FROM O(...) TO O(...)

Топологическая сортировка: от O(n²) до O(V+E)

Задача: есть список задач с зависимостями. Найти порядок выполнения.

# Задачи и их зависимости
# "одеться" требует "надеть носки" и "надеть рубашку"
# "надеть пиджак" требует "одеться"

tasks = {
"носки": [],
"рубашка": [],
"одеться": ["носки", "рубашка"],
"пиджак": ["одеться"],
"галстук": ["рубашка"],
}
# Ответ: ['носки', 'рубашка', 'одеться', 'галстук', 'пиджак']
# или любой другой валидный порядок


Наивное решение — повторные проходы:

def topo_sort_naive(tasks):
result = []
remaining = dict(tasks)
while remaining:
for task, deps in list(remaining.items()):
if all(d in result for d in deps):
result.append(task)
del remaining[task]
break
else:
raise ValueError("Цикл в зависимостях")
return result


На каждом шаге ищем задачу без зависимостей — O(n²) в худшем случае.

Алгоритм Кана — O(V+E):

from collections import deque

def topo_sort(tasks):
in_degree = {task: 0 for task in tasks}
for task, deps in tasks.items():
for dep in deps:
in_degree[task] += 1

queue = deque([t for t, d in in_degree.items() if d == 0])
result = []

while queue:
task = queue.popleft()
result.append(task)
for dependent, deps in tasks.items():
if task in deps:
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)

if len(result) != len(tasks):
raise ValueError("Цикл в зависимостях")
return result


O(V+E) — каждая вершина и каждое ребро обрабатываются ровно один раз.

Идея:

Считаем входящие зависимости для каждой задачи. Начинаем с тех у кого 0 зависимостей. Обрабатываем — уменьшаем счётчик зависимостей у соседей. Новые нули добавляем в очередь.

Бонус — обнаружение цикла:

Если в результате меньше вершин чем в графе — есть цикл. Задачи зависят друг от друга и никогда не получат нулевой счётчик.

🐍Вопросы с собесов -> ProstoPython
👍3
В Django есть 3 типа наследования.

Abstract Base Class

🔹 Не создаёт таблицу в БД
🔹 Используется для переиспользования полей

class Base(models.Model):
created_at = models.DateTimeField()

class Meta:
abstract = True


Multi-table inheritance

🔹 Каждая модель → отдельная таблица
🔹 Связь через OneToOne

class Base(models.Model):
name = models.CharField(max_length=100)

class Employee(Base):
salary = models.IntegerField()


Proxy model

🔹 Таблица не создаётся
🔹 Меняется только поведение (методы, менеджеры)

class UserProxy(User):
class Meta:
proxy = True


🐍Вопросы с собесов -> ProstoPython
👍2
⏱️ Big O Breakdown — выпуск 24

Посмотри на код:

def example(n):
i = 0
j = 0

while i < n:
i += 1

while j < n:
j += 1


Какая сложность?

A) O(n)
B) O(n log n)
C) O(n²)
D) O(1)

Правильный ответ: A) O(n)

🧠 Разбор

Первый цикл:

выполняется n раз → O(n)


Второй цикл:

выполняется n раз → O(n)


Теперь объединяем:

O(n) + O(n) = O(2n)


В Big O константы отбрасываются:

O(2n) → O(n)


📌 Итог

Даже если циклов два,
это не значит O(n²).

Важно:

они выполняются последовательно,
а не вложенно.

🐍Вопросы с собесов -> ProstoPython
🔥4
📈 FROM O(...) TO O(...)

Edit Distance: от O(3ⁿ) до O(n·m)

Задача: найти минимальное количество операций чтобы превратить одну строку в другую. Операции — вставка, удаление, замена символа.

s1 = "kitten"
s2 = "sitting"
# Ответ: 3
# kitten → sitten (замена k→s)
# sitten → sittin (замена e→i)
# sittin → sitting (вставка g)


Это называется расстояние Левенштейна. Используется в spell checker, git diff, поиске похожих строк.

Наивное решение — рекурсия:

def edit_distance(s1, s2, i, j):
if i == 0:
return j # вставить j символов
if j == 0:
return i # удалить i символов
if s1[i-1] == s2[j-1]:
return edit_distance(s1, s2, i-1, j-1)
return 1 + min(
edit_distance(s1, s2, i-1, j), # удаление
edit_distance(s1, s2, i, j-1), # вставка
edit_distance(s1, s2, i-1, j-1) # замена
)


O(3ⁿ) — три рекурсивных вызова на каждом шаге. На строках длиной 10 — тысячи повторных вычислений.

DP — O(n·m):

def edit_distance(s1, s2):
n, m = len(s1), len(s2)
dp = [[0] * (m + 1) for _ in range(n + 1)]

for i in range(n + 1):
dp[i][0] = i # удалить i символов
for j in range(m + 1):
dp[0][j] = j # вставить j символов

for i in range(1, n + 1):
for j in range(1, m + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # удаление
dp[i][j-1], # вставка
dp[i-1][j-1] # замена
)

return dp[n][m]


Визуализация для "cat" и "cut":

    ""  c  u  t
"" 0 1 2 3
c 1 0 1 2
a 2 1 1 2
t 3 2 2 1 ← ответ


Символы совпали — берём диагональ. Не совпали — минимум из трёх соседей плюс 1.

Оптимизация до O(m) памяти:

Как и в LCS — хранить только две строки таблицы.

🐍Вопросы с собесов -> ProstoPython
5
🧰 Code Cleanup

На первый взгляд — обычный код.
Но именно так пишут на автомате… и теряют читаемость.

Плохой код

users = [
{"name": "Alice", "age": 17},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 16},
]

result = []

for u in users:
if u["age"] >= 18:
result.append(u["name"])

print(result)


Улучшенный код

result = [user["name"] for user in users if user["age"] >= 18]


💥 Объяснение

Работает и так, и так.

Но есть нюанс.

👉 В первом варианте ты заставляешь мозг держать:

🔷 временный список
🔷 цикл
🔷 условие
🔷 мутацию (append)

👉 Во втором — всё выражено как одна мысль:

«возьми имена пользователей старше 18»

И это ключевое отличие.

🧠 Где здесь ловушка


Многие думают:

🔷 «list comprehension — это просто короче»

На самом деле:

🔷 Это декларативный стиль, а не просто синтаксический сахар

Ты описываешь *что хочешь получить*, а не *как это собирать по шагам*.

⚠️ Но не переусердствуй

Вот так — уже плохо:

result = [u["name"].upper() for u in users if u["age"] >= 18 and u["name"].startswith("A")]


Становится сложно читать.

🐍Вопросы с собесов -> ProstoPython
👍4
UDP (User Datagram Protocol) — транспортный протокол для передачи данных без установления соединения.

Особенности

🔹 нет установки соединения (connectionless)
🔹 нет гарантии доставки
🔹 нет порядка доставки
🔹 минимальные накладные расходы → высокая скорость

Когда используется

🔹 стриминг (видео/аудио)
🔹 онлайн-игры
🔹 DNS-запросы

Итог: UDP — быстрый, но ненадёжный протокол передачи данных без подтверждений и контроля доставки.

🐍Вопросы с собесов -> ProstoPython
🔥3
Rookie Mistakes

На первый взгляд — код абсолютно безопасный.
Но именно здесь многие ловят странные баги.

Пример с ошибкой

def add_user(name, data={}):
data[name] = len(name)
return data

print(add_user("Alice"))
print(add_user("Bob"))
print(add_user("Charlie"))


🤯 Ожидание

{'Alice': 5}
{'Bob': 3}
{'Charlie': 7}


💥 Реальность

{'Alice': 5}
{'Alice': 5, 'Bob': 3}
{'Alice': 5, 'Bob': 3, 'Charlie': 7}


🧠 Почему это ошибка

На первый взгляд кажется:
data={} создаётся заново при каждом вызове.

Но…

👉 Значение по умолчанию создаётся один раз
👉 И это один и тот же словарь

Ты не создаёшь новый dict — ты каждый раз дописываешь в старый

⚠️ Именно здесь чаще всего ошибаются

Многие думают:

«Функция вызвалась → значит всё внутри новое»

На самом деле:

Дефолтные аргументы живут между вызовами


Исправленный вариант

def add_user(name, data=None):
if data is None:
data = {}
data[name] = len(name)
return data


Теперь каждый вызов создаёт новый словарь.


⚡️ Краткий вывод

Если видишь в аргументах:

{} / [] / set()


👉 это потенциальный баг

🐍Вопросы с собесов -> ProstoPython
👍4
🧩 Code Cleanup

itertools — перестань писать вложенные циклы руками

Типичный код когда нужно перебрать все пары:


colors = ["red", "blue"]
sizes = ["S", "M", "L"]

result = []
for color in colors:
for size in sizes:
result.append((color, size))


Работает. Но две вложенности только ради декартова произведения — многовато.

После:


from itertools import product

result = list(product(colors, sizes))
# [('red','S'),('red','M'),('red','L'),('blue','S'),('blue','M'),('blue','L')]


Одна строка. Читается как задача — "произведение цветов и размеров".

🐍Вопросы с собесов -> ProstoPython
👍4
Rookie Mistakes

Не сравнивай float через ==

Код который ломается в самый неожиданный момент:


total = 0.1 + 0.2
if total == 0.3:
print("равно")
else:
print("не равно")


Выведет "не равно". Всегда.


print(0.1 + 0.2) # 0.30000000000000004


Числа с плавающей точкой хранятся в бинарном виде — и большинство десятичных дробей не представимы точно. 0.1 в памяти это не ровно 0.1 — это ближайшее представимое число.

🐍Вопросы с собесов -> ProstoPython
👍3
В множество (set) можно помещать только hashable (хешируемые) объекты.

Требования

🔹 объект имеет __hash__()
🔹 объект неизменяемый (immutable)
🔹 корректно реализует __eq__()

Можно

🔹 int, float, str, bool
🔹 tuple (если внутри тоже hashable)
🔹 frozenset

Нельзя

🔹 list
🔹 dict
🔹 set

(они изменяемые → нет стабильного хеша)

🐍Вопросы с собесов -> ProstoPython
🔥4
🧰 Code Cleanup

На первый взгляд — аккуратный код.
Но в нём есть дублирование, которое замедляет чтение.

Плохой код

def get_active_users(users):
result = []

for user in users:
if "is_active" in user:
if user["is_active"]:
result.append(user["name"])

return result


Улучшенный код

def get_active_users(users):
return [user["name"] for user in users if user.get("is_active")]


💥 Объяснение

В первом варианте происходит лишнее:

👉 Ты сначала проверяешь наличие ключа
👉 Потом сразу используешь его же

Это дублирование логики.

🐍Вопросы с собесов -> ProstoPython
🔥4
🧠 Interview Thinking

Классическая задача.
Но валятся здесь не из-за кода — а из-за мышления.

📌 Задача

Дана строка:

s = "leetcode"


Нужно вернуть индекс первого уникального символа.
Если нет — вернуть -1.

👶 Как думает junior

def first_unique(s):
for i in range(len(s)):
if s.count(s[i]) == 1:
return i
return -1


На первый взгляд — логично.

👉 Проверяем каждый символ
👉 Если встречается 1 раз — возвращаем индекс

💥 Проблема

Метод count() — это O(n)

А он вызывается внутри цикла.

👉 Итог: O(n²)

На маленьких строках — ок
На больших — просадка


🧠 Как думает сильный кандидат

from collections import Counter

def first_unique(s):
freq = Counter(s)

for i, char in enumerate(s):
if freq[char] == 1:
return i

return -1


🚀 Что изменилось

👉 Сначала считаем частоты → O(n)
👉 Потом один проход → O(n)

Итого: O(n)

⚠️ Но дело не только в оптимизации

Сильный кандидат:

🔷 сразу проговаривает сложность
🔷 замечает повторные операции
🔷ищет способ вынести их из цикла


🎯 Что хочет интервьюер

Не просто решение.

А чтобы ты сказал:

«Здесь есть повторный подсчёт.
Я могу вынести его в отдельную структуру»

🐍Вопросы с собесов -> ProstoPython
🔥3
📈 From O(n²) to O(n)

Задача простая.
Но именно здесь многие застревают в brute force.

📌 Задача

Дан массив чисел и число target.

Нужно найти два индекса, сумма элементов по которым равна target.

nums = [2, 7, 11, 15]
target = 9


Ответ: [0, 1]


Наивное решение (brute force)

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²)

На маленьких массивах — нормально
На больших — боль

🧠 Ключевой вопрос

На первый взгляд кажется:
«Ну а как иначе? Надо же проверить все пары»

Но…

👉 А если мы будем помнить, что уже видели?

Оптимизированное решение

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(1)

Итого: O(n)

⚠️ Где часто ошибаются

Сначала добавляют в seen, потом проверяют
→ ловят кейс, где элемент используется дважды

Правильно:

👉 сначала проверка
👉 потом добавление

🐍Вопросы с собесов -> ProstoPython
👍3
Попробуйте ответить без запуска, что выведет код, разбор будет через 2 часа

🐍Вопросы с собесов -> ProstoPython
👍4
Правильный ответ:
[1, 2, 3, 4]
[1, 2, 3, 4]

💥 Разбор

На первый взгляд кажется:

data += [4] создаёт новый список

Но это не так.

🧠 Что происходит на самом деле

Для списков:

data += [4]

👉 это мутация (аналог extend)
👉 объект изменяется на месте

А data и lst — это одна и та же ссылка.

⚠️ Где ловушка

Многие думают:

+= всегда создаёт новый объект

Но в Python:

для list → мутация
для tuple, str → новый объект

Один и тот же оператор — разное поведение.

🐍Вопросы с собесов -> ProstoPython
👍3
🧰 Code Cleanup

На первый взгляд — нормальный код.
Но в нём скрытая неэффективность и шум.

Плохой код

def group_by_type(items):
result = {}

for item in items:
if item["type"] not in result:
result[item["type"]] = []

result[item["type"]].append(item)

return result


Улучшенный код

from collections import defaultdict

def group_by_type(items):
result = defaultdict(list)

for item in items:
result[item["type"]].append(item)

return result


💥 Объяснение

В первом варианте:

👉 дважды обращаешься к item["type"]
👉 руками обрабатываешь инициализацию

Код работает, но перегружен лишними действиями.

🧠 Что здесь неочевидно


Многие думают:

«Ну это же стандартный паттерн»

На самом деле:

ты берёшь на себя работу, которую уже решил Python

⚠️ Где часто ошибаются

Пишут так «по привычке»:

if key not in dict:
dict[key] = []


Хотя это почти всегда можно упростить.

⚡️ Маленькое правило

👉 Если собираешь значения в группы → смотри в сторону defaultdict
👉 Убирай повторные обращения к одним и тем же данным

🐍Вопросы с собесов -> ProstoPython
👍4
⏱️ Big O Breakdown

def process(matrix):
result = []
for row in matrix:
for val in row:
result.append(val)
return result


matrix — это список из n элементов суммарно (например, 100 чисел, разбитых на строки разной длины).

Какая сложность?

A) O(n)
B) O(n²)
C) O(n · m)
D) Зависит от формы матрицы

Правильный ответ: A — O(n)

Разбор


Глаз видит два вложенных цикла и автоматически кричит «O(n²)!». Это рефлекс, и он часто врёт.

Сложность считается не по количеству циклов, а по числу итераций относительно размера входа.

Здесь внутренний
for val in row пробегает по элементам строки. А внешний — по строкам. В сумме мы касаемся каждого элемента ровно один раз.

Если всего элементов
n — мы делаем n шагов. Это O(n).

🐍Вопросы с собесов -> ProstoPython
🔥4