1️⃣Декоратор @𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚢 создаёт объект дескриптора, который хранит функцию 𝚗𝚊𝚖𝚎. Этот объект дескриптор помещается в словарь класса 𝚄𝚜𝚎𝚛.𝚍𝚒𝚌𝚝 под именем 𝚗𝚊𝚖𝚎. 2️⃣Выражение 𝚄𝚜𝚎𝚛.𝚗𝚊𝚖𝚎 — это обращение к атрибуту через класс (без создания экземпляра). Python находит дескриптор, но поскольку обращение идёт не через экземпляр, дескриптор просто возвращает сам себя — объект 𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚢. Первый 𝚙𝚛𝚒𝚗𝚝 выводит что‑то вроде <𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚢 𝚘𝚋𝚓𝚎𝚌𝚝 𝚊𝚝 𝟶𝚡...>.
3️⃣Выражение 𝚄𝚜𝚎𝚛() создаёт новый экземпляр класса 𝚄𝚜𝚎𝚛.
4️⃣Обращение .𝚗𝚊𝚖𝚎 на этом экземпляре заставляет дескриптор сработать: он вызывает сохранённую функцию 𝚗𝚊𝚖𝚎(𝚜𝚎𝚕𝚏) и возвращает её результат «𝚒𝚗𝚜𝚝𝚊𝚗𝚌𝚎».
5️⃣Второй 𝚙𝚛𝚒𝚗𝚝 выводит строку «𝚒𝚗𝚜𝚝𝚊𝚗𝚌𝚎».
Когда интерпретатор видит метод с декоратором 𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚢, он примерно делает так: 🔘берёт функцию 𝚏𝚞𝚗𝚌 = 𝚄𝚜𝚎𝚛.𝚗𝚊𝚖𝚎; 🔘создаёт объект 𝚙𝚛𝚘𝚙 = 𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚢(𝚏𝚞𝚗𝚌); 🔘кладёт его в класс: в 𝚄𝚜𝚎𝚛.𝚍𝚒𝚌𝚝 под ключом 𝚗𝚊𝚖𝚎 теперь лежит не функция, а объект 𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚢.
Класс 𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚢 сам по себе — обычный класс, у которого реализованы методы 𝚐𝚎𝚝, 𝚜𝚎𝚝, 𝚍𝚎𝚕𝚎𝚝𝚎. Это и называют дескриптором.
Благодаря такому устройству 🔘можно писать 𝚞𝚜𝚎𝚛.𝚊𝚐𝚎 вместо 𝚞𝚜𝚎𝚛.𝚐𝚎𝚝_𝚊𝚐𝚎(), а под капотом при этом выполняется логика (валидация, преобразования, кэширование и т. п.); 🔘можно сперва сделать обычное поле 𝚞𝚜𝚎𝚛.𝚊𝚐𝚎, а позже заменить его на вычисляемое свойство, не ломая код, который его читает — внешний интерфейс (𝚞𝚜𝚎𝚛.𝚊𝚐𝚎) не меняется.
1️⃣Определены функции 𝚏𝚎𝚝𝚌𝚑 и 𝚖𝚊𝚒𝚗, но они ничего не делают сами по себе, пока не будут запущены через await/цикл событий.
2️⃣Строка 𝚌𝚘𝚛𝚘 = 𝚖𝚊𝚒𝚗() создаёт объект‑корутину 𝚌𝚘𝚛𝚘; тело 𝚖𝚊𝚒𝚗 (и, следовательно, 𝚏𝚎𝚌𝚝𝚌𝚑) при этом не исполняется.
3️⃣Вызов 𝚌𝚘𝚛𝚘.𝚌𝚕𝚘𝚜𝚎() корректно завершает корутину, что предотвращает предупреждение «coroutine was never awaited»; метод 𝚌𝚕𝚘𝚜𝚎 доступен у корутин по спецификации.
4️⃣Поскольку ни 𝚖𝚊𝚒𝚗, ни 𝚏𝚎𝚌𝚑 не выполнялись, строки "fetching" и "done" не печатаются; затем исполняется обычный синхронный 𝚙𝚛𝚒𝚗𝚝("after") и выводит единственную строку.
Как работает async/await Асинхронное программирование позволяет программе не простаивать во время долгих операций (сеть, диск, база данных). Ключевое слово 𝚊𝚜𝚢𝚗𝚌 превращает функцию в корутину — объект, который можно «приостановить» на await и потом продолжить, при этом цикл событий в момент ожидания переключается на другие задачи. Для реального запуска корутины нужен либо 𝚊𝚠𝚊𝚒𝚝 внутри другой async‑функции, либо явный вызов через 𝚊𝚜𝚢𝚗𝚌𝚒𝚘.𝚛𝚞𝚗() или цикл событий.
Метод 𝚜𝚎𝚗𝚍() отправляет значение в генератор, которое становится результатом текущего выражения 𝚢𝚒𝚎𝚕𝚍. Первый 𝚗𝚎𝚡𝚝(𝚐) запускает генератор и выдаёт 1, затем 𝚐.𝚜𝚎𝚗𝚍(5) отправляет 5 в переменную 𝚡, вычисляет 𝚡 * 2 = 10 и выдаёт это значение.
Подробный разбор по шагам
1️⃣Создаётся объект генератора 𝚐 = 𝚐𝚎𝚗(). На этом этапе функция ещё не начала выполняться, генератор находится в состоянии «не запущен».
2️⃣Вызов 𝚗𝚎𝚡𝚝(𝚐) запускает выполнение генератора. Код доходит до строки 𝚡 = 𝚢𝚒𝚎𝚕𝚍 1, выдаёт значение 1 и приостанавливается. Выражение 𝚢𝚒𝚎𝚕𝚍 1 ещё не завершено — переменная 𝚡 пока не получила значение. Функция 𝚙𝚛𝚒𝚗𝚝 выводит первую строку: 1.
3️⃣Вызов 𝚐.𝚜𝚎𝚗𝚍(5) возобновляет выполнение генератора и отправляет значение 5 внутрь генератора. Это значение становится результатом выражения 𝚢𝚒𝚎𝚕𝚍 1, то есть переменная 𝚡 получает значение 5.
4️⃣Генератор продолжает выполнение со следующей строки 𝚢𝚒𝚎𝚕𝚍 𝚡 * 2. Вычисляется 5 * 2 = 10, и это значение выдаётся наружу. Метод 𝚜𝚎𝚗𝚍() возвращает выданное значение 10, которое передаётся в 𝚙𝚛𝚒𝚗𝚝.
5️⃣Функция 𝚙𝚛𝚒𝚗𝚝 выводит вторую строку: 10. Итого на экране появляются две строки: 1 и 10.
Эта задача показывает, что генераторы поддерживают двустороннюю коммуникацию — паттерн, используемый в конвейерах обработки данных и state machines.
Давайте разберём пошагово: 1️⃣𝚡 = [𝟷, 𝟸, 𝟹] — создаётся список. 2️⃣𝚢 = 𝚡 — теперь 𝚢 и 𝚡 указывают на один и тот же объект списка [𝟷, 𝟸, 𝟹]. 3️⃣𝚡, 𝚢[𝟶] = 𝚢, 𝟺 — здесь начинается хитрость множественного присваивания.
Python сначала вычисляет правую часть целиком: создаётся временный кортеж из текущих значений 𝚢 и 𝟺, то есть [𝟷, 𝟸, 𝟹] и 𝟺.
Только после этого начинается присваивание слева направо: 4️⃣𝚡 = [𝟷, 𝟸, 𝟹] — переменная 𝚡 теперь указывает на тот же список, что и раньше (потому что 𝚢 указывал на него). 5️⃣𝚢[𝟶] = 𝟺 — модифицируется первый элемент списка, на который указывает 𝚢.
Но поскольку 𝚡 и 𝚢 указывают на один и тот же список, изменение 𝚢[𝟶] сразу видно и через 𝚡. В итоге список становится [𝟺, 𝟸, 𝟹], и обе переменные выводят его.
Эта задачка показывает важную особенность: порядок вычисления и присваивания в множественном присваивании может приводить к неочевидным результатам, когда левая и правая части «переплетены» через ссылки на изменяемые объекты.
2️⃣Вызов: 𝚜 = ":".𝚓𝚘𝚒𝚗(𝚏()) — здесь берётся встроенный метод 𝚓𝚘𝚒𝚗, разделителем служит строка ":", а аргументом — генератор. Генератор по очереди выдаёт "a", потом "b".
3️⃣Метод join работает так: — «Собери все строки из генератора, вставь между ними разделитель» — Результат — строка "a:b".
4️⃣Вывод: 𝚙𝚛𝚒𝚗𝚝(𝚜) → 'a:b'
Почему это важно 🔘join очень полезен: работает с любым итерируемым объектом: списком, кортежем, даже генератором. 🔘Передавая генератор, можно собирать строки «на лету» без лишней памяти и промежуточных списков. 🔘Если передать нестроковые элементы, будет TypeError, но для генератора, выдающего строки — всё корректно.
Главный подвох: Python использует late binding в замыканиях — переменные захватываются по ссылке, и их значение берётся в момент вызова функции, а не в момент создания.
Разбор по шагам в псевдо‑записи:
1️⃣Создание списка функций: 𝚏𝚞𝚗𝚌𝚜 = 𝚏𝚘𝚛 𝚒 𝚒𝚗 𝚛𝚊𝚗𝚐𝚎(𝟸): ▸ 𝚏𝚞𝚗𝚌𝚜.𝚊𝚙𝚙𝚎𝚗𝚍(𝚕𝚊𝚖𝚋𝚍𝚊 𝚡: 𝚡 * 𝚒) — создаётся три лямбды, каждая ссылается на переменную i из внешнего scope. — переменная i меняется: 0 → 1 → 2.
2️⃣Момент вызова функций: Когда выполняется [f(1) for f in funcs], цикл for i in range(3) уже завершился. Переменная i в глобальном scope теперь равна 2 (последнее значение).
3️⃣Выполнение каждой лямбды: Первый вызов: funcs[0](1) → lambda x: x * i, где i = 2 → 1 * 2 = 2. Второй вызов: funcs[1](1) → та же лямбда, та же ссылка на i, i = 2 → 1 * 2 = 2. Третий вызов: funcs[2](1) → опять i = 2 → 1 * 2 = 2.
4️⃣Результат: [2, 2, 2].
Чтобы каждая лямбда «запомнила» своё значение i, нужно захватить его через default argument:
funcs = [lambda x, i=i: x * i for i in range(3)] print([f(1) for f in funcs]) # [0, 1, 2]
Здесь i=i создаёт локальную переменную внутри лямбды с значением на момент создания, а не ссылкой на внешнюю переменную. Почему это важно 🔘Late binding — частая причина ошибок в циклах с lambda, list comprehension и callback‑функциями. 🔘Правило: если нужно захватить переменную из цикла, всегда используйте default argument для lambda или локальную переменную внутри comprehension. 🔘Это поведение отличается от некоторых других языков, где захват происходит по значению по умолчанию.
Главный подвох: обычно в Python 3 переменные цикла внутри list comprehension локальны и не «утекают» наружу, но walrus operator := специально создаёт или перезаписывает переменную в вмещающей области видимости.
Сравнение с обычным list comprehension > x = 10 > nums = [x for x in range(3)] > print(x, nums) Здесь вывод: 10 [0, 1, 2], потому что переменная x внутри comprehension локальна и не затрагивает внешнюю переменную.
Почему это важно 🔘Walrus operator := создан для присваивания внутри выражений, но его «утечка» в outer scope может быть неожиданной. 🔘В продакшен‑коде лучше использовать := осторожно, чтобы не перезаписать случайно важные переменные. 🔘Для чистого list comprehension без побочных эффектов используйте обычную переменную цикла, а не :=.