Привет! Весь июль мы будем вести этот канал в партнёрстве с Яндекс Практикумом PRO.
Драматически ничего не меняется: мы продолжим выкладывать задачки и решения — даже более системно и регулярно, чем последние пару месяцев. А ещё расскажем про большой мидловый курс по Python.
Для нас это новый формат соседства двух брендов — надеемся, будет полезно. А пока, всем отличных выходных 🌴
Драматически ничего не меняется: мы продолжим выкладывать задачки и решения — даже более системно и регулярно, чем последние пару месяцев. А ещё расскажем про большой мидловый курс по Python.
Для нас это новый формат соседства двух брендов — надеемся, будет полезно. А пока, всем отличных выходных 🌴
✍10
Python: задачки и вопросы
Развёрнутое пояснение:
В сигнатуре функции f параметры a и b стоят до /, поэтому их можно передать только позиционно. Параметр d стоит после *, поэтому его можно передать только по имени. Параметр c находится между / и *, поэтому допускает оба варианта. Корректен вызов с позиционными a, b и именованными c, d.
✍4
По шагам:
1. Когда Python встречает class A(metaclass=Meta), он не сразу выполняет тело класса. Сначала он спрашивает у метакласса: «в каком словаре хранить имена этого класса?» — и вызывает Meta.__prepare__.
2. Обычно __prepare__ возвращает пустой словарь, но здесь он возвращает {'x': 10}. То есть тело класса начинает выполняться в пространстве имён, где имя x уже существует и равно 10.
3. Выполняется строка result = x + 5. Python ищет x в текущем пространстве имён, находит 10 и записывает result = 15.
4. Следующей выполняется строка x = 1 — она перезаписывает ключ x в том же словаре. Но result уже вычислен, задним числом он не пересчитывается.
5. В итоге у класса A два атрибута: result = 15 и x = 1. Поэтому print(A.result) выводит 15.
Почему это важно
__prepare__ — редкий хук, но он показывает главное: тело класса — это обычный код, выполняющийся в словаре, которым можно управлять. Именно на этой механике работают Django-модели: вы пишете class Movie(models.Model) с обычными атрибутами, а метакласс превращает их в поля таблицы, валидацию и запросы к базе. Понимание таких вещей — разница между «использую фреймворк» и «понимаю, что он делает». Если хотите прокачаться именно во втором, посмотритекурс Middle Python от Практикума PRO: за 6 месяцев вы глубоко разберёте Django и его ORM, асинхронность на FastAPI, микросервисную архитектуру и соберёте бэкенд из 10+ сервисов.
Реклама. Рекламодатель: АНО ДПО "Образовательные технологии Яндекса" ИНН 7704282033, erid: 2W5zFJM1sK7
1. Когда Python встречает class A(metaclass=Meta), он не сразу выполняет тело класса. Сначала он спрашивает у метакласса: «в каком словаре хранить имена этого класса?» — и вызывает Meta.__prepare__.
2. Обычно __prepare__ возвращает пустой словарь, но здесь он возвращает {'x': 10}. То есть тело класса начинает выполняться в пространстве имён, где имя x уже существует и равно 10.
3. Выполняется строка result = x + 5. Python ищет x в текущем пространстве имён, находит 10 и записывает result = 15.
4. Следующей выполняется строка x = 1 — она перезаписывает ключ x в том же словаре. Но result уже вычислен, задним числом он не пересчитывается.
5. В итоге у класса A два атрибута: result = 15 и x = 1. Поэтому print(A.result) выводит 15.
Почему это важно
__prepare__ — редкий хук, но он показывает главное: тело класса — это обычный код, выполняющийся в словаре, которым можно управлять. Именно на этой механике работают Django-модели: вы пишете class Movie(models.Model) с обычными атрибутами, а метакласс превращает их в поля таблицы, валидацию и запросы к базе. Понимание таких вещей — разница между «использую фреймворк» и «понимаю, что он делает». Если хотите прокачаться именно во втором, посмотрите
✍4
Python: задачки и вопросы
Развёрнутое пояснение:
1. Первый вызов `load_users('a')` не находит ключ в кэше, выполняет `return []`, сохраняет созданный список в кэше и сразу добавляет в него `'u1'` через `.append`. Теперь в кэше лежит список `['u1']`.
2. Второй вызов `load_users('a')` попадает в кэш и возвращает тот же самый объект-список, а не новый. `.append('u2')` изменяет этот же объект, и он становится `['u1', 'u2']`.
3. Вызов `print(load_users('a'))` снова получает тот же объект из кэша и печатает `['u1', 'u2']`.
Почему это важно: кэширование изменяемых объектов — распространённая ловушка при оптимизации репозиториев, загрузчиков конфигурации и API-клиентов. Если возвращаемый объект мутируется, кэш превращается в разделяемое изменяемое состояние, и разные вызовы начинают влиять друг на друга, что приводит к трудноуловимым багам в продакшене.
2. Второй вызов `load_users('a')` попадает в кэш и возвращает тот же самый объект-список, а не новый. `.append('u2')` изменяет этот же объект, и он становится `['u1', 'u2']`.
3. Вызов `print(load_users('a'))` снова получает тот же объект из кэша и печатает `['u1', 'u2']`.
Почему это важно: кэширование изменяемых объектов — распространённая ловушка при оптимизации репозиториев, загрузчиков конфигурации и API-клиентов. Если возвращаемый объект мутируется, кэш превращается в разделяемое изменяемое состояние, и разные вызовы начинают влиять друг на друга, что приводит к трудноуловимым багам в продакшене.
✍2
Python: задачки и вопросы
Развёрнутое пояснение:
1. Определяется класс Key с методом __eq__, который сравнивает атрибут val.
2. В Python класс, определяющий __eq__, но не определяющий __hash__, автоматически получает __hash__ = None.
3. При выполнении строки cache = {Key(1): 'hit'} Python пытается вычислить хеш ключа, чтобы разместить его в словаре.
4. Поскольку Key.__hash__ равен None, объект считается нехешируемым, и создание словаря падает с TypeError: unhashable type: 'Key'.
5. Вызов cache.get(Key(1)) не выполняется, потому что программа прерывается раньше.
Почему это важно: кастомные ключи для кэшей, конфигураций или агрегаций часто требуют своего сравнения. Если забыть реализовать __hash__, весь механизм ломается ещё на этапе создания dict или set. В dataclasses это решается через frozen=True для автоматического __hash__, а в собственных классах — явным определением __hash__.
2. В Python класс, определяющий __eq__, но не определяющий __hash__, автоматически получает __hash__ = None.
3. При выполнении строки cache = {Key(1): 'hit'} Python пытается вычислить хеш ключа, чтобы разместить его в словаре.
4. Поскольку Key.__hash__ равен None, объект считается нехешируемым, и создание словаря падает с TypeError: unhashable type: 'Key'.
5. Вызов cache.get(Key(1)) не выполняется, потому что программа прерывается раньше.
Почему это важно: кастомные ключи для кэшей, конфигураций или агрегаций часто требуют своего сравнения. Если забыть реализовать __hash__, весь механизм ломается ещё на этапе создания dict или set. В dataclasses это решается через frozen=True для автоматического __hash__, а в собственных классах — явным определением __hash__.
✍2
Развёрнутое пояснение:
1. fix(fn) вызывает fn один раз и сохраняет результат в v.
2. fix возвращает lambda, которая всегда отдаёт это же значение v.
3. buf указывает на lambda, а значит все вызовы buf() возвращают один и тот же список.
4. test_a получает этот список, добавляет в него 'a' и печатает ['a'].
5. test_b получает уже изменённый тот же список, добавляет 'b' и печатает ['a', 'b'].
Почему это важно
В pytest фикстура со scope='module' или scope='session' создаёт один объект на все тесты модуля или сессии. Если фикстура отдаёт изменяемый объект — список, словарь, множество, — мутации в одном тесте протекают в другие. На проде это выглядит как flaky-тесты: локально всё зелёное, в CI падает из-за другого порядка, или тесты проходят только по отдельности. Это ложно-зелёный сигнал, который подрывает доверие к регрессии. Причина обычно в неверном scope фикстуры. Правильный подход: scope='function' для изолированных данных, а общие ресурсы делать неизменяемыми или копировать перед использованием.
💻 На практике такие кейсы разбирают в курсе Практикума PRO «Автоматизатор тестирования на Python» : поймёте фикстуры, их scope, изоляцию состояния между тестами и уровни покрытия.
Реклама. Рекламодатель: АНО ДПО "Образовательные технологии Яндекса" ИНН 7704282033, erid: 2W5zFGUXzho
2. fix возвращает lambda, которая всегда отдаёт это же значение v.
3. buf указывает на lambda, а значит все вызовы buf() возвращают один и тот же список.
4. test_a получает этот список, добавляет в него 'a' и печатает ['a'].
5. test_b получает уже изменённый тот же список, добавляет 'b' и печатает ['a', 'b'].
Почему это важно
В pytest фикстура со scope='module' или scope='session' создаёт один объект на все тесты модуля или сессии. Если фикстура отдаёт изменяемый объект — список, словарь, множество, — мутации в одном тесте протекают в другие. На проде это выглядит как flaky-тесты: локально всё зелёное, в CI падает из-за другого порядка, или тесты проходят только по отдельности. Это ложно-зелёный сигнал, который подрывает доверие к регрессии. Причина обычно в неверном scope фикстуры. Правильный подход: scope='function' для изолированных данных, а общие ресурсы делать неизменяемыми или копировать перед использованием.
Please open Telegram to view this post
VIEW IN TELEGRAM
✍3
Python: задачки и вопросы
Развёрнутое пояснение:
1. Создаётся словарь d с ключами 'a' и 'b'.
2. Запускается цикл for key in d. Python создаёт итератор словаря, который фиксирует его текущий размер.
3. На первой итерации key равно 'a'. Выполняется d['a_copy'] = 1, и размер словаря увеличивается.
4. Когда цикл пытается перейти к следующей итерации, итератор обнаруживает изменение размера и выбрасывает RuntimeError: dictionary changed size during iteration.
5. Программа завершается с этой ошибкой, print(d) не выполняется.
Почему это важно: при обработке конфигов, метрик или логов часто хочется на ходу добавлять производные ключи в тот же словарь, но это приводит к падению. Правильный путь — собирать изменения в отдельную структуру, а затем обновлять оригинал.
2. Запускается цикл for key in d. Python создаёт итератор словаря, который фиксирует его текущий размер.
3. На первой итерации key равно 'a'. Выполняется d['a_copy'] = 1, и размер словаря увеличивается.
4. Когда цикл пытается перейти к следующей итерации, итератор обнаруживает изменение размера и выбрасывает RuntimeError: dictionary changed size during iteration.
5. Программа завершается с этой ошибкой, print(d) не выполняется.
Почему это важно: при обработке конфигов, метрик или логов часто хочется на ходу добавлять производные ключи в тот же словарь, но это приводит к падению. Правильный путь — собирать изменения в отдельную структуру, а затем обновлять оригинал.
✍5
Python: задачки и вопросы
Развёрнутое пояснение:
1. Создаём defaults = {'theme': 'light'}.
2. Создаём пустой user = {}.
3. ChainMap(user, defaults) формирует цепочку поиска: сначала user, затем defaults.
4. Чтение config['theme'] не находит ключ в user и возвращает 'light' из defaults.
5. Запись config['theme'] = 'dark' всегда изменяет первый словарь цепочки, то есть user.
6. В user появляется {'theme': 'dark'}, а defaults остаётся {'theme': 'light'}.
7. print(defaults['theme']) выводит 'light'.
Почему это важно: ChainMap удобен для многоуровневых настроек — базовые значения, пользовательские, аргументы командной строки. Важно помнить, что присваивание пишет только в первый словарь, иначе можно случайно изменить общие дефолты или, наоборот, удивиться, почему изменение не дошло до последующих читателей конфига.
2. Создаём пустой user = {}.
3. ChainMap(user, defaults) формирует цепочку поиска: сначала user, затем defaults.
4. Чтение config['theme'] не находит ключ в user и возвращает 'light' из defaults.
5. Запись config['theme'] = 'dark' всегда изменяет первый словарь цепочки, то есть user.
6. В user появляется {'theme': 'dark'}, а defaults остаётся {'theme': 'light'}.
7. print(defaults['theme']) выводит 'light'.
Почему это важно: ChainMap удобен для многоуровневых настроек — базовые значения, пользовательские, аргументы командной строки. Важно помнить, что присваивание пишет только в первый словарь, иначе можно случайно изменить общие дефолты или, наоборот, удивиться, почему изменение не дошло до последующих читателей конфига.
✍2
Развёрнутое пояснение:
1. grid = [[0] * 3] * 3 создаёт один внутренний список [0, 0, 0] и трижды копирует ссылку на него, так что все три строки указывают на один объект.
2. grid[0][0] = 1 через первую ссылку изменяет этот общий список, поэтому у любой строки значение в позиции 0 становится равно 1.
3. print(grid) печатает один и тот же изменённый список три раза: [[1, 0, 0], [1, 0, 0], [1, 0, 0]].
Почему это важно
Такая ловушка встречается при инициализации DP-таблиц, матриц смежности или игровых полей. Если написать [[0] * n] * m, любая запись в ячейку размножается по всем строкам, и алгоритм начинает работать с испорченными данными. Правильный способ — [[0] * n for _ in range(m)]: здесь каждая строка создаётся отдельным выражением и живёт своим объектом.
💻 Это различие между копией ссылки и копией объекта — базовая часть работы с массивами и алгоритмами на них. Такие паттерны, сложность операций и подготовку к алгоритмическим собеседованиям разбирают в Практикуме PRO на курсе Алгоритмы и структуры данных : 4 месяца, 100+ задач и пробное интервью.
Реклама. Рекламодатель: АНО ДПО "Образовательные технологии Яндекса" ИНН 7704282033, erid: 2W5zFK6uaWV
1. grid = [[0] * 3] * 3 создаёт один внутренний список [0, 0, 0] и трижды копирует ссылку на него, так что все три строки указывают на один объект.
2. grid[0][0] = 1 через первую ссылку изменяет этот общий список, поэтому у любой строки значение в позиции 0 становится равно 1.
3. print(grid) печатает один и тот же изменённый список три раза: [[1, 0, 0], [1, 0, 0], [1, 0, 0]].
Почему это важно
Такая ловушка встречается при инициализации DP-таблиц, матриц смежности или игровых полей. Если написать [[0] * n] * m, любая запись в ячейку размножается по всем строкам, и алгоритм начинает работать с испорченными данными. Правильный способ — [[0] * n for _ in range(m)]: здесь каждая строка создаётся отдельным выражением и живёт своим объектом.
Реклама. Рекламодатель: АНО ДПО "Образовательные технологии Яндекса" ИНН 7704282033, erid: 2W5zFK6uaWV
Please open Telegram to view this post
VIEW IN TELEGRAM
✍2
Python: задачки и вопросы
Развёрнутое пояснение:
1. Создаётся u с id=1; его __hash__ возвращает 1.
2. Словарь d помещает пару u: 'a' в корзину для хэша 1.
3. u.id меняется на 2; сам объект u всё ещё лежит в корзине 1, потому что словарь не пересчитывает хэш.
4. d.get(User(2)) вычисляет хэш нового объекта — 2 — и ищет в корзине 2.
5. В корзине 2 ничего нет, поэтому возвращается None.
Почему это важно: ключи dict и элементы set должны быть неизменяемыми относительно величины, участвующей в __hash__. Если объект-ключ мутирует, он остаётся в старой корзине, и поиск по новому хэшу его не найдёт. Это приводит к утечкам в кэшах, потере данных в индексах и нестабильным тестам при использовании изменяемых сущностей в качестве ключей.
2. Словарь d помещает пару u: 'a' в корзину для хэша 1.
3.
4. d.get(User(2)) вычисляет хэш нового объекта — 2 — и ищет в корзине 2.
5. В корзине 2 ничего нет, поэтому возвращается None.
Почему это важно: ключи dict и элементы set должны быть неизменяемыми относительно величины, участвующей в __hash__. Если объект-ключ мутирует, он остаётся в старой корзине, и поиск по новому хэшу его не найдёт. Это приводит к утечкам в кэшах, потере данных в индексах и нестабильным тестам при использовании изменяемых сущностей в качестве ключей.
✍2
Python: задачки и вопросы
Развёрнутое пояснение:
1. Определяется класс Field с методами __set__ и __get__, что делает его дескриптором данных.
2. Класс Record получает атрибут класса value, равный экземпляру Field.
3. Создаётся r = Record().
4. Присваивание r.value = 3 вызывает дескрипторный метод __set__, который записывает в r._val значение 3 * 10, то есть 30.
5. Прямая запись r.__dict__["value"] = 9 помещает 9 в словарь экземпляра, но не вызывает дескриптор и не заменяет атрибут класса.
6. При чтении r.value правило поиска атрибутов в Python сначала проверяет дескриптор данных в классе; он находит value = Field() и вызывает его __get__, возвращающий r._val, равное 30.
7. Значение 9 из __dict__ остаётся недоступным через обычный доступ r.value, пока дескриптор с __set__ существует.
Почему это важно: В ORM, валидаторах и property с setter часто скрывают хранение значения за дескрипторами. Ошибка возникает, когда разработчик пишет obj.__dict__["field"] =... или сериализатор заполняет словарь напрямую, ожидая, что это эквивалентно obj.field =.... В действительности дескриптор данных переопределяет чтение, поэтому тесты могут показать одно значение, а обычный доступ — другое. Понимание приоритета дескрипторов помогает избежать рассинхронизации между внутренним хранилищем и публичным интерфейсом.
2. Класс Record получает атрибут класса value, равный экземпляру Field.
3. Создаётся r = Record().
4. Присваивание r.value = 3 вызывает дескрипторный метод __set__, который записывает в r._val значение 3 * 10, то есть 30.
5. Прямая запись r.__dict__["value"] = 9 помещает 9 в словарь экземпляра, но не вызывает дескриптор и не заменяет атрибут класса.
6. При чтении r.value правило поиска атрибутов в Python сначала проверяет дескриптор данных в классе; он находит value = Field() и вызывает его __get__, возвращающий r._val, равное 30.
7. Значение 9 из __dict__ остаётся недоступным через обычный доступ r.value, пока дескриптор с __set__ существует.
Почему это важно: В ORM, валидаторах и property с setter часто скрывают хранение значения за дескрипторами. Ошибка возникает, когда разработчик пишет obj.__dict__["field"] =... или сериализатор заполняет словарь напрямую, ожидая, что это эквивалентно obj.field =.... В действительности дескриптор данных переопределяет чтение, поэтому тесты могут показать одно значение, а обычный доступ — другое. Понимание приоритета дескрипторов помогает избежать рассинхронизации между внутренним хранилищем и публичным интерфейсом.
✍2