Python · Подготовка к собеседованию
Вопросы на собеседовании по Python: короткие ответы
Короткий ответ из каждого урока линии — тот же текст, что в уроке, только без подписи «Короткий ответ:», — и вопросы из его раздела «Дальше спросят». Ответы на эти вопросы, механизм и замеры, на которых всё это стоит, — в уроке по ссылке.
01
Классы и ООП
Класс хранит общие для экземпляров атрибуты и методы, экземпляр обычного класса — свои данные в собственном словаре. Метод — функция в классе,
self— её первый аргумент:a.m()равносильноA.m(a). Чтение обычного атрибута ищет в экземпляре, потом в классе; присваивание через экземпляр пишет в экземпляр.@classmethodполучает класс,@staticmethod— ничего.Дальше спросят
02
Встроенные коллекции
Список — упорядоченный изменяемый набор с повторами, кортеж — неизменяемый, словарь — ключ → значение с уникальными ключами, множество — уникальные элементы без порядка. Проверку вхождения и поиск по значению делают по множеству или словарю: там это поиск по хешу — O(1) в среднем и O(n) в худшем случае, когда хеши совпадают, — а в списке — перебор. Элементом множества и ключом словаря может быть только хешируемый объект; значения словаря могут быть любыми. Словарь хранит порядок вставки, множество — нет.
Дальше спросят
03
Модули и импорт
importнаходит модуль и связывает имя. Тело модуля выполняется при первой успешной загрузке; повторныйimportпод тем же полным именем в том же интерпретаторе берёт модуль из словаряsys.modules.from m import xкопирует ссылку на значение в момент импорта — новое присваиваниеm.xеё не меняет.if __name__ == "__main__"отделяет код запуска от кода, который выполнится при импорте.Дальше спросят
04
Сложность операций
O-нотация описывает, как растёт работа с ростом входа; строго это верхняя оценка.
x in list,insert(0, x),remove— O(n);x in set,d[key]— O(1) в среднем и O(n) в худшем случае, когда хеши совпадают;append— амортизированное O(1);bisect— O(log n);sorted()— O(n log n) в среднем и худшем случае. Сложность кода — это число итераций, умноженное на стоимость операции внутри.Дальше спросят
05
Строки и форматирование
Строка неизменяема — методы и
+=возвращают результат, а исходная строка не меняется. Срезыs[i:j:k]не включают правую границу, отрицательные индексы считаются с конца, границы среза урезаются до длины строки — поэтому срез целиком за её пределами пуст. Форматируют f-строками со спецификацией после двоеточия. Строку из многих кусков собирают через"".join(...): документация называет повторную склейку квадратичной, а быстрый путь CPython — условная оптимизация.str— кодовые точки Юникода,bytes— байты, между ними —encodeиdecode.Дальше спросят
06
Тестирование
Тест вызывает код и проверяет результат; в
unittestFAIL — сработавшая проверка (AssertionError), ERROR — любое другое исключение. Внешние зависимости подменяютMockчерезpatch— в том модуле, где имя используется. Главное утверждение теста — о результате кода; проверка вызова mock — дополнительная. Моки создают по настоящему объекту:Mock(spec=…)отвергает несуществующие имена, а неверный вызов ловит толькоcreate_autospecилиautospec=True. Тесты независимы: на порядок их выполнения не опираются, состояние готовится вsetUpили фикстуре.Дальше спросят
07
Потоки и процессы
Потоки делят память процесса, процессы изолированы. В обычном CPython — сборке с GIL — потоки выполняют Python-код по очереди, поэтому для ожидания — сети, диска — берут потоки или asyncio, а для вычислений на Python — процессы; расширения на C могут отпускать GIL сами. Процессам задачи передают через pickle, запуск процессов дороже, а пул процессов создают в
main()подif __name__ == "__main__":. GIL не избавляет от гонок: общее состояние защищаютLock.Дальше спросят
08
Окружения и зависимости
Виртуальное окружение — папка со своим интерпретатором и своим
site-packages; создаётсяpython -m venv. Интерпретатор окружения находит его сам поpyvenv.cfg, аactivateнужен оболочке — черезPATHкомандаpythonнаходит интерпретатор окружения. Пакеты ставят в окружение черезpython -m pip, а установленные версии записывают вrequirements.txtчерезpip freeze— это снимок установленного, а не lock-файл. В системный Python пакеты не ставят: окружение изолирует версии проектов.Дальше спросят
09
Декораторы
@dнадdef f— этоf = d(f), выполненное один раз, в момент, когда интерпретатор дошёл доdef. Когда декораторов несколько, выражения после@вычисляются сверху вниз, а применяются снизу вверх.functools.wrapsделает обёртку похожей на цель, а не равной ей: сигнатуру цели показываетinspect, идущий по__wrapped__, а сама обёртка по-прежнему принимает*args, **kwargs.Дальше спросят
10
Дескрипторы
Дескриптор — это объект, который лежит в классе и вмешивается в обращение к атрибуту экземпляра; признак один — у его типа есть
__get__,__set__или__delete__. Всё поведение выводится из одной строки приоритета: дескриптор данных (есть__set__или__delete__) сильнее словаря экземпляра, дескриптор не-данных (только__get__) — слабее.property, обычный метод,staticmethod,classmethod, слот иcached_propertyуже дескрипторы.Дальше спросят
11
Итераторы и генераторы
Итерируемое умеет отдать итератор (
__iter__), итератор умеет отдать следующий элемент (__next__) и обязан отдавать себя из__iter__. Разница в одном методе, и в поведении она тоже одна: список обходится дважды, итератор — нет. Генератор — это итератор, написанный как функция, и вызов генераторной функции тело не выполняет: он создаёт объект и возвращает управление.Дальше спросят
12
Контекстные менеджеры
with— это гарантия того, что выход из блока будет обработан, чем бы блок ни кончился. Протокол — два метода на типе:__enter__вызывается на входе,__exit__на выходе, иasсвязывает то, что вернул__enter__, а не сам объект.Дальше спросят
13
Исключения и finally
try— попытка,except— обработка ошибки,else— ветка «ошибки не было»,finally— выход в любом случае. И главное проfinally: если он сам передаёт управление наружу —return,break,continue, — летящее исключение исчезает. Справочник формулирует это одним предложением: «сохранённое исключение отбрасывается».Дальше спросят
14
lambda
lambda— та же функция, записанная выражением. Объект получается того же типа, что и уdef, с тем же байт-кодом тела; пишут её там, где функция нужна прямо в месте вызова и отдельное имя ухудшило бы читаемость.Дальше спросят
15
async/await
Вызов корутинной функции (
async defбезyield) не выполняет её тело — он только создаёт объектcoroutine. Тело исполняет тот, кто этот объект ведёт:awaitв текущей задаче,asyncio.runили задача изcreate_task, и задача может начать работу раньше, чем дело дойдёт доawait task.awaitуступает управление другим задачам, только если ожидаемое ещё не готово:awaitзавершившегося Future хода не передаёт. Отсюда забытыйawait:if is_allowed(user)проверяет на истинность объект корутины вместо результата проверки и проходит при любых данных.Дальше спросят
16
*args и **kwargs
Звёздочка в объявлении собирает, звёздочка в вызове раскладывает. Одна собирает лишние позиционные аргументы в кортеж
args, две — лишние именованные в словарьkwargs; в вызове одна раскладывает последовательность в позиционные аргументы, две — словарь в именованные. И оба объекта создаются, даже когда передавать нечего: приходят()и{}, а неNone.Дальше спросят
17
Список против кортежа
Список — изменяемая последовательность, кортеж — неизменяемая, и выбирают между ними по этому признаку, а не по байтам. Кортеж годится в ключи словаря, когда годится каждый его элемент, и говорит читателю «это одна запись фиксированной формы»; список говорит «это сколько угодно однородных штук». Разница в памяти есть, но она не то, ради чего тип меняют.
Дальше спросят
18
Включения
Включение — это цикл, записанный одним выражением, но выполняется оно в собственной области видимости, и граница этой области проходит внутри квадратных скобок, а не по ним. Всё, кроме выражения-источника в самом левом
for, исполняется в отдельной неявно вложенной области — поэтому переменная включения наружу не течёт, а само включение не всегда видит те же имена, что окружающий его код.Дальше спросят
19
Замыкания
Замыкание — это функция, которая сохранила доступ к переменным места, где была создана, и пользуется ими после того, как это место отработало. Сохранён именно доступ к переменной, а не копия её значения: значение читается в момент вызова. Поэтому изменение переменной после создания функции до неё доходит.
Дальше спросят
20
MRO
MRO — порядок, в котором Python ищет атрибут по классам иерархии; он лежит в
Class.__mro__и строится алгоритмом C3. C3 ставит класс раньше его предков и сохраняет порядок баз, поэтому в ромбеD(Left, Right)общий предок уходит в хвост:D, Left, Right, Base, object. Аsuper()— посредник: поиск через него идёт по MRO экземпляра, начиная после класса, где написан метод, и это не обязательно родитель.Дальше спросят
21
Копирование
Присваивание не копирует ничего — это второе имя для того же объекта;
copy.copyсоздаёт новый верхний объект и оставляет вложенные общими;copy.deepcopyдобирается и до вложенных. Разница видна только тогда, когда меняют вложенный объект: после поверхностной копии изменение видно через обе стороны, после глубокой — нет.Дальше спросят
22
is против ==
isспрашивает, один ли это объект; его нельзя переопределить, и его цена не зависит от объекта.==вызывает__eq__, обычный метод, который может вернуть что угодно.isуместен там, где контракт API именно про тождество: у любого объекта-одиночки и у любой метки, заведённой ради такого сравнения, —None,NotImplemented,Ellipsis, членыEnum, собственный часовойobject().Дальше спросят
23
Сборка мусора
В CPython память освобождается прежде всего подсчётом ссылок: объект умирает, как только на него пропадает последняя ссылка, сразу.
delудаляет имя, а не объект. Сборщик мусора дополняет счётчик и нужен для циклов: объекты, ссылающиеся друг на друга, счётчик не освободит никогда, иgcнаходит такие группы без внешних ссылок.Дальше спросят
24
Метаклассы
Метакласс — это тип класса. Класс — экземпляр метакласса ровно так же, как объект — экземпляр класса, и метакласс управляет созданием классов примерно так же, как класс управляет созданием экземпляров. Отсюда и то, что
Class(...)— это вызов__call__у метакласса: класс — его экземпляр.Дальше спросят
25
Аргумент по умолчанию
Выражение после
=вычисляется один раз, при выполненииdef, и получившийся объект достаётся каждому вызову, в котором этот аргумент не передан. Поэтому изменяемое значение по умолчанию накапливает изменения между вызовами. Чинится проверкой наNone; своя метка нужна только там, гдеNone— допустимое значение аргумента.Дальше спросят