Python: задачки и вопросы
Развёрнутое пояснение:
1. Создаётся словарь base с ключом retry, значением которого является другой словарь {limit: 3}.
2. copy.copy(base) создаёт новый внешний словарь cfg, но значение по ключу retry — это тот же вложенный словарь, а не его отдельная копия.
3. Через cfg['retry']['limit'] = 5 изменяется значение во вложенном словаре, на который ссылаются и base, и cfg.
4. Поэтому base['retry']['limit'] теперь равно 5, и print выводит 5.
Почему это важно: в реальных проектах часто клонируют конфиг, кэш или настройки. Если использовать поверхностное копирование, вложенные изменяемые структуры остаются общими, и правки в одной копии неожиданно затрагивают другую. Для полной изоляции вложенных объектов нужен copy.deepcopy.
2. copy.copy(base) создаёт новый внешний словарь cfg, но значение по ключу retry — это тот же вложенный словарь, а не его отдельная копия.
3. Через cfg['retry']['limit'] = 5 изменяется значение во вложенном словаре, на который ссылаются и base, и cfg.
4. Поэтому base['retry']['limit'] теперь равно 5, и print выводит 5.
Почему это важно: в реальных проектах часто клонируют конфиг, кэш или настройки. Если использовать поверхностное копирование, вложенные изменяемые структуры остаются общими, и правки в одной копии неожиданно затрагивают другую. Для полной изоляции вложенных объектов нужен copy.deepcopy.
✍2
Python: задачки и вопросы
Развёрнутое пояснение:
1. Импортируется Counter из collections.
2. a получает значения x=2, y=1; b получает x=1, y=2.
3. Операция a - b вычитает счётчики по каждому ключу: x становится 2 - 1 = 1, y становится 1 - 2 = -1.
4. Counter автоматически отбрасывает ключи с неположительным значением, поэтому результат содержит только x=1.
5. Метод elements() возвращает итератор, который выдаёт каждый ключ столько раз, чему равен его положительный счётчик: здесь 'x' один раз.
6. list(...) превращает итератор в список ['x'], который и печатает print.
Почему это важно: при сравнении частот событий, тегов или ошибок за два периода часто используют вычитание Counter. Важно помнить, что отрицательные и нулевые значения исчезают из результата, а elements() не покажет отсутствующие ключи, иначе можно принять просевшие метрики за новые события.
2. a получает значения x=2, y=1; b получает x=1, y=2.
3. Операция a - b вычитает счётчики по каждому ключу: x становится 2 - 1 = 1, y становится 1 - 2 = -1.
4. Counter автоматически отбрасывает ключи с неположительным значением, поэтому результат содержит только x=1.
5. Метод elements() возвращает итератор, который выдаёт каждый ключ столько раз, чему равен его положительный счётчик: здесь 'x' один раз.
6. list(...) превращает итератор в список ['x'], который и печатает print.
Почему это важно: при сравнении частот событий, тегов или ошибок за два периода часто используют вычитание Counter. Важно помнить, что отрицательные и нулевые значения исчезают из результата, а elements() не покажет отсутствующие ключи, иначе можно принять просевшие метрики за новые события.
✍2
Python: задачки и вопросы
Развёрнутое пояснение:
1. Цикл for event_type in ('click', 'scroll') начинает первую итерацию: event_type = 'click'.
2. На этой итерации определяется функция handler(e=event_type). Значение по умолчанию для e вычисляется сейчас и сохраняется как 'click' внутри объекта функции.
3. Переменная event_type в теле handler не является локальной: она свободная и будет искаться в объемлющей области видимости во время вызова.
4. Вторая итерация: event_type = 'scroll'. Определяется новая функция handler, её значение по умолчанию для e — 'scroll'.
5. После цикла event_type остаётся равной 'scroll', так как это последнее значение итерируемого.
6. Вызов handlers[0]() использует значение по умолчанию e='click' и берёт event_type='scroll' из замыкания, возвращая ('click', 'scroll').
7. Вызов handlers[1]() использует e='scroll' и event_type='scroll', возвращая ('scroll', 'scroll').
Почему это важно: разница между ранним и поздним связыванием встречается при создании колбэков, обработчиков событий и фабрик функций в цикле. Ошибка приводит к тому, что все созданные обработчики ссылаются на последнее значение переменной цикла, а использование параметра по умолчанию — распространённый способ зафиксировать нужное значение для каждой функции.
2. На этой итерации определяется функция handler(e=event_type). Значение по умолчанию для e вычисляется сейчас и сохраняется как 'click' внутри объекта функции.
3. Переменная event_type в теле handler не является локальной: она свободная и будет искаться в объемлющей области видимости во время вызова.
4. Вторая итерация: event_type = 'scroll'. Определяется новая функция handler, её значение по умолчанию для e — 'scroll'.
5. После цикла event_type остаётся равной 'scroll', так как это последнее значение итерируемого.
6. Вызов handlers[0]() использует значение по умолчанию e='click' и берёт event_type='scroll' из замыкания, возвращая ('click', 'scroll').
7. Вызов handlers[1]() использует e='scroll' и event_type='scroll', возвращая ('scroll', 'scroll').
Почему это важно: разница между ранним и поздним связыванием встречается при создании колбэков, обработчиков событий и фабрик функций в цикле. Ошибка приводит к тому, что все созданные обработчики ссылаются на последнее значение переменной цикла, а использование параметра по умолчанию — распространённый способ зафиксировать нужное значение для каждой функции.
✍1
Python: задачки и вопросы
Развёрнутое пояснение:
1. Вызов await t(1) запускает первую корутину: на печать идёт 1, затем await sleep(0) приостанавливает выполнение main и отдаёт управление event loop.
2. create_task(t(3)) только планирует фоновую задачу в очередь event loop, но не запускает её немедленно.
3. await t(2) печатает 2 и тоже приостанавливается на sleep(0).
4. Event loop видит три готовых продолжения: t(1), запланированная t(3) и main после t(2). Порядок FIFO, поэтому сначала возобновляется t(1), которая печатает 2 и завершается.
5. Следующей запускается t(3): печатается 3, затем await sleep(0) приостанавливает её.
6. Затем event loop возвращает управление в main после await t(2), где сразу выполняется await x и main ждёт завершения t(3).
7. Event loop возобновляет t(3), печатается 4, задача завершается.
8. В результате на печать идёт последовательность 122334.
Почему это важно: в асинхронном коде побочные эффекты и порядок записи строк не всегда совпадают с порядком выполнения. sleep(0) — не пустая операция, а точка уступки управления, после которой event loop выбирает следующую задачу из очереди. Это влияет на порядок логов, обновление кэша, коммитов состояния и других операций с побочными эффектами, поэтому важно понимать, что create_task планирует задачу, а await передаёт управление.
2. create_task(t(3)) только планирует фоновую задачу в очередь event loop, но не запускает её немедленно.
3. await t(2) печатает 2 и тоже приостанавливается на sleep(0).
4. Event loop видит три готовых продолжения: t(1), запланированная t(3) и main после t(2). Порядок FIFO, поэтому сначала возобновляется t(1), которая печатает 2 и завершается.
5. Следующей запускается t(3): печатается 3, затем await sleep(0) приостанавливает её.
6. Затем event loop возвращает управление в main после await t(2), где сразу выполняется await x и main ждёт завершения t(3).
7. Event loop возобновляет t(3), печатается 4, задача завершается.
8. В результате на печать идёт последовательность 122334.
Почему это важно: в асинхронном коде побочные эффекты и порядок записи строк не всегда совпадают с порядком выполнения. sleep(0) — не пустая операция, а точка уступки управления, после которой event loop выбирает следующую задачу из очереди. Это влияет на порядок логов, обновление кэша, коммитов состояния и других операций с побочными эффектами, поэтому важно понимать, что create_task планирует задачу, а await передаёт управление.
✍2
Python: задачки и вопросы
Развёрнутое пояснение:
1. В строке from decimal import Decimal импортируется тип Decimal.
2. Выражение Decimal('0.1') + Decimal('0.2') выполняет точное десятичное сложение и даёт Decimal('0.3').
3. Сравнение a == 0.3 сравнивает Decimal с float. Python преобразует float 0.3 в Decimal через его двоичное приближение, которое не равно Decimal('0.3'), поэтому результат False.
4. Сравнение a == Decimal('0.3') сравнивает два одинаковых Decimal, поэтому True.
5. float(a) превращает Decimal('0.3') в float 0.3, и сравнение с литералом 0.3 даёт True.
6. Итоговый вывод: False True True.
Почему это важно: В финансовых расчётах и работе с ценами часто используют Decimal, чтобы избежать ошибок float. Но при смешивании Decimal и float в сравнениях, сортировках или условиях результат может оказаться неожиданным: равенство может быть ложным, а max или min может выбрать не тот элемент. Правильно — приводить оба операнда к Decimal или к float явно и не полагаться на неявное преобразование.
2. Выражение Decimal('0.1') + Decimal('0.2') выполняет точное десятичное сложение и даёт Decimal('0.3').
3. Сравнение a == 0.3 сравнивает Decimal с float. Python преобразует float 0.3 в Decimal через его двоичное приближение, которое не равно Decimal('0.3'), поэтому результат False.
4. Сравнение a == Decimal('0.3') сравнивает два одинаковых Decimal, поэтому True.
5. float(a) превращает Decimal('0.3') в float 0.3, и сравнение с литералом 0.3 даёт True.
6. Итоговый вывод: False True True.
Почему это важно: В финансовых расчётах и работе с ценами часто используют Decimal, чтобы избежать ошибок float. Но при смешивании Decimal и float в сравнениях, сортировках или условиях результат может оказаться неожиданным: равенство может быть ложным, а max или min может выбрать не тот элемент. Правильно — приводить оба операнда к Decimal или к float явно и не полагаться на неявное преобразование.
✍3
Python: задачки и вопросы
Развёрнутое пояснение:
1. MRO класса D(B, C): D, B, C, A.
2. D().f() печатает D и через super().f() делегирует следующему классу в MRO — B.
3. B.f печатает B и через super().f() делегирует следующему — C, а не A, потому что super() опирается на линейный MRO объекта, а не на прямого родителя.
4. C.f печатает C и через super().f() делегирует A.
5. A.f печатает A.
6. Итоговый вывод: DBCA.
Почему это важно: понимание MRO и super() нужно при проектировании миксинов и множественного наследования, иначе можно случайно пропустить ветку или вызвать метод дважды.
2. D().f() печатает D и через super().f() делегирует следующему классу в MRO — B.
3. B.f печатает B и через super().f() делегирует следующему — C, а не A, потому что super() опирается на линейный MRO объекта, а не на прямого родителя.
4. C.f печатает C и через super().f() делегирует A.
5. A.f печатает A.
6. Итоговый вывод: DBCA.
Почему это важно: понимание MRO и super() нужно при проектировании миксинов и множественного наследования, иначе можно случайно пропустить ветку или вызвать метод дважды.
✍4
Python: задачки и вопросы
Развёрнутое пояснение:
1. При первом обращении c.ports вызывается метод ports и возвращается список [80].
2. cached_property записывает этот объект в c.__dict__ под именем ports.
3. Вызов c.ports.append(443) изменяет именно тот список, который теперь лежит в кэше.
4. При повторном обращении c.ports cached_property находит ports в __dict__ и возвращает сохранённый объект [80, 443].
5. print выводит [80, 443].
Почему это важно: cached_property удобен для дорогих вычислений, но кэшированный объект остаётся общим для всех последующих обращений. Если метод возвращает изменяемую коллекцию и кто-то её мутирует, изменения застревают в кэше. В конфигах и синглтонах это приводит к «засорению» настроек между тестами или запросами: безопаснее возвращать неизменяемые копии или отделять изменяемое состояние от кэшированного свойства.
2. cached_property записывает этот объект в c.__dict__ под именем ports.
3. Вызов c.ports.append(443) изменяет именно тот список, который теперь лежит в кэше.
4. При повторном обращении c.ports cached_property находит ports в __dict__ и возвращает сохранённый объект [80, 443].
5. print выводит [80, 443].
Почему это важно: cached_property удобен для дорогих вычислений, но кэшированный объект остаётся общим для всех последующих обращений. Если метод возвращает изменяемую коллекцию и кто-то её мутирует, изменения застревают в кэше. В конфигах и синглтонах это приводит к «засорению» настроек между тестами или запросами: безопаснее возвращать неизменяемые копии или отделять изменяемое состояние от кэшированного свойства.
✍3
Python: задачки и вопросы
Развёрнутое пояснение:
1. State — перечисление, член State.OK имеет значение ok, но сам является отдельным объектом Enum.
2. Функция check сравнивает строку ok с объектом State.OK оператором равенства.
3. У Enum операция равенства считает равными только члены одного и того же перечисления, поэтому сравнение со строкой возвращает False.
4. В результате print выводит False.
Почему это важно: при рефакторинге строковых констант в Enum часто забывают, что сравнение со строкой из JSON или БД перестаёт работать. Правильно сравнивать через State(raw) == State.OK или raw == State.OK.value. Это типичная ошибка в валидации API и конфигурации.
2. Функция check сравнивает строку ok с объектом State.OK оператором равенства.
3. У Enum операция равенства считает равными только члены одного и того же перечисления, поэтому сравнение со строкой возвращает False.
4. В результате print выводит False.
Почему это важно: при рефакторинге строковых констант в Enum часто забывают, что сравнение со строкой из JSON или БД перестаёт работать. Правильно сравнивать через State(raw) == State.OK или raw == State.OK.value. Это типичная ошибка в валидации API и конфигурации.
✍3
Python: задачки и вопросы
Развёрнутое пояснение:
1. Создаётся пустой список queue.
2. defaultdict инициализируется фабрикой lambda: queue, которая при каждом обращении по отсутствующему ключу возвращает ссылку на этот же список.
3. tasks['api'] вызывает фабрику, возвращает queue и добавляет в него строку retry.
4. tasks['queue'] снова вызывает фабрику и возвращает тот же список queue, который уже содержит retry.
5. Поэтому print выводит ['retry'].
Почему это важно: такая ошибка возникает, когда разработчик хочет инициализировать значение изменяемым объектом и вместо конструктора list передаёт lambda, замкнутую на заранее созданный объект. В планировщиках задач, группировке событий по категориям или кэшировании результатов по ключам это приводит к тому, что разные ключи делят одно состояние. Правильный способ — передать list или функцию, создающую новый объект при каждом вызове, например defaultdict(list).
2. defaultdict инициализируется фабрикой lambda: queue, которая при каждом обращении по отсутствующему ключу возвращает ссылку на этот же список.
3. tasks['api'] вызывает фабрику, возвращает queue и добавляет в него строку retry.
4. tasks['queue'] снова вызывает фабрику и возвращает тот же список queue, который уже содержит retry.
5. Поэтому print выводит ['retry'].
Почему это важно: такая ошибка возникает, когда разработчик хочет инициализировать значение изменяемым объектом и вместо конструктора list передаёт lambda, замкнутую на заранее созданный объект. В планировщиках задач, группировке событий по категориям или кэшировании результатов по ключам это приводит к тому, что разные ключи делят одно состояние. Правильный способ — передать list или функцию, создающую новый объект при каждом вызове, например defaultdict(list).
✍2