Классические ловушки: аргументы по умолчанию, позднее связывание и другие
Эти ошибки делают все — поэтому о них так любят спрашивать. Для каждой: запустите пример, посмотрите 👣 Пошагово и запомните исправление.
1. Изменяемое значение по умолчанию
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1))
print(append_to(2)) # [1, 2] — список «помнит» прошлый вызов
print(append_to.__defaults__)
Почему: значения по умолчанию вычисляются один раз — при выполнении def, и хранятся в func.__defaults__. Все вызовы получают один и тот же список.
def append_to(item, target=None):
if target is None:
target = [] # новый список на каждый вызов
target.append(item)
return target
print(append_to(1), append_to(2))
То же касается {}, set(), объектов и даже datetime.now(): def log(msg, ts=datetime.now()) запомнит время определения функции.
2. Позднее связывание в замыканиях
callbacks = [lambda: i for i in range(3)]
print([cb() for cb in callbacks]) # [2, 2, 2]
callbacks = [lambda i=i: i for i in range(3)]
print([cb() for cb in callbacks]) # [0, 1, 2]
Почему: замыкание хранит ссылку на переменную, значение читается при вызове. Исправление: аргумент по умолчанию или functools.partial.
3. Умножение списка со вложенными списками
board = [[" "] * 3] * 3
board[0][0] = "X"
print(board) # X во всех строках
board = [[" "] * 3 for _ in range(3)]
board[0][0] = "X"
print(board)
Почему: * копирует ссылки — три ссылки на один внутренний список.
4. Изменение коллекции во время перебора
nums = [1, 2, 3, 4, 5, 6]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # кажется, работает…
nums = [2, 4, 6, 8]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # [4, 8] — элементы пропущены!
nums = [2, 4, 6, 8]
nums = [n for n in nums if n % 2] # правильно
print(nums)
Для словаря и множества изменение размера во время перебора — сразу RuntimeError.
5. Изменяемый атрибут класса
class Cart:
items = [] # общий для ВСЕХ экземпляров!
def add(self, x):
self.items.append(x)
a, b = Cart(), Cart()
a.add("яблоко")
print(b.items) # ['яблоко']
class Cart:
def __init__(self):
self.items = [] # у каждого экземпляра свой
def add(self, x):
self.items.append(x)
a, b = Cart(), Cart()
a.add("яблоко")
print(b.items)
6. is вместо ==
a = 256; b = 256
print(a is b) # True — кеш малых чисел
x = int("257"); y = int("257")
print(x is y, x == y) # False True
s1 = "hello world!"; s2 = " ".join(["hello", "world!"])
print(s1 is s2, s1 == s2) # False True
is — только для None, True, False.
7. or для значений по умолчанию
def connect(timeout=None):
timeout = timeout or 30
return timeout
print(connect(0)) # 30, хотя просили 0!
def connect(timeout=None):
return 30 if timeout is None else timeout
print(connect(0))
8. Сравнение float
import math
print(0.1 * 3 == 0.3, math.isclose(0.1 * 3, 0.3))
print(sum([0.1] * 10) == 1.0)
9. Одноразовые итераторы
squares = map(lambda x: x * x, [1, 2, 3])
print(list(squares), list(squares)) # второй раз — пусто
data = (x for x in range(3))
print(2 in data, list(data)) # проверка «съела» элементы
10. Затенение встроенных имён и модулей
list = [1, 2, 3]
sum = 10
print(list(range(3))) # TypeError: 'list' object is not callable
Такая же беда — файл random.py или json.py в своём проекте: import random импортирует ваш файл вместо стандартного модуля.
11. «Голый» except и проглатывание ошибок
def parse(value):
try:
return int(valeu) # опечатка → NameError
except: # перехватывает и её тоже!
return 0
print(parse("42")) # 0 — ошибка в коде незаметна
def parse(value):
try:
return int(value)
except ValueError: # только ожидаемое исключение
return 0
print(parse("42"), parse("x"))
12. Распаковка и += с кортежем
t = ([1],)
try:
t[0] += [2]
except TypeError as e:
print(e)
print(t) # ([1, 2],) — список изменился, хотя была ошибка
13. Целочисленное деление и округление
print(-7 // 2, int(-7 / 2), round(2.5), round(3.5))
// округляет вниз, int() — к нулю, round() — к чётному.
14. Порядок вычисления значений по умолчанию и время
import time
def stamp(ts=time.time()):
return ts
a = stamp()
time.sleep(0.01)
print(stamp() == a) # True — время зафиксировано при def
Как отвечать на собеседовании
Не просто называйте результат, а объясняйте механизм: «значения по умолчанию вычисляются один раз при определении функции», «замыкание захватывает переменную, а не значение», «умножение списка копирует ссылки». Затем — исправление.
Итог
| Ловушка | Исправление |
|---|---|
def f(x=[]) |
x=None + создать внутри |
lambda: i в цикле |
lambda i=i: i |
[[0] * n] * m |
[[0] * n for _ in range(m)] |
remove во время for |
новый список / перебор копии |
| изменяемый атрибут класса | создавать в __init__ |
is для значений |
== |
x or default при допустимом 0 |
default if x is None else x |
a == b для float |
math.isclose |
| повторный проход генератора | list(...) или создать заново |
except: |
конкретное исключение |
Практика решено 0 из 6
Что выведет код?
def add(item, items=[]):
items.append(item)
return items
print(add(1))
print(add(2))
Исправьте функцию add_tag, чтобы каждый вызов без аргумента tags работал с новым списком.
Какие тесты проверяют код (3)
assert add_tag("a") == ["a"]
assert add_tag("b") == ["b"], "Второй вызов не должен видеть тег из первого"
my = ["x"]; assert add_tag("y", my) == ["x", "y"] and my == ["x", "y"]
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
Исправьте класс Team, чтобы у каждой команды был свой список участников.
Какие тесты проверяют код (2)
a = Team("A"); b = Team("B"); a.add("Аня"); assert b.members == [], "Участники команды A попали в команду B"
c = Team("C"); c.add("Боря"); assert c.members == ["Боря"]
class Team:
def __init__(self, name):
self.name = name
self.members = []
def add(self, person):
self.members.append(person)
Что выведет код?
nums = (x * x for x in range(3))
print(sum(nums))
print(sum(nums))
Чем плох «голый» except: (без указания исключения)?
Есть ли в строке ловушка?
| Ловушка | Всё нормально | |
|---|---|---|
def f(x, cache={}): ... (кеш специально общий) |
||
if x is 1000: |
||
timeout = value or 30 (0 — допустимое значение) |
||
if 0.1 + 0.2 == 0.3: |
||
grid = [[0] * 3 for _ in range(3)] |
||
for k in list(d): del d[k] |
Войдите, чтобы отмечать пройденные уроки.