Python: задачки и вопросы
前往频道在 Telegram
Вопросы и задачки для подготовки к собеседованиям и прокачки навыков Разместить рекламу: @tproger_sales_bot Правила общения: https://tprg.ru/rules Другие каналы: @tproger_channels Другие наши проекты: https://tprg.ru/media
显示更多7 131
订阅者
-124 小时
+77 天
无数据30 天
帖子存档
Давайте разберём пошагово:
1️⃣𝚡 = [𝟷, 𝟸, 𝟹] — создаётся список.
2️⃣𝚢 = 𝚡 — теперь 𝚢 и 𝚡 указывают на один и тот же объект списка [𝟷, 𝟸, 𝟹].
3️⃣𝚡, 𝚢[𝟶] = 𝚢, 𝟺 — здесь начинается хитрость множественного присваивания.
Python сначала вычисляет правую часть целиком: создаётся временный кортеж из текущих значений 𝚢 и 𝟺, то есть [𝟷, 𝟸, 𝟹] и 𝟺.
Только после этого начинается присваивание слева направо:
4️⃣𝚡 = [𝟷, 𝟸, 𝟹] — переменная 𝚡 теперь указывает на тот же список, что и раньше (потому что 𝚢 указывал на него).
5️⃣𝚢[𝟶] = 𝟺 — модифицируется первый элемент списка, на который указывает 𝚢.
Но поскольку 𝚡 и 𝚢 указывают на один и тот же список, изменение 𝚢[𝟶] сразу видно и через 𝚡. В итоге список становится [𝟺, 𝟸, 𝟹], и обе переменные выводят его.
Эта задачка показывает важную особенность: порядок вычисления и присваивания в множественном присваивании может приводить к неочевидным результатам, когда левая и правая части «переплетены» через ссылки на изменяемые объекты.
В пятницу не стал жестить, большинство справились с задачей.
Разбор по шагам:
1️⃣Определяется функция‑генератор:
𝚍𝚎𝚏 𝚏():
𝚢𝚒𝚎𝚕𝚍 "𝚊"
𝚢𝚒𝚎𝚕𝚍 "𝚋"
2️⃣Вызов:
𝚜 = ":".𝚓𝚘𝚒𝚗(𝚏()) — здесь берётся встроенный метод 𝚓𝚘𝚒𝚗, разделителем служит строка ":", а аргументом — генератор.
Генератор по очереди выдаёт "a", потом "b".
3️⃣Метод join работает так:
— «Собери все строки из генератора, вставь между ними разделитель»
— Результат — строка "a:b".
4️⃣Вывод:
𝚙𝚛𝚒𝚗𝚝(𝚜) → 'a:b'
Почему это важно
🔘join очень полезен: работает с любым итерируемым объектом: списком, кортежем, даже генератором.
🔘Передавая генератор, можно собирать строки «на лету» без лишней памяти и промежуточных списков.
🔘Если передать нестроковые элементы, будет TypeError, но для генератора, выдающего строки — всё корректно.
Главный подвох: Python использует late binding в замыканиях — переменные захватываются по ссылке, и их значение берётся в момент вызова функции, а не в момент создания.
Разбор по шагам в псевдо‑записи:
🔘Создание списка функций:
𝚏𝚞𝚗𝚌𝚜 =
𝚏𝚘𝚛 𝚒 𝚒𝚗 𝚛𝚊𝚗𝚐𝚎(𝟸):
▸ 𝚏𝚞𝚗𝚌𝚜.𝚊𝚙𝚙𝚎𝚗𝚍(𝚕𝚊𝚖𝚋𝚍𝚊 𝚡: 𝚡 * 𝚒)
— создаётся три лямбды, каждая ссылается на переменную i из внешнего scope.
— переменная i меняется: 0 → 1 → 2.
🔘Момент вызова функций:
Когда выполняется [f(1) for f in funcs], цикл for i in range(3) уже завершился.
Переменная i в глобальном scope теперь равна 2 (последнее значение).
🔘Выполнение каждой лямбды:
Первый вызов: 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.
🔘Результат: [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 := специально создаёт или перезаписывает переменную в вмещающей области видимости.
Разбор по шагам в псевдо‑записи:
🔘Инициализация:
𝚡 = 𝟷𝟶 — внешняя переменная равна 10.
🔘List comprehension с walrus:
𝚗𝚞𝚖𝚜 = 𝚡 := 𝚒 𝚏𝚘𝚛 𝚒 𝚒𝚗 𝚛𝚊𝚗𝚐𝚎(𝟸)
— 𝚒 = 𝟶 → 𝚡 := 𝟶 (перезаписывает внешний 𝚡, теперь 𝚡 = 0)
— 𝚒 = 𝟷 → 𝚡 := 𝟷 (перезаписывает внешний 𝚡, теперь 𝚡 = 1)
— 𝚒 = 𝟸 → 𝚡 := 𝟸 (перезаписывает внешний 𝚡, теперь 𝚡 = 2)
Результат: 𝚗𝚞𝚖𝚜 = 𝟶, 𝟷, 𝟸, а внешний 𝚡 = 𝟸.
🔘Вывод:
𝚙𝚛𝚒𝚗𝚝(𝚡, 𝚗𝚞𝚖𝚜) → печатает 2 [0, 1, 2].
Сравнение с обычным 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 без побочных эффектов используйте обычную переменную цикла, а не :=.
Главный подвох здесь в том, что переменная цикла num остаётся доступной после цикла и сохраняет последнее значение из итерируемой последовательности.
Разбор по шагам:
🔘Создание исходных данных:
𝚗𝚞𝚖𝚜 = 𝟷, 𝟸, 𝟹, 𝟺, 𝟻, 𝟼, 𝟽, 𝟾, 𝟿 # список чисел от 1 до 9.
𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗 = [] # пустой список для чётных чисел.
🔘Цикл фильтрации:
𝚏𝚘𝚛 𝚗𝚞𝚖 𝚒𝚗 𝚗𝚞𝚖𝚜: # перебираем каждое число.
▸ 𝚒𝚏 𝚗𝚘𝚝 𝚗𝚞𝚖 % 𝟸: # проверяем чётность (остаток от деления на 2 равен 0).
▸▸ 𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗.𝚊𝚙𝚙𝚎𝚗𝚍(𝚗𝚞𝚖) # добавляем чётные числа: 𝟸, 𝟺, 𝟼, 𝟾.
После цикла:
𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗 = 𝟸, 𝟺, 𝟼, 𝟾 # четыре элемента.
𝚗𝚞𝚖 = 𝟿 # переменная цикла сохраняет последнее значение из 𝚗𝚞𝚖𝚜.
🔘Попытка удаления:
𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗.𝚙𝚘𝚙(𝚗𝚞𝚖) # пытаемся вызвать pop(9).
Метод list.pop(index) принимает индекс, а не значение.
Список имеет длину 4, индексы от 𝟶 до 𝟹, поэтому индекс 𝟹 выходит за границы и вызывает IndexError: pop index out of range.
🔘Почему это неочевидно:
Вообще говоря pop() удаляет последний элемент, если не указан индекс, но здесь указан индекс 𝚗𝚞𝚖 = 𝟿. Переменная 𝚗𝚞𝚖 после цикла не «сбрасывается», а сохраняет последнее значение, что часто приводит к ошибкам.
— Если нужно удалить последний элемент, следует использовать 𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗.𝚙𝚘𝚙() без аргументов.
— Если нужно удалить по значению, следует использовать 𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗.𝚛𝚎𝚖𝚘𝚟𝚎(𝚗𝚞𝚖).
Кратко выводы
Правило: переменные цикла for в Python не локальны внутри цикла, они остаются в той же области видимости, где был цикл. После цикла for num in nums: переменная num доступна и равна последнему элементу из nums. Это часто приводит к ошибкам, если забыть, что num сохранилась, и использовать её в коде после цикла.
Главный подвох здесь в том, что переменная цикла num остаётся доступной после цикла и сохраняет последнее значение из итерируемой последовательности.
Разбор по шагам:
🔘Создание исходных данных:
𝚗𝚞𝚖𝚜 = 𝟷, 𝟸, 𝟹, 𝟺, 𝟻, 𝟼, 𝟽, 𝟾, 𝟿 # список чисел от 1 до 9.
𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗 = [] # пустой список для чётных чисел.
🔘Цикл фильтрации:
𝚏𝚘𝚛 𝚗𝚞𝚖 𝚒𝚗 𝚗𝚞𝚖𝚜: — перебираем каждое число.
▸ 𝚒𝚏 𝚗𝚘𝚝 𝚗𝚞𝚖 % 𝟸: — проверяем чётность (остаток от деления на 2 равен 0).
▸▸ 𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗.𝚊𝚙𝚙𝚎𝚗𝚍(𝚗𝚞𝚖) — добавляем чётные числа: 𝟸, 𝟺, 𝟼, 𝟾.
После цикла:
𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗 =
𝟸
,
𝟺
,
𝟼
,
𝟾
2,4,6,8 — четыре элемента.
𝚗𝚞𝚖 = 𝟿 — переменная цикла сохраняет последнее значение из 𝚗𝚞𝚖𝚜.
Попытка удаления:
𝚗𝚞𝚖𝚜_𝚎𝚟𝚎𝚗.𝚙𝚘𝚙(𝚗𝚞𝚖) — пытаемся вызвать pop(9).
Метод list.pop(index) принимает индекс, а не значение.
Список имеет длину 4, индексы от 𝟶 до 𝟹, поэтому индекс 𝟹 выходит за границы и вызывает IndexError: pop index out of range.
Почему это неочевидно:
Многие думают, что pop() удаляет последний элемент, если не указан индекс, но здесь указан индекс 𝚗𝚞𝚖 = 𝟿.
Переменная 𝚗𝚞𝚖 после цикла не «сбрасывается», а сохраняет последнее значение, что часто приводит к ошибкам.
Если нужно удалить последний элемент, следует использовать 𝚗𝚞𝚖𝚜\_𝚎𝚟𝚎𝚗.𝚙𝚘𝚙() без аргументов.
Если нужно удалить по значению, следует использовать 𝚗𝚞𝚖𝚜\_𝚎𝚟𝚎𝚗.𝚛𝚎𝚖𝚘𝚟𝚎(𝚗𝚞𝚖).
Как про это думать
Правило: переменные цикла for в Python не локальны внутри цикла, они остаются в той же области видимости, где был цикл.
После цикла for num in nums: переменная num доступна и равна последнему элементу из nums.
Это часто приводит к ошибкам, если забыть, что num сохранилась, и использовать её в коде после цикла.
Главная идея: в конструкции try/except/finally блок finally выполняется всегда — и перед выходом из функции, и при исключениях, и даже если в try уже выполнен return. При этом return внутри finally имеет приоритет и заменяет любое ранее вычисленное значение возврата.
Что происходит внутри 𝚏𝚘𝚘():
🔘Заходим в блок 𝚝𝚛𝚢, выполняется 𝚛𝚎𝚝𝚞𝚛𝚗 "𝚝𝚛𝚢" — Python запоминает, что функция должна вернуть "try", но перед реальным выходом обязан выполнить 𝚏𝚒𝚗𝚊𝚕𝚕𝚢.
🔘Заходим в блок 𝚏𝚒𝚗𝚊𝚕𝚕𝚢:
▸ 𝚙𝚛𝚒𝚗𝚝("𝚏𝚒𝚗𝚊𝚕𝚕𝚢") — на экран выводится первая строка finally.
▸ 𝚛𝚎𝚝𝚞𝚛𝚗 "𝚏𝚒𝚗𝚊𝚕𝚕𝚢" — это новое значение возврата, которое перезаписывает ранее подготовленный 𝚛𝚎𝚝𝚞𝚛𝚗 "𝚝𝚛𝚢".
В итоге 𝚏𝚘𝚘() возвращает "finally".
Что печатает 𝚙𝚛𝚒𝚗𝚝:
🔘Внутри 𝚏𝚘𝚘(): печатается finally.
🔘Снаружи: 𝚙𝚛𝚒𝚗𝚝(𝚏𝚘𝚘()) печатает возвращённое значение "finally".
Итоговый вывод в терминале: finally finally. То есть первая строка — побочный эффект из 𝚏𝚒𝚗𝚊𝚕𝚕𝚢, вторая — результат возврата функции.
Почему это важно
🔘𝚏𝚒𝚗𝚊𝚕𝚕𝚢 всегда выполняется: при нормальном завершении, при return, при исключении.
🔘𝚛𝚎𝚝𝚞𝚛𝚗 внутри 𝚏𝚒𝚗𝚊𝚕𝚕𝚢 перетирает результат 𝚝𝚛𝚢/𝚎𝚡𝚌𝚎𝚙𝚝, поэтому так писать опасно: легко скрыть реальные ошибки или неожиданные возвраты.
🔘Рекомендуется не использовать 𝚛𝚎𝚝𝚞𝚛𝚗 в 𝚏𝚒𝚗𝚊𝚕𝚕𝚢; лучше ограничиваться чистым cleanup — закрытием файлов, соединений и т.п.
现已上线!2025 年 Telegram 研究 — 年度关键洞察 
