Топ вопросов по языку с ответами

⏱ 50 мин · 🧩 заданий: 5

Как пользоваться уроком

Раскройте вопрос, сначала ответьте вслух сами (это важно: на интервью вы будете говорить, а не читать), потом сверьтесь. Код в ответах запускается — меняйте его и проверяйте свои гипотезы. Хороший ответ: определение в одно-два предложения → пример → где это важно на практике.

Типы и объекты

Что такое изменяемые и неизменяемые типы? Приведите примеры.

Неизменяемый (immutable) объект нельзя поменять после создания: int, float, bool, str, bytes, tuple, frozenset, None. Любая «модификация» создаёт новый объект. Изменяемые (mutable): list, dict, set, bytearray, большинство пользовательских объектов.

Почему это важно: только неизменяемые (точнее, хешируемые) объекты могут быть ключами словаря и элементами множества; изменяемые объекты, переданные в функцию, функция может изменить «снаружи».

s = "abc"
print(id(s), id(s + "d"))      # новая строка — новый объект
lst = [1, 2]
before = id(lst)
lst.append(3)
print(before == id(lst))       # тот же объект изменился на месте

t = (1, [2, 3])                # кортеж неизменяем, но его элемент-список — изменяем
t[1].append(4)
print(t)
Чем отличается is от ==?

== сравнивает значения (вызывает __eq__), is — идентичность: один ли это объект в памяти (id(a) == id(b)). is используют для синглтонов: x is None, x is True (редко), сравнения с sentinel-объектом.

a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b, a is b, a is c)

x = 256; y = 256
print(x is y)                  # True: CPython кеширует целые от -5 до 256
x = int("1000"); y = int("1000")
print(x is y)                  # False: разные объекты — полагаться на is для чисел нельзя
Чем list отличается от tuple? Когда что использовать?

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

import sys
print(sys.getsizeof([1, 2, 3]), sys.getsizeof((1, 2, 3)))
point = (10, 20)
distances = {point: 0}
x, y = point                   # распаковка
print(distances, x, y)
Как устроен dict? Почему поиск по ключу O(1)?

Словарь — хеш-таблица. По ключу вычисляется hash(key), по хешу — номер ячейки; при коллизии CPython пробует другие ячейки (открытая адресация). Поэтому поиск, вставка и удаление в среднем O(1), а в худшем (много коллизий) — O(n). Ключи должны быть хешируемыми, а равные объекты обязаны иметь равные хеши. С Python 3.7 словарь гарантированно сохраняет порядок вставки. При заполнении таблица расширяется (амортизированно O(1) на вставку).

d = {"b": 1, "a": 2}
d["c"] = 3
print(list(d))                 # порядок вставки
print(hash("a") == hash("a"), hash((1, 2)))
try:
    {[1, 2]: "x"}
except TypeError as e:
    print(e)
Что такое хешируемый объект? Можно ли сделать свой класс ключом словаря?

Хешируемый объект имеет __hash__, значение которого не меняется за время жизни, и __eq__, согласованный с ним (a == b ⇒ hash(a) == hash(b)). Объекты пользовательских классов по умолчанию хешируемы по id. Если переопределить __eq__, Python обнулит __hash__ — нужно определить его самому (по неизменяемым полям) или использовать @dataclass(frozen=True).

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

print({Point(1, 2): "A"}[Point(1, 2)])

class Mutable:
    def __eq__(self, other):
        return True
print(Mutable.__hash__)        # None — объект стал нехешируемым
Чем поверхностная копия отличается от глубокой?

Поверхностная копия (list(x), x[:], x.copy(), copy.copy) создаёт новый контейнер, но элементы — те же объекты. Глубокая (copy.deepcopy) рекурсивно копирует и вложенные объекты.

import copy
matrix = [[1, 2], [3, 4]]
shallow = matrix.copy()
deep = copy.deepcopy(matrix)
matrix[0].append(99)
print(shallow)                 # вложенный список общий
print(deep)

grid = [[0] * 3] * 3           # классическая ловушка: три ссылки на один список
grid[0][0] = 1
print(grid)
print([[0] * 3 for _ in range(3)])   # правильно
Как Python передаёт аргументы в функцию: по значению или по ссылке?

Ни так, ни так: «по присваиванию» (call by sharing). Параметр — новое имя, привязанное к тому же объекту. Если объект изменяемый и функция его меняет — изменение видно снаружи. Если функция присваивает параметру новый объект — снаружи ничего не меняется.

def modify(items):
    items.append(4)            # меняем объект — видно снаружи

def rebind(items):
    items = [0]                # перепривязываем локальное имя — снаружи не видно

data = [1, 2, 3]
modify(data); rebind(data)
print(data)

Функции

Что не так с def f(items=[])?

Значение по умолчанию вычисляется один раз при определении функции, и один и тот же список используется во всех вызовах. Правильно — None по умолчанию и создание объекта внутри.

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

