«Что выведет код?»: каверзные задачи

⏱ 45 мин · 🧩 заданий: 8

Как решать такие задачи

На собеседовании такие задачи проверяют, понимаете ли вы модель исполнения 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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Ввод ответа

Что выведет код?

def f(x, acc=[]):
    acc.append(x)
    return acc
f(1)
print(f(2))
Задание 2 Ввод ответа

Что выведет код?

fs = [lambda: i * 10 for i in range(3)]
print([f() for f in fs])
Задание 3 Ввод ответа

Что выведет код?

board = [[0] * 2] * 2
board[0][0] = 5
print(board)
Задание 4 Ввод ответа

Что выведет код?

def g():
    try:
        return "try"
    finally:
        return "finally"
print(g())
Задание 5 Ввод ответа

Что выведет код?

t = (1, [2])
try:
    t[1] += [3]
except TypeError:
    pass
print(t)
Задание 6 Ввод ответа

Что выведет код?

print(1 < 2 < 3, 1 < 3 > 2, (1 < 2) < 1)
Задание 7 Ввод ответа

Что выведет код?

print(0 or [] or "x" or 5, 3 and 0 and 7)
Задание 8 Ввод ответа

Что выведет код?

print(round(2.5), round(3.5), round(0.1 + 0.2, 1) == 0.3, 0.1 + 0.2 == 0.3)

Войдите, чтобы отмечать пройденные уроки.