RecallDeck

Направление

Подготовка к собеседованию — Backend-инженер

Колода из 584+ карточек с вопросами для собеседования по направлению «Backend-инженер» — по темам и уровню сложности, с возвратом ровно перед тем, как вы забудете. Посмотрите несколько карточек ниже, затем войдите, чтобы учить всё направление по расписанию в стиле Anki (SM-2).

584 карточки17 тем

Бесплатно · вход через GitHub · ваш прогресс остаётся с вами.

Что внутри

Все темы направления, сгруппированные так, как вы будете их учить.

Python

130 карточек
CoreData Model InternalsConcurrency AsyncStdlib Typing Testing

Databases

104 карточки
Sql FundamentalsPostgres Internals OptimizationNosql Redis

Backend

175 карточек
Http Rest ApiArchitecture ScalingAuth SecurityFrameworks Django FastapiOrm Sqlalchemy

CS Fundamentals

76 карточек
Data Structures AlgorithmsNetworks

DevOps & Infra

35 карточек
Docker Cicd Linux

System Design

29 карточек
System Design

Behavioral

35 карточек
Behavioral

Примеры вопросов

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

Чем отличаются изменяемые (mutable) и неизменяемые (immutable) типы?

Короткий ответ: Изменяемые объекты можно менять «на месте» без создания нового объекта (list, dict, set, bytearray), неизменяемые — нет (int, float, str, bytes, tuple, frozenset, bool, None). Любое «изменение» неизменяемого создаёт новый объект.

Подробно: Изменяемость — это про то, может ли объект поменять своё содержимое, сохранив тот же id() (адрес в памяти).

# Неизменяемый: операция создаёт НОВЫЙ объект
s = "hello"
print(id(s))
s += " world"      # создаётся новая строка
print(id(s))       # id поменялся — это другой объект

# Изменяемый: меняется тот же объект
lst = [1, 2, 3]
print(id(lst))
lst.append(4)      # меняем на месте
print(id(lst))     # id тот же

Почему это важно:

  • Хешируемость. Только неизменяемые (по содержимому) объекты можно класть в dict-ключи и set. Если бы ключ менялся, его хеш «уехал» бы, и его нельзя было бы найти.
  • Безопасность при шаринге. Неизменяемый объект можно безопасно расшарить между потоками/функциями — никто его не испортит.
  • Аргументы функций. Передача изменяемого объекта означает, что функция может его поменять (см. вопрос про передачу аргументов).

⚠️ Ловушка: tuple неизменяем, но если в нём лежит list, этот список можно поменять:

t = ([1, 2], 3)
t[0].append(99)   # OK! список внутри мутабелен
print(t)          # ([1, 2, 99], 3)
# t[0] = [...]    # вот ЭТО — TypeError, переприсвоить элемент нельзя

А ещё hash(([1,2], 3)) упадёт — кортеж нехешируем, если внутри есть нехешируемый элемент.

Чем отличается list от tuple?

Короткий ответ: list изменяемый, tuple — нет. tuple хешируем (если все элементы хешируемы), занимает меньше памяти, чуть быстрее создаётся. list используют для однородных изменяемых коллекций, tuple — для фиксированных гетерогенных записей.

Подробно:

Признак list tuple
Изменяемость да нет
Хешируемость нет да (если элементы хешируемы)
Память больше (есть запас под рост) меньше
Создание медленнее быстрее (литерал может кешироваться)
Семантика коллекция однородных элементов запись фиксированной структуры
import sys
print(sys.getsizeof([1, 2, 3]))   # ~88 байт
print(sys.getsizeof((1, 2, 3)))   # ~64 байта

Почему list больше: он держит переаллокацию (over-allocation) — заранее выделяет место под будущие append, чтобы амортизировать стоимость роста до O(1). tuple фиксирован, ему запас не нужен.

Когда что выбирать:

  • tuple — когда количество и смысл элементов фиксированы: координаты (x, y), возврат нескольких значений из функции, ключ словаря.
  • list — когда коллекция растёт/меняется/сортируется.

⚠️ Ловушка: «tuple быстрее» — правда лишь для создания литерала и доступа, но не магически во всём. И помни про вложенный мутабельный объект: tuple гарантирует неизменность ссылок, а не объектов, на которые они указывают.

Что такое хешируемость и зачем она нужна?

Короткий ответ: Объект хешируем, если у него есть __hash__(), возвращающий стабильное значение за время жизни, и __eq__(). Хешируемость нужна, чтобы быть ключом dict или элементом set.