print(add(1), add(2))          # [1, 2] [1, 2] — один и тот же список
print(add.__defaults__)

def add_ok(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
print(add_ok(1), add_ok(2))
Что такое *args и **kwargs? Какие ещё бывают виды параметров?

*args собирает лишние позиционные аргументы в кортеж, **kwargs — лишние именованные в словарь. При вызове * и ** наоборот распаковывают. Порядок параметров: позиционные-только (до /), обычные, *args, только-именованные (после *), **kwargs.

def f(a, b=2, /, c=3, *args, d, e=5, **kwargs):
    print(a, b, c, args, d, e, kwargs)

f(1, 20, 30, 40, 50, d=4, z=26)
nums = [1, 2]
opts = {"d": 0}
f(*nums, **opts)
try:
    f(a=1, d=4)                # a — только позиционный
except TypeError as e:
    print(e)
Что такое области видимости и правило LEGB? Зачем global и nonlocal?

Имя ищется по порядку: Local (функция) → Enclosing (объемлющие функции) → Global (модуль) → Built-in. Присваивание внутри функции делает имя локальным для всей функции. Чтобы присвоить переменной модуля — global, переменной объемлющей функции — nonlocal.

x = "global"

def outer():
    x = "enclosing"
    def inner():
        nonlocal x
        x = "изменено в inner"
    inner()
    return x

print(outer(), x)

count = 0
def broken():
    try:
        count += 1             # присваивание делает count локальной → ещё не определена
    except UnboundLocalError as e:
        print(type(e).__name__, e)
broken()
Что такое замыкание?

Функция, которая запоминает переменные из объемлющей области видимости, даже когда та уже завершилась. Переменные хранятся в «ячейках» (__closure__). Используются для фабрик функций, декораторов, хранения состояния без класса.

def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter()
print(c(), c(), c())
print(c.__closure__[0].cell_contents)

# ловушка: позднее связывание
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])                     # [2, 2, 2]
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])                     # [0, 1, 2]
Что такое декоратор? Напишите декоратор с параметром.

Декоратор — функция, которая принимает функцию и возвращает новую (обычно обёртку). @deco над def f — это f = deco(f). Декоратор с параметрами — функция, возвращающая декоратор. functools.wraps сохраняет имя и docstring исходной функции.

import functools, time

def retry(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except ValueError as e:
                    print(f"попытка {attempt}: {e}")
            raise RuntimeError("все попытки неудачны")
        return wrapper
    return decorator

calls = iter([ValueError("сбой"), ValueError("ещё сбой"), "ok"])

@retry(times=3)
def flaky():
    """Иногда падает."""
    result = next(calls)
    if isinstance(result, Exception):
        raise result
    return result

print(flaky(), flaky.__name__, flaky.__doc__)
Что такое lambda и когда её использовать?

Анонимная функция из одного выражения: lambda x: x * 2. Уместна как короткий аргумент: key= в sorted, max, min, коллбэки. Если лямбду хочется присвоить переменной или она не помещается в строку — пишите def.

users = [{"name": "Вера", "age": 31}, {"name": "Аня", "age": 25}, {"name": "Борис", "age": 31}]
print(sorted(users, key=lambda u: (-u["age"], u["name"])))
from operator import itemgetter
print(max(users, key=itemgetter("age"))["name"])

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

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

Итерируемый объект (iterable) можно обойти в for: у него есть __iter__, возвращающий итератор (списки, строки, словари, файлы). Итератор хранит позицию обхода: у него есть __next__ (следующий элемент или StopIteration) и __iter__, возвращающий себя. Итератор одноразовый, по списку можно пройтись сколько угодно раз.

nums = [1, 2, 3]
it = iter(nums)
print(next(it), next(it), next(it))
try:
    next(it)
except StopIteration:
    print("StopIteration")
print(iter(it) is it, iter(nums) is nums)

squares = (x * x for x in nums)
print(list(squares), list(squares))   # второй раз пусто
Что такое генератор? Что делает yield?

Генератор — функция с yield: при вызове возвращает объект-генератор (итератор), а тело выполняется по частям — до очередного yield, где функция «замораживается» вместе с локальными переменными. Значения вычисляются лениво — можно обрабатывать бесконечные и огромные последовательности за O(1) памяти. yield from делегирует другому итератору.

def read_batches(items, size):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch

def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(read_batches(range(7), 3)))
import itertools
print(list(itertools.islice(fib(), 10)))
import sys
print(sys.getsizeof([x for x in range(10**5)]), sys.getsizeof(x for x in range(10**5)))
Какие бывают comprehension-выражения?

Списковое [expr for x in it if cond], множественное {...}, словарное {k: v for ...} и генераторное выражение (...). Они короче и обычно быстрее цикла с append. Не злоупотребляйте вложенностью: больше двух for — лучше обычный цикл.

