«Что выведет код?»: каверзные задачи
Как решать такие задачи
На собеседовании такие задачи проверяют, понимаете ли вы модель исполнения Python, а не помните ли трюки. Рассуждайте вслух: «список по умолчанию вычисляется при определении функции, значит…». Если не уверены — скажите, в чём сомнение и как бы вы проверили.
Каждая задача ниже: код → ваш ответ (вслух или на бумаге) → раскройте разбор и запустите. Первые восемь есть и в заданиях с проверкой внизу страницы.
Функции и замыкания
1. Изменяемый аргумент по умолчанию.
def f(x, acc=[]):
acc.append(x)
return acc
print(f(1), f(2), f(3, []), f(4))
Разбор
Значение по умолчанию вычисляется один раз — при выполнении def. Все вызовы без acc работают с одним списком. Обратите внимание: print получает ссылки на объекты, поэтому первый, второй и четвёртый аргумент печатаются в финальном состоянии.
def f(x, acc=[]):
acc.append(x)
return acc
print(f(1), f(2), f(3, []), f(4))
print(f.__defaults__)
2. Позднее связывание.
fs = [lambda: i * 10 for i in range(3)]
print([f() for f in fs])
Разбор
Замыкание хранит ссылку на переменную, а не на значение. Все три лямбды видят одну i, которая после цикла равна 2. Исправления: аргумент по умолчанию (lambda i=i: ...) или functools.partial.
fs = [lambda: i * 10 for i in range(3)]
print([f() for f in fs])
fs = [lambda i=i: i * 10 for i in range(3)]
print([f() for f in fs])
3. Присваивание делает переменную локальной.
x = 10
def show():
print(x)
x = 20
show()
Разбор
Компилятор видит присваивание x = 20 и считает x локальной во всей функции. На момент print(x) локальная x ещё не связана → UnboundLocalError.
x = 10
def show():
print(x)
x = 20
show()
4. finally и return.
def g():
try:
return "try"
finally:
print("finally!")
def h():
try:
1 / 0
finally:
return "подавлено"
print(g(), h())
Разбор
finally выполняется перед выходом из функции, поэтому finally! печатается раньше результата. return внутри finally отменяет исключение — ZeroDivisionError молча исчезает. Никогда так не пишите.
def g():
try:
return "try"
finally:
print("finally!")
def h():
try:
1 / 0
finally:
return "подавлено"
print(g(), h())
Списки и коллекции
5. Умножение вложенных списков.
board = [[0] * 2] * 2
board[0][0] = 5
print(board)
Разбор
board = [[0] * 2] * 2
print(board[0] is board[1])
board[0][0] = 5
print(board)
board = [[0] * 2 for _ in range(2)]
board[0][0] = 5
print(board)
[0] * 2 безопасно: числа неизменяемы. Внешнее * 2 копирует ссылку на один список.
6. Удаление во время обхода.
nums = [1, 2, 2, 3, 4]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums)
Разбор
Итератор списка идёт по индексам. После удаления элементы сдвигаются влево, и следующий элемент пропускается. Обходите копию или стройте новый список.
nums = [1, 2, 2, 3, 4]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums)
nums = [1, 2, 2, 3, 4]
print([n for n in nums if n % 2])
d = {"a": 1, "b": 2}
try:
for k in d:
d["c"] = 3
except RuntimeError as e:
print(e)
7. += для кортежа со списком.
t = (1, [2])
try:
t[1] += [3]
except TypeError as e:
print(e)
print(t)
Разбор
t[1] += [3] раскладывается на tmp = t[1]; tmp = tmp.__iadd__([3]); t[1] = tmp. __iadd__ списка меняет его на месте, а потом присваивание в кортеж падает. Итог: и ошибка, и изменение.
t = (1, [2])
try:
t[1] += [3]
except TypeError as e:
print(e)
print(t)
8. += против = для списков.
a = [1, 2]
b = a
a += [3]
c = [1, 2]
d = c
c = c + [3]
print(b, d)
Разбор
a += [3] меняет список на месте (b — тот же объект). c = c + [3] создаёт новый список и перепривязывает имя c; d смотрит на старый.
a = [1, 2]; b = a; a += [3]
c = [1, 2]; d = c; c = c + [3]
print(b, d, a is b, c is d)
9. Ключи словаря 1, 1.0 и True.
d = {1: "int", 1.0: "float", True: "bool"}
print(d)
Разбор
1 == 1.0 == True и их хеши равны, поэтому это один ключ. Ключ остаётся первым (1), значение — последним.
d = {1: "int", 1.0: "float", True: "bool"}
print(d, hash(1) == hash(1.0) == hash(True))
print({0, False, 0.0, ""})
Сравнения и логика
10. Цепочки сравнений.
print(1 < 2 < 3, 1 < 3 > 2, (1 < 2) < 1, 3 > 2 == 2)
Разбор
print(1 < 2 < 3, 1 < 3 > 2, (1 < 2) < 1, 3 > 2 == 2)
print(False == False in [False]) # (False == False) and (False in [False])
a op1 b op2 c — это a op1 b and b op2 c, причём b вычисляется один раз. Так же цепляются == и in, отсюда второй сюрприз.
11. and и or возвращают операнды.
print(0 or [] or "x" or 5, 3 and 0 and 7, [] and 1/0, None or {} or 0)
Разбор
Ленивое вычисление: or останавливается на первом истинном, and — на первом ложном. [] and 1/0 не делит на ноль: правая часть не вычисляется.
print(0 or [] or "x" or 5, 3 and 0 and 7, [] and 1/0, None or {} or 0)
name = "" or "Гость"
print(name)
12. is и числа.
a = 256; b = 256
c = 257; d = 257
print(a is b, int("257") is int("257"))
Разбор
Числа от -5 до 256 закешированы. Для больших результат is зависит от того, как и где созданы объекты (константы в одном блоке кода компилятор может объединить). Вывод: числа и строки сравнивают только через ==.
a = 256; b = 256
print(a is b, int("257") is int("257"), int("256") is int("256"))
13. Float.
print(0.1 + 0.2 == 0.3, round(2.5), round(3.5), round(2.675, 2))
Разбор
from decimal import Decimal
import math
print(0.1 + 0.2, 0.1 + 0.2 == 0.3, math.isclose(0.1 + 0.2, 0.3))
print(round(2.5), round(3.5), round(2.675, 2)) # к чётному; 2.675 на деле 2.67499999...
print(Decimal("0.1") + Decimal("0.2"), Decimal(2.675))
Классы
14. Атрибут класса против атрибута экземпляра.
class Cart:
items = []
def add(self, x):
self.items.append(x)
a, b = Cart(), Cart()
a.add("кофе")
print(b.items)
Разбор
items — атрибут класса, общий для всех экземпляров. self.items.append находит его через класс и меняет. Изменяемое состояние экземпляра создают в __init__. А вот присваивание a.count = 1 создаст атрибут экземпляра, «затеняющий» классовый.
class Cart:
items = []
count = 0
def add(self, x):
self.items.append(x)
self.count += 1 # читает Cart.count, записывает в экземпляр
a, b = Cart(), Cart()
a.add("кофе")
print(b.items, a.count, b.count, Cart.count, a.__dict__)
15. Искажение имён.
class A:
def __init__(self):
self.__secret = 1
class B(A):
def reveal(self):
return self.__secret
print(B().reveal())
Разбор
self.__secret внутри класса A превращается в self._A__secret, а внутри B — в self._B__secret. Атрибута _B__secret нет → AttributeError.
class A:
def __init__(self):
self.__secret = 1
class B(A):
def reveal(self):
try:
return self.__secret
except AttributeError as e:
return f"AttributeError: {e}"
print(B().reveal(), B().__dict__)
16. Порядок вызова super() при множественном наследовании.
class A:
def hi(self): return "A"
class B(A):
def hi(self): return "B" + super().hi()
class C(A):
def hi(self): return "C" + super().hi()
class D(B, C):
def hi(self): return "D" + super().hi()
print(D().hi())
Разбор
MRO класса D: D → B → C → A. super() в B вызывает следующий класс в MRO объекта — это C, а не A.
class A:
def hi(self): return "A"
class B(A):
def hi(self): return "B" + super().hi()
class C(A):
def hi(self): return "C" + super().hi()
class D(B, C):
def hi(self): return "D" + super().hi()
print(D().hi(), [k.__name__ for k in D.mro()])
Итераторы и генераторы
17. Одноразовый итератор.
nums = map(int, ["1", "2", "3"])
print(sum(nums), sum(nums), 2 in nums)
Разбор
map, filter, zip, генераторы и файлы — итераторы, они исчерпываются за один проход. 2 in nums тоже потребляет элементы.
nums = map(int, ["1", "2", "3"])
print(sum(nums), sum(nums), 2 in nums)
gen = (x for x in range(5))
print(3 in gen, list(gen)) # поиск съел 0..3
18. Ленивость генератора и значение переменной.
items = [1, 2, 3]
gen = (x * factor for x in items)
factor = 10
items = [7, 8, 9]
print(list(gen))
Разбор
В генераторном выражении первый итерируемый объект (items) вычисляется сразу — это старый список [1, 2, 3]. Всё остальное (factor) вычисляется лениво, при обходе, — к тому моменту factor уже 10.
items = [1, 2, 3]
factor = 1
gen = (x * factor for x in items)
factor = 10
items = [7, 8, 9]
print(list(gen))
Разное
19. Распаковка и звёздочка.
first, *middle, last = "Python"
a, b = b, a = 1, 2
print(first, middle, last, a, b)
Разбор
*middle собирает «всё остальное» в список. Цепочка присваиваний a, b = b, a = 1, 2 выполняется слева направо: сначала a, b = (1, 2), затем b, a = (1, 2).
first, *middle, last = "Python"
a, b = b, a = 1, 2
print(first, middle, last, a, b)
20. Сортировка устойчива, а sort возвращает None.
words = ["bb", "a", "ccc", "dd", "e"]
result = words.sort(key=len)
print(result, words)
Разбор
list.sort() сортирует на месте и возвращает None (как и append, reverse, random.shuffle) — чтобы никто не думал, что получил копию. Сортировка устойчива: элементы с равным ключом сохраняют исходный порядок (bb перед dd). Для новой отсортированной копии — sorted().
words = ["bb", "a", "ccc", "dd", "e"]
result = words.sort(key=len)
print(result, words)
print(sorted(["bb", "a", "ccc", "dd", "e"], key=len, reverse=True))
Итог
Почти все задачи сводятся к нескольким идеям:
- Имена — это ссылки на объекты; изменение объекта видно через все ссылки, присваивание перепривязывает только одно имя.
- Когда вычисляется выражение: аргументы по умолчанию — при
def, тело замыкания и генератора — при вызове/обходе. - Итераторы одноразовые.
- Атрибуты ищутся в экземпляре, потом в классе и по MRO.
and/orвозвращают операнды, сравнения цепляются, float неточен.
Практика решено 0 из 8
Что выведет код?
def f(x, acc=[]):
acc.append(x)
return acc
f(1)
print(f(2))
Что выведет код?
fs = [lambda: i * 10 for i in range(3)]
print([f() for f in fs])
Что выведет код?
board = [[0] * 2] * 2
board[0][0] = 5
print(board)
Что выведет код?
def g():
try:
return "try"
finally:
return "finally"
print(g())
Что выведет код?
t = (1, [2])
try:
t[1] += [3]
except TypeError:
pass
print(t)
Что выведет код?
print(1 < 2 < 3, 1 < 3 > 2, (1 < 2) < 1)
Что выведет код?
print(0 or [] or "x" or 5, 3 and 0 and 7)
Что выведет код?
print(round(2.5), round(3.5), round(0.1 + 0.2, 1) == 0.3, 0.1 + 0.2 == 0.3)
Войдите, чтобы отмечать пройденные уроки.