Подробно: Контракт: если a == b, то hash(a) == hash(b). Обратное не обязано выполняться (коллизии разрешены). По умолчанию пользовательские объекты хешируются по id().

hash((1, 2))        # ок
hash(frozenset()))  # ок
# hash([1, 2])      # TypeError: unhashable type: 'list'

Если переопределяешь __eq__, то __hash__ автоматически становится None (объект перестаёт быть хешируемым) — Python защищает контракт. Нужно задать __hash__ явно:

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
    __hash__ = lambda self: hash((self.x, self.y))

⚠️ Ловушка: Нельзя делать __hash__ по изменяемому полю. Если объект-ключ изменится после вставки, его хеш «уедет», и d[key] его не найдёт — он застрянет в словаре навсегда.

Как Python передаёт аргументы в функцию?

Короткий ответ: «Pass by object reference» (передача по ссылке на объект, она же «call by sharing»). В функцию передаётся ссылка на тот же объект. Мутабельный объект можно изменить изнутри, переприсваивание имени внутри — нет.

Подробно: Имя в Python — это ярлык на объект. При вызове параметр становится новым ярлыком на тот же объект.

def mutate(lst):
    lst.append(99)        # меняем сам объект → видно снаружи

def rebind(lst):
    lst = [0, 0, 0]       # переприсваиваем ЛОКАЛЬНОЕ имя → снаружи не видно

x = [1, 2]
mutate(x);  print(x)   # [1, 2, 99]
rebind(x);  print(x)   # [1, 2, 99] — не изменилось

То есть это не «pass by value» (копии нет) и не классический «pass by reference» (нельзя переприсвоить переменную вызывающего). Передаётся значение ссылки.

⚠️ Ловушка: Неизменяемые объекты создают иллюзию «pass by value»:

def inc(n): n += 1      # n = n + 1 создаёт новый int, имя локально
a = 5; inc(a); print(a) # 5 — не изменилось

Чтобы «вернуть» изменение неизменяемого — возвращай значение через return.

В чём разница между is и ==?

Короткий ответ: == сравнивает значения (вызывает __eq__). is сравнивает идентичность — это один и тот же объект в памяти (id(a) == id(b)).

Подробно:

a = [1, 2, 3]
b = [1, 2, 3]
a == b     # True  — значения равны
a is b     # False — разные объекты

c = a
a is c     # True  — один объект

is используют для сравнения с синглтонами: None, True, False.

if x is None:  ...     # правильно
if x == None:  ...     # работает, но плохой стиль и медленнее

⚠️ Ловушка: is иногда «случайно» совпадает с == из-за кеширования объектов (см. следующий вопрос), и новички пишут if x is 256 — оно работает в REPL для мелких чисел и ломается для крупных. Никогда не используй is для сравнения значений чисел/строк.

Что такое *args и **kwargs, и какие бывают виды аргументов?

Короткий ответ: *args собирает лишние позиционные аргументы в кортеж, **kwargs — лишние именованные в словарь. В сигнатуре / отделяет positional-only, * — keyword-only аргументы.

Подробно:

def f(a, b, /, c, d, *args, e, f=10, **kwargs):
    ...
#      ^^^^      ^^^^^      ^^^^^^^^^
#  positional-   обычные   keyword-only (после *)
#  only (до /)
  • До /только позиционные (нельзя передать по имени).
  • После * или *argsтолько по имени (keyword-only).
  • *argstuple, **kwargsdict.

Распаковка при вызове:

def add(a, b, c): return a + b + c
nums = [1, 2, 3]
add(*nums)                     # распаковка списка в позиционные
d = {"a": 1, "b": 2, "c": 3}
add(**d)                       # распаковка словаря в именованные

Зачем positional-only (/): чтобы имена параметров не стали частью публичного API и их можно было переименовать. Зачем keyword-only (*): заставить вызывающего писать func(verbose=True) для читаемости и защиты от перепутанного порядка.

⚠️ Ловушка: Имена args/kwargs — соглашение, важны именно * и **. И порядок при распаковке нескольких источников должен быть валидным: f(*a, *b, **c, **d) допустимо, но ключи в **c/**d не должны конфликтовать.

Готовы закрепить навсегда?

Первая сессия — меньше минуты. Ваше будущее «я» на собеседовании скажет спасибо.

Другие направления

RecallDeckПодготовка к собеседованию на интервальных повторениях