words = ["яблоко", "банан", "вишня", "арбуз", "банан"]
print([w.upper() for w in words if len(w) > 5])
print({w[0] for w in words})
print({w: len(w) for w in words})
print(sum(len(w) for w in words))
matrix = [[1, 2, 3], [4, 5, 6]]
print([x for row in matrix for x in row])
print([[row[i] for row in matrix] for i in range(3)])     # транспонирование

Исключения и ресурсы

Как работают try/except/else/finally?

except ловит указанные исключения; else выполняется, если исключения не было; finally — всегда (даже при return или необработанном исключении) — для освобождения ресурсов. Ловите конкретные исключения, не голый except: (он поймает даже KeyboardInterrupt). raise ... from e сохраняет цепочку причин.

def parse(value):
    try:
        number = int(value)
    except ValueError as e:
        print(f"не число: {e}")
        return None
    else:
        print("успешно")
        return number
    finally:
        print("finally выполняется всегда")

print(parse("42"))
print(parse("abc"))
Что такое контекстный менеджер? Как написать свой?

Объект для with: __enter__ выполняется при входе, __exit__ — при выходе, даже если было исключение. Гарантирует освобождение ресурсов: файлы, блокировки, соединения, транзакции. Свой — классом с __enter__/__exit__ или функцией-генератором с @contextlib.contextmanager.

from contextlib import contextmanager
import time

@contextmanager
def timer(name):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{name}: {time.perf_counter() - start:.4f} с")

with timer("сумма"):
    sum(range(10**6))

class Transaction:
    def __enter__(self):
        print("BEGIN"); return self
    def __exit__(self, exc_type, exc, tb):
        print("ROLLBACK" if exc_type else "COMMIT")
        return False           # не подавлять исключение

try:
    with Transaction():
        raise ValueError("ошибка")
except ValueError:
    pass

Строки, модули и прочее

Как эффективно собрать строку из многих частей?

Строки неизменяемы, поэтому s += part в цикле в общем случае копирует строку каждый раз (O(n²); CPython иногда оптимизирует, но полагаться на это нельзя). Правильно — собрать части в список и сделать "".join(parts). Для форматирования — f-строки.

parts = [str(i) for i in range(10)]
print(",".join(parts))
name, price = "кофе", 199.9
print(f"{name!r:>10} | {price:8.2f} | {price * 3=}")
Что значит if __name__ == "__main__"?

У каждого модуля есть __name__. При запуске файла как программы (python app.py) оно равно "__main__", при импорте — имени модуля. Блок под условием выполняется только при прямом запуске: так один файл служит и модулем, и скриптом, а импорт не запускает лишний код.

Что такое PEP 8 и какие инструменты следят за стилем?

PEP 8 — руководство по стилю кода Python: отступы 4 пробела, snake_case для функций и переменных, CamelCase для классов, UPPER_CASE для констант, длина строки, пробелы, порядок импортов. Автоматизируют: форматеры black / ruff format, линтеры ruff / flake8 / pylint, проверка типов mypy / pyright.

Что нового в последних версиях Python?

Полезно знать хотя бы пару пунктов: 3.8 — моржовый оператор :=, позиционные-только параметры; 3.9 — dict | dict, list[int] без typing; 3.10 — match/case, X | Y в аннотациях, понятные сообщения об ошибках; 3.11 — ускорение на 10–60%, группы исключений и except*, TaskGroup; 3.12 — синтаксис обобщений def f[T](x: T), type Alias = ..., улучшенные f-строки; 3.13 — экспериментальный режим без GIL (free-threaded) и JIT, новый интерактивный REPL.

import sys
print(sys.version)
data = [3, 8, 1, 9]
if (n := len(data)) > 3:
    print(f"длинный список: {n}")
match {"type": "point", "x": 1, "y": 2}:
    case {"type": "point", "x": x, "y": y}:
        print("точка", x, y)

Итог

  • Мутабельность, хешируемость, is/==, копирование, передача аргументов — фундамент, о котором спросят почти всегда.
  • Функции: изменяемые значения по умолчанию, *args/**kwargs, LEGB, замыкания и позднее связывание, декораторы с wraps.
  • Итераторы и генераторы — ленивость и экономия памяти; comprehension — коротко и быстро.
  • Исключения ловим конкретные; ресурсы освобождаем через with.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Почему кортеж (1, [2, 3]) нельзя использовать как ключ словаря?

Задание 2 Несколько вариантов

Какие объекты неизменяемы?

Задание 3 Ввод ответа

Что выведет print([i * i for i in range(5) if i % 2])? Введите результат так, как его напечатает Python.

Задание 4 Один вариант ответа

Чем генератор (x * 2 for x in data) отличается от [x * 2 for x in data]?

Задание 5 Заполнить таблицу

Сложность операций в среднем случае

O(1)O(n)
x in some_list
x in some_set
d[key] для словаря
lst.append(x)
lst.insert(0, x)
lst.pop()
lst.pop(0)
Отметьте ячейку — «+», пустая — «−».

Войдите, чтобы отмечать пройденные уроки.