Каверзные вопросы по Python

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


2020-11-19 00:53

разработка по

Мне кажется, в каждом языке программирования есть моменты, которые требуют повышенной концентрации внимания или больше практики для своего понимания. Python в этом плане не исключение, и сегодня я расскажу вам о нескольких каверзных вопросах, с которыми вы можете столкнуться как в повседневной разработке, так и в ходе прохождения собеседования.

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

Обратите внимание, что первичное объяснение не всегда корректно. Чтобы получить правильный ответ, дочитывайте пример до конца.

Первый пример

Очень короткий. Про операторы и порядок их вычислений.

11 > 0 is True

С ходу можно интерпретировать это выражение следующим образом:

  • Приоритет операторов сравнения и is одинаков.
  • 11 > 0 — это True.
  • Упрощаем выражение, получая True is True.
  • Всё выражение в итоге станет True.

На самом же деле это выражение вернёт False.

Вот еще пара похожих выражений.

0 < 0 == 0             # False 1 in range(2) == True  # False

Также обратите внимание, что расстановка скобок изменит результат.

(11 > 0) is True         # True (0 < 0) == 0             # True (1 in range(2)) == True  # True

Как всегда, никакой магии в этих примерах нет. Приведенные выражения — это 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. То есть благодаря скобкам приоритет смещается на выражение в них, оно вычисляется первым, а затем выполняется вторая операция.

Второй пример

В отличие от остальных примеров, незнание следующего факта вряд ли приведёт к ошибке в коде, но знать о таких вещах, мне кажется, как минимум любопытно.

a = 123 b = 123 a == b a is b

Двойное равно проверяет объекты на равенство (и очевидно, что 123 == 123). А оператор is проверяет, что переменные ссылаются на один и тот же объект. a и b — разные объекты, поэтому a is b вернёт False.

На самом деле в Python есть оптимизация, касающаяся небольших int-ов (от -5 до 256 включительно). Эти объекты загружаются в память интерпретатора при его запуске. Получается небольшой кеш. Из-за этого объект получается один, и результат будет True.

Аналогичный пример для числа > 256 сработает ожидаемо:

a = 257 b = 257 a == b  # True a is b  # False

Второй пример. Продолжение

Давайте попробуем копнуть глубже и посмотрим на следующий пример:

def test():     a = 257     b = 257     print(a is b) test()

257 не входит в кеш, и должно отобразиться False.

Отличие от предыдущего примера в том, что тут за счёт функции все инструкции интерпретатору подаются единым блоком. Чтобы понять, что происходит, давайте обратимся к байткоду этой функции:

import dis dis.dis(test)

Мы увидим следующие инструкции:

  2           0 LOAD_CONST               1 (257)               2 STORE_FAST               0 (a)    3           4 LOAD_CONST               1 (257)               6 STORE_FAST               1 (b)    4           8 LOAD_GLOBAL              0 (print)              10 LOAD_FAST                0 (a)              12 LOAD_FAST                1 (b)              14 COMPARE_OP               8 (is)              16 CALL_FUNCTION            1              18 POP_TOP              20 LOAD_CONST               0 (None)              22 RETURN_VALUE

Предпоследняя колонка — это аргументы для операций. Для LOAD_CONST — это индекс в массиве констант. Поскольку для обеих операций LOAD_CONST подаётся один и тот же индекс, в байткоде у нас лишь один объект, отвечающий за число 257.

Получается, что интерпретатор способен на подобные оптимизации: код предварительно анализируется, и некоторые константы переиспользуются (float-ы тоже, но не tuple-ы).
Итак, исходный код выведет True.

Третий пример

Этот вопрос однажды встретился мне на собеседовании. Он про классы и методы.

class C:     a = lambda self: self.b()      def __init__(self):         self.b = lambda self: None  c = C() c.a()

Здесь стоит внимательно пройти цепочку вызовов, отличая методы класса от обычных функций.

Вспоминаем, что вызов метода применительно к экземпляру класса c.method() — это то же самое, что вызов метода применительно к классу с первым аргументом в качестве экземпляра: C.method(c).

Теперь проверим, во что превратились параметры a и b класса C.

type(c.a)  # <class 'method'> type(c.b)  # <class 'function'>

Параметр а превратился в метод класса, такой же, как при определении метода внутри класса через def a(self).

А вот b — это обычная функция, потому что она присваивается атрибуту экземпляра класса, а не определяется (как a) в момент создания класса.

Получается, что при вызове c.a() мы получаем C.a(c). Тут в качестве аргумента self в метод валидно передастся экземпляр класса. Далее внутри a вызывается функция b. Поскольку это обычная функция, то «автоматической» передачи экземпляра в качестве первого аргумента не произойдёт. И получается, что функция b вызовется без аргументов. Но она требует аргумент! Ведь она задана как lambda self: None. Не обращайте внимание, что аргумент называется self. Это сделано для дополнительного запутывания.

Итак, ответ:

Traceback (most recent call last):   File "<input>", line 1, in <module>   File "<input>", line 2, in <lambda> TypeError: <lambda>() missing 1 required positional argument: 'self'

Это происходит потому, что функции b не передан аргумент.

Четвёртый пример

Он про определение переменных в замыкании. Взят из списка хитрых вопросов с toptall:

def create_multipliers():     return [lambda x : i * x for i in range(5)]  for multiplier in create_multipliers():     print(multiplier(2))

Кажется, ничего сложного. create_multipliers вернёт список из 5 функций (назовём их list_lamba_f). Каждая list_lamba_f будет умножать свой аргумент на свой индекс в результирующем массиве.

Получается, что на экране мы увидим:

0 2 4 6 8

Дальнейший разбор предполагает, что вам знакомо замыкание (closure) при использовании вложенных функций (nested functions).

Свои коррективы в наивное объяснение выше вносит позднее связывание. Согласно ему, значение переменной из замыкания (это переменная i) вычисляется в тот момент, когда вызывается внутренняя функция (наши list_lamba_f).

Получается, что значение i в list_lamba_f вычисляется в момент вызова multiplier(2) в пятой строчке. Но в этот момент create_multipliers уже отработала целиком. и значение i — это 4. То есть для всех list_lamba_f значение i равно 4.

Итак, ответ:

8 8 8 8 8

Надеюсь, вам было интересно и понятно. С удовольствием почитаю комментарии с вопросами и задачами, которые кажутся вам полезными для понимания Python и просто занятными. Только не пишите ответ сразу!


Источник: habr.com

Комментарии: