50 вопросов

50 вопросов.md

🐍 ТОП 50 вопросов на собеседование Python-разработчика

Простые ответы с примерами — понятно даже ребёнку


📦 Структуры данных и типы

1. Что такое список (list) и кортеж (tuple)? Чем отличаются?

Список — это как корзина покупок: можно добавлять, убирать, менять товары.
Кортеж — это как запаянная посылка: содержимое нельзя изменить.

lst = [1, 2, 3]   # список — изменяемый
lst[0] = 99       # ✅ можно

tpl = (1, 2, 3)   # кортеж — неизменяемый
tpl[0] = 99       # ❌ TypeError

Когда использовать кортеж? Когда данные не должны меняться: координаты, RGB-цвет, день рождения.


2. Что такое словарь (dict)?

Словарь — это телефонная книга: у каждого имени (ключа) есть номер (значение). Поиск за O(1) — мгновенно, потому что внутри хэш-таблица.

phone_book = {"Маша": "123-45-67", "Вася": "987-65-43"}
print(phone_book["Маша"])  # "123-45-67"

3. Что такое множество (set)?

Множество — это список уникальных элементов без порядка. Как список гостей: каждый человек упоминается один раз.

guests = {"Маша", "Вася", "Маша"}  # "Маша" добавится один раз
print(guests)  # {"Маша", "Вася"}

# Удаление дубликатов из списка:
unique = list(set([1, 2, 2, 3, 3]))  # [1, 2, 3]

4. Чем is отличается от ==?

  • == сравнивает значения (содержимое)
  • is сравнивает идентичность (один ли это объект в памяти)
a = [1, 2, 3]
b = [1, 2, 3]

a == b   # True  — значения одинаковые
a is b   # False — это два разных объекта в памяти

c = a
a is c   # True — c и a ссылаются на один объект

Правило: Используй is только для сравнения с None, True, False.


5. Что такое None?

None — это "ничего". Как пустая коробка. Используется когда функция ничего не возвращает или значение отсутствует.

def greet(name=None):
    if name is None:
        return "Привет, незнакомец!"
    return f"Привет, {name}!"

6. Как сделать список уникальным (без повторений)?

numbers = [1, 2, 2, 3, 3, 1]

# Быстро, но порядок не гарантирован:
unique = list(set(numbers))

# С сохранением порядка (Python 3.7+):
unique = list(dict.fromkeys(numbers))  # [1, 2, 3]

7. Что такое срезы (slices)?

Срезы — как вырезать кусок из багета: указываешь с какого места и до какого.

s = [0, 1, 2, 3, 4, 5]

s[1:4]    # [1, 2, 3]   — с 1 по 4 (не включая 4)
s[:3]     # [0, 1, 2]   — первые 3
s[-2:]    # [4, 5]      — последние 2
s[::-1]   # [5, 4, 3, 2, 1, 0] — перевернуть

🔧 Функции

8. Что такое *args и **kwargs?

  • *args — принимает любое количество позиционных аргументов (кортеж)
  • **kwargs — принимает любое количество именованных аргументов (словарь)
def show(*args, **kwargs):
    print(args)    # (1, 2, 3)
    print(kwargs)  # {"name": "Вася", "age": 25}

show(1, 2, 3, name="Вася", age=25)

Аналогия: *args — принести сколько угодно яблок, **kwargs — принести яблоки с ярлыками.


9. Почему нельзя использовать изменяемые объекты как аргументы по умолчанию?

Значение по умолчанию создаётся один раз при определении функции, а не при каждом вызове. Список живёт между вызовами!

# ❌ Плохо:
def add(item, lst=[]):
    lst.append(item)
    return lst

add(1)  # [1]
add(2)  # [1, 2] — список сохранился!

