Топ вопросов по языку с ответами
Как пользоваться уроком
Раскройте вопрос, сначала ответьте вслух сами (это важно: на интервью вы будете говорить, а не читать), потом сверьтесь. Код в ответах запускается — меняйте его и проверяйте свои гипотезы. Хороший ответ: определение в одно-два предложения → пример → где это важно на практике.
Типы и объекты
Что такое изменяемые и неизменяемые типы? Приведите примеры.
Неизменяемый (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, [2, 3]) нельзя использовать как ключ словаря?
Какие объекты неизменяемы?
Что выведет print([i * i for i in range(5) if i % 2])? Введите результат так, как его напечатает Python.
Чем генератор (x * 2 for x in data) отличается от [x * 2 for x in data]?
Сложность операций в среднем случае
| 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) |
Войдите, чтобы отмечать пройденные уроки.