Мне кажется, в каждом языке программирования есть моменты, которые требуют повышенной концентрации внимания или больше практики для своего понимания. Python в этом плане не исключение, и сегодня я расскажу вам о нескольких каверзных вопросах, с которыми вы можете столкнуться как в повседневной разработке, так и в ходе прохождения собеседования. Каждый пример будет построен так, чтобы сначала можно было подумать самому, потом получить подсказку, почитать объяснение, и только в конце увидеть ответ. Обратите внимание, что первичное объяснение не всегда корректно. Чтобы получить правильный ответ, дочитывайте пример до конца. Очень короткий. Про операторы и порядок их вычислений. С ходу можно интерпретировать это выражение следующим образом: На самом же деле это выражение вернёт False. Вот еще пара похожих выражений. Также обратите внимание, что расстановка скобок изменит результат. Как всегда, никакой магии в этих примерах нет. Приведенные выражения — это chained comparisons, которые следует читать так: a op1 b op2 c ... y opN z эквивалентно a op1 b and b op2 c and ... y opN z. Итак, ответ: исходное выражение эквивалентно (11 > 0) and (0 is True), что, очевидно, является ложью. Остался вопрос про скобки? Расстановка скобок превращает выражение в обычное, не chained comparisons. То есть благодаря скобкам приоритет смещается на выражение в них, оно вычисляется первым, а затем выполняется вторая операция. В отличие от остальных примеров, незнание следующего факта вряд ли приведёт к ошибке в коде, но знать о таких вещах, мне кажется, как минимум любопытно. Двойное равно проверяет объекты на равенство (и очевидно, что 123 == 123). А оператор is проверяет, что переменные ссылаются на один и тот же объект. a и b — разные объекты, поэтому a is b вернёт False. На самом деле в Python есть оптимизация, касающаяся небольших int-ов (от -5 до 256 включительно). Эти объекты загружаются в память интерпретатора при его запуске. Получается небольшой кеш. Из-за этого объект получается один, и результат будет True. Аналогичный пример для числа > 256 сработает ожидаемо: Давайте попробуем копнуть глубже и посмотрим на следующий пример: 257 не входит в кеш, и должно отобразиться False. Отличие от предыдущего примера в том, что тут за счёт функции все инструкции интерпретатору подаются единым блоком. Чтобы понять, что происходит, давайте обратимся к байткоду этой функции: Мы увидим следующие инструкции: Предпоследняя колонка — это аргументы для операций. Для LOAD_CONST — это индекс в массиве констант. Поскольку для обеих операций LOAD_CONST подаётся один и тот же индекс, в байткоде у нас лишь один объект, отвечающий за число 257. Получается, что интерпретатор способен на подобные оптимизации: код предварительно анализируется, и некоторые константы переиспользуются (float-ы тоже, но не tuple-ы).
Итак, исходный код выведет True. Этот вопрос однажды встретился мне на собеседовании. Он про классы и методы. Здесь стоит внимательно пройти цепочку вызовов, отличая методы класса от обычных функций. Вспоминаем, что вызов метода применительно к экземпляру класса c.method() — это то же самое, что вызов метода применительно к классу с первым аргументом в качестве экземпляра: C.method(c). Теперь проверим, во что превратились параметры a и b класса C. Параметр а превратился в метод класса, такой же, как при определении метода внутри класса через def a(self). А вот b — это обычная функция, потому что она присваивается атрибуту экземпляра класса, а не определяется (как a) в момент создания класса. Получается, что при вызове c.a() мы получаем C.a(c). Тут в качестве аргумента self в метод валидно передастся экземпляр класса. Далее внутри a вызывается функция b. Поскольку это обычная функция, то «автоматической» передачи экземпляра в качестве первого аргумента не произойдёт. И получается, что функция b вызовется без аргументов. Но она требует аргумент! Ведь она задана как lambda self: None. Не обращайте внимание, что аргумент называется self. Это сделано для дополнительного запутывания. Итак, ответ: Это происходит потому, что функции b не передан аргумент. Он про определение переменных в замыкании. Взят из списка хитрых вопросов с toptall: Кажется, ничего сложного. create_multipliers вернёт список из 5 функций (назовём их list_lamba_f). Каждая list_lamba_f будет умножать свой аргумент на свой индекс в результирующем массиве. Получается, что на экране мы увидим: Дальнейший разбор предполагает, что вам знакомо замыкание (closure) при использовании вложенных функций (nested functions). Свои коррективы в наивное объяснение выше вносит позднее связывание. Согласно ему, значение переменной из замыкания (это переменная i) вычисляется в тот момент, когда вызывается внутренняя функция (наши list_lamba_f). Получается, что значение i в list_lamba_f вычисляется в момент вызова multiplier(2) в пятой строчке. Но в этот момент create_multipliers уже отработала целиком. и значение i — это 4. То есть для всех list_lamba_f значение i равно 4. Итак, ответ: Надеюсь, вам было интересно и понятно. С удовольствием почитаю комментарии с вопросами и задачами, которые кажутся вам полезными для понимания Python и просто занятными. Только не пишите ответ сразу!