# ✅ Хорошо:
def add(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

10. Что такое лямбда-функция?

Лямбда — это маленькая анонимная функция в одну строку. Как записка вместо письма.

# Обычная функция:
def double(x):
    return x * 2

# Лямбда:
double = lambda x: x * 2

# Чаще используется "на месте":
numbers = [3, 1, 4, 1, 5]
numbers.sort(key=lambda x: -x)  # сортировка по убыванию

11. Что такое замыкание (closure)?

Замыкание — это функция, которая "запоминает" переменные из внешней функции, даже после её завершения.

def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, {name}!"  # запомнила greeting
    return greet

hello = make_greeter("Привет")
hi = make_greeter("Хай")

hello("Маша")  # "Привет, Маша!"
hi("Вася")     # "Хай, Вася!"

12. Что такое декоратор?

Декоратор — это обёртка для функции. Как коробочка вокруг подарка: подарок тот же, но с украшением.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Готово!")
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

add(2, 3)
# Вызов: add
# Готово!

13. Зачем нужен functools.wraps?

Без wraps задекорированная функция теряет своё имя и документацию. wraps сохраняет "паспорт" оригинальной функции.

from functools import wraps

def decorator(func):
    @wraps(func)          # сохраняем имя и __doc__
    def wrapper(*args):
        return func(*args)
    return wrapper

@decorator
def my_func():
    """Моя функция"""
    pass

print(my_func.__name__)  # "my_func" (без @wraps было бы "wrapper")

🔄 Итераторы и генераторы

14. Чем итерируемый объект отличается от итератора?

  • Итерируемый объект — можно обойти в for (список, строка, словарь)
  • Итератор — объект с методами __iter__ и __next__, выдаёт по одному элементу
lst = [1, 2, 3]          # итерируемый объект
it = iter(lst)           # итератор

next(it)  # 1
next(it)  # 2
next(it)  # 3
next(it)  # StopIteration

15. Что такое генератор и зачем он нужен?

Генератор — это функция с yield. Она не создаёт весь список сразу, а выдаёт по одному элементу. Экономит память!

# Список: создаёт 1 000 000 чисел в памяти сразу
nums = [x * 2 for x in range(1_000_000)]

# Генератор: создаёт числа по одному, память почти не тратит
def gen():
    for x in range(1_000_000):
        yield x * 2

# Аналогия: список — весь фильм скачан, генератор — стриминг

16. Чем [x for x in y] отличается от (x for x in y)?

  • [...]список, создаётся сразу целиком (занимает память)
  • (...)генераторное выражение, ленивое (элементы по одному)
lst = [x**2 for x in range(10)]   # список из 10 чисел в памяти
gen = (x**2 for x in range(10))   # генератор — числа по требованию

🏛️ ООП

17. Что такое __init__?

__init__ — это конструктор класса. Вызывается автоматически при создании объекта. Как заполнение анкеты при регистрации.

class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

my_dog = Dog("Шарик", "Лабрадор")
print(my_dog.name)   # "Шарик"

18. В чём разница между __init__ и __new__?

  • __new__создаёт объект (выделяет память)
  • __init__инициализирует объект (заполняет данными)

Обычно переопределяют только __init__. __new__ нужен редко — например, для синглтонов.


19. Что такое self?

self — это ссылка на сам объект. Как слово "я" для человека. Позволяет методу обращаться к данным своего объекта.

class Cat:
    def __init__(self, name):
        self.name = name       # self = "этот конкретный кот"

    def speak(self):
        return f"{self.name} говорит: Мяу!"

cat = Cat("Мурзик")
cat.speak()  # "Мурзик говорит: Мяу!"

20. Что такое наследование?

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

class Animal:
    def breathe(self):
        return "Дышу воздухом"

class Dog(Animal):        # Dog наследует Animal
    def bark(self):
        return "Гав!"

dog = Dog()
dog.breathe()  # ✅ унаследовал от Animal
dog.bark()     # ✅ собственный метод

21. Что такое super()?

super() — вызов метода родительского класса. Как сказать "сначала сделай то, что делает родитель, потом добавлю своё".

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)    # вызываем Animal.__init__
        self.breed = breed        # добавляем своё

dog = Dog("Шарик", "Хаски")

22. Что такое MRO (порядок разрешения методов)?

MRO — порядок, в котором Python ищет метод при множественном наследовании. Смотрит слева направо, снизу вверх.

class A:
    def hello(self):
        return "A"

class B(A):
    pass

class C(A):
    def hello(self):
        return "C"

class D(B, C):
    pass

D().hello()  # "C" — Python ищет по цепочке D → B → C → A
print(D.__mro__)  # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, ...)

23. Что такое магические методы (__dunder__)?

Методы с двойным подчёркиванием — "магия" Python. Они вызываются автоматически при определённых операциях.

class Box:
    def __init__(self, items):
        self.items = items

    def __len__(self):         # вызывается при len(box)
        return len(self.items)

    def __str__(self):         # вызывается при print(box)
        return f"Коробка с {len(self.items)} предметами"

box = Box([1, 2, 3])
len(box)    # 3
print(box)  # "Коробка с 3 предметами"

24. Что такое __slots__?

__slots__ фиксирует набор атрибутов объекта. Экономит память при создании миллионов объектов.

class Point:
    __slots__ = ('x', 'y')   # только эти атрибуты разрешены

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
p.z = 3  # ❌ AttributeError — z не разрешён

25. Что такое @property?

@property позволяет вызывать метод как обычный атрибут (без скобок). Удобно для валидации.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def fahrenheit(self):         # вызывается как t.fahrenheit
        return self._celsius * 9/5 + 32

t = Temperature(100)
print(t.fahrenheit)  # 212 (без скобок!)

💾 Память и производительность

26. Как Python управляет памятью?

Python использует подсчёт ссылок: каждый объект знает, сколько переменных на него ссылаются. Как только ссылок 0 — объект удаляется. Для циклических ссылок есть Garbage Collector.

a = [1, 2, 3]   # ссылок: 1
b = a            # ссылок: 2
del a            # ссылок: 1
del b            # ссылок: 0 → объект удалён

27. Что такое GIL?

GIL (Global Interpreter Lock) — замок, который позволяет в один момент времени выполняться только одному потоку Python. Это значит, что настоящего параллелизма в потоках нет.

Аналогия: Одна кассирша в супермаркете — в каждый момент обслуживает только одного покупателя, даже если очередей несколько.

  • ✅ Потоки подходят для задач с ожиданием (сеть, файлы)
  • ❌ Потоки не ускоряют математические вычисления
  • ✅ Для вычислений — используй multiprocessing

28. В чём разница между потоками и процессами?

Потоки (threading) Процессы (multiprocessing)
Память Общая Раздельная
GIL Есть (ограничение) Нет
Для чего I/O задачи CPU задачи
Создание Быстро Медленнее

29. Что такое deepcopy и copy?

import copy

original = [[1, 2], [3, 4]]

shallow = copy.copy(original)       # поверхностная копия
deep = copy.deepcopy(original)      # глубокая копия

original[0][0] = 99

print(shallow[0][0])  # 99 — изменилось! (ссылается на те же вложенные списки)
print(deep[0][0])     # 1  — не изменилось (полностью независимая копия)

Аналогия: copy — скопировать папку со ссылками на файлы; deepcopy — скопировать папку вместе со всеми файлами.


⚠️ Исключения

30. Как работает обработка исключений?

try:
    result = 10 / 0          # здесь произойдёт ошибка
except ZeroDivisionError:
    print("Делить на ноль нельзя!")
except Exception as e:
    print(f"Ошибка: {e}")
else:
    print("Всё хорошо!")     # если ошибок не было
finally:
    print("Выполняется всегда")  # всегда

31. Как создать своё исключение?

class InsufficientFundsError(Exception):
    def __init__(self, amount):
        self.amount = amount
        super().__init__(f"Не хватает {amount} рублей")

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(amount - balance)
    return balance - amount

🔀 Функциональное программирование

32. Что делают map, filter, reduce?

from functools import reduce

numbers = [1, 2, 3, 4, 5]

# map — применить функцию к каждому элементу
doubled = list(map(lambda x: x * 2, numbers))   # [2, 4, 6, 8, 10]

# filter — оставить только подходящие
evens = list(filter(lambda x: x % 2 == 0, numbers))  # [2, 4]

# reduce — свернуть в одно значение
total = reduce(lambda acc, x: acc + x, numbers)  # 15

33. Что такое lru_cache?

lru_cache запоминает результаты функции. При повторном вызове с теми же аргументами — возвращает из кэша, не считает заново.

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

fibonacci(100)  # мгновенно, результаты кэшированы

🌐 Async / Await

34. Что такое async/await?

async/await — инструмент для асинхронного кода. Пока одна задача ждёт ответа (сеть, БД), другие задачи продолжают работать.

import asyncio

async def fetch_data(url):
    print(f"Загружаю {url}...")
    await asyncio.sleep(1)    # ждём, но не блокируем
    return f"Данные с {url}"

async def main():
    # Запускаем оба запроса одновременно!
    results = await asyncio.gather(
        fetch_data("сайт1.ру"),
        fetch_data("сайт2.ру"),
    )

asyncio.run(main())

Аналогия: Официант принял заказ у стола 1, пошёл к столу 2, потом к столу 3 — а не стоит и ждёт пока приготовят еду.


🧩 Модули и пакеты

35. Как Python ищет модули при импорте?

Python ищет в таком порядке:
1. Встроенные модули (os, sys)
2. Папка с запускаемым скриптом
3. Переменная окружения PYTHONPATH
4. Стандартные системные пути

import sys
print(sys.path)   # список путей поиска

36. Что такое __name__ == "__main__"?

Защита от выполнения кода при импорте. Код внутри if запустится только если файл запущен напрямую.

def add(a, b):
    return a + b

if __name__ == "__main__":
    print(add(2, 3))   # выполнится только при python my_file.py
                        # но НЕ при import my_file

🗃️ Базы данных

37. Что такое транзакция и свойства ACID?

Транзакция — группа операций, которые выполняются как одно целое.

Свойство Что значит Пример
Atomicity (Атомарность) Всё или ничего Перевод денег: списать И зачислить
Consistency (Согласованность) Данные всегда корректны Баланс не может быть отрицательным
Isolation (Изолированность) Транзакции не мешают друг другу Два кассира не выдадут одну купюру
Durability (Долговечность) Зафиксированные данные не потеряются Даже после сбоя питания

38. Какие бывают JOIN-ы?

-- INNER JOIN — только совпадения в обеих таблицах
SELECT * FROM orders
INNER JOIN customers ON orders.customer_id = customers.id;

-- LEFT JOIN — все из левой + совпадения из правой
SELECT * FROM customers
LEFT JOIN orders ON customers.id = orders.customer_id;

-- FULL JOIN — все записи из обеих таблиц
SELECT * FROM customers
FULL JOIN orders ON customers.id = orders.customer_id;

Аналогия: Два списка гостей. INNER — кто есть в обоих. LEFT — все из первого + кто есть во втором.


🔑 Паттерны проектирования

39. Что такое Singleton?

Singleton гарантирует, что у класса есть только один экземпляр.

class Database:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

db1 = Database()
db2 = Database()
db1 is db2  # True — один и тот же объект!

40. Что такое декоратор как паттерн?

Добавляет функциональность объекту, не изменяя его код. Как надеть пальто — человек тот же, но теперь ему тепло.


🌍 HTTP и веб

41. Что такое REST?

REST — соглашение о том, как строить API. Основные принципы:
- Ресурсы в URL: /users/42
- Действия через HTTP-методы: GET, POST, PUT, DELETE
- Без состояния: каждый запрос самодостаточен

GET    /users         — получить список
POST   /users         — создать пользователя
GET    /users/42      — получить пользователя #42
PUT    /users/42      — обновить пользователя #42
DELETE /users/42      — удалить пользователя #42

42. Что такое HTTP-коды состояния?

Диапазон Значение Примеры
2xx Успех 200 OK, 201 Created
3xx Перенаправление 301 Moved, 304 Not Modified
4xx Ошибка клиента 400 Bad Request, 404 Not Found
5xx Ошибка сервера 500 Internal Error, 503 Unavailable

43. Что такое JWT?

JWT (JSON Web Token) — компактный токен для передачи данных и аутентификации. Состоит из трёх частей: заголовок, данные, подпись.

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0Mn0.SflKxwRJSMeKKF2QT4fwpMeJf
     заголовок              данные              подпись

🧪 Тестирование

44. Что такое Unit-тесты?

Unit-тест проверяет одну маленькую функцию в изоляции. Как тест одной детали машины на заводе.

import pytest

def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

45. Что такое Mock?

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

from unittest.mock import patch

def get_user(user_id):
    # В реальности идёт запрос к базе
    return database.find(user_id)

def test_get_user():
    with patch('database.find') as mock_find:
        mock_find.return_value = {"id": 1, "name": "Маша"}
        user = get_user(1)
        assert user["name"] == "Маша"

⚙️ Git и инструменты

46. Что такое git rebase vs git merge?

  • merge — сливает ветки, сохраняя всю историю коммитов (добавляет "merge commit")
  • rebase — переставляет твои коммиты поверх ветки (история линейная, чище)
merge:   A--B--C--M     (M = merge commit)
              \  /
               D--E

rebase:  A--B--C--D'--E'  (коммиты D,E перенесены наверх)

47. Что такое CI/CD?

  • CI (Continuous Integration) — автоматически запускает тесты при каждом коммите
  • CD (Continuous Delivery) — автоматически доставляет изменения на сервер после успешных тестов

Аналогия: CI — контролёр качества на конвейере. CD — автоматический грузовик доставки.


🏗️ SOLID и принципы

48. Расскажи о SOLID простыми словами

Принцип Простое объяснение
S — Single Responsibility Один класс = одна задача (повар готовит, официант подаёт)
O — Open/Closed Расширяй, не ломая старое (добавляй плагины, не трогая ядро)
L — Liskov Substitution Наследник должен работать везде, где работает родитель
I — Interface Segregation Не заставляй реализовывать ненужные методы
D — Dependency Inversion Зависеть от абстракций, не от конкретных классов

49. Что такое DRY и KISS?

DRY (Don't Repeat Yourself) — не копируй код, выноси в функцию.

# ❌ Нарушение DRY
price_usd = 100 * 0.18 + 100   # с НДС
price_eur = 200 * 0.18 + 200

# ✅ DRY
def add_tax(price, rate=0.18):
    return price * rate + price

KISS (Keep It Simple, Stupid) — не усложняй без нужды. Простой код лучше умного.


50. Что такое технический долг?

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

Признаки накопленного долга:
- Страшно трогать старый код
- Любое изменение ломает что-то ещё
- Новые разработчики долго разбираются в коде

Как бороться:
- Рефакторинг в рамках каждой задачи
- Выделять время в спринте на технический долг
- Писать тесты перед изменением legacy-кода