Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 10 из 20 Начальный 20 мин 120 XP

setdefault: значение при первом обращении

setdefault возвращает значение по ключу, а если ключа нет — сначала записывает умолчание: группировка гостей по городам без if-проверок, счётчики и подводный камень с вычислением аргумента.

Редакция Питоники

Метод get из урока 4 умеет возвращать умолчание, но не записывает его в словарь, а оператор in из урока 7 только отвечает «есть или нет». Для группировки нужен третий жест: при первом упоминании ключа создать пустой список, а дальше просто дополнять его. Это делает метод setdefault — буквально «установи по умолчанию».

Группировка: гости по городам

одна строка на весь цикл
guests = [
    ('Казань', 'Анна'),
    ('Москва', 'Борис'),
    ('Казань', 'Вера'),
    ('Москва', 'Лев'),
]

trips = {}
for city, name in guests:
    trips.setdefault(city, []).append(name)

print(trips)
Вывод
{'Казань': ['Анна', 'Вера'], 'Москва': ['Борис', 'Лев']}

Одна строка внутри цикла делает всю работу. setdefault смотрит на ключ: если города ещё нет — записывает под ним пустой список и возвращает его, и append тут же кладёт имя гостя. Если город уже встречался — возвращается существующий список. setdefault делает ровно одну вещь: при первом обращении создаёт умолчание, а дальше просто возвращает то, что уже лежит в словаре.

Что возвращает setdefault

есть ключ — вернётся значение, нет — умолчание
d = {'чай': 2}

print(d.setdefault('чай', 0))
print(d.setdefault('кофе', 1))
print(d)
Вывод
2
1
{'чай': 2, 'кофе': 1}

Для ключа 'чай' умолчание 0 проигнорировано: вернулось настоящее значение 2, и словарь не изменился. Для 'кофе' ключа не было — умолчание 1 записалось в словарь и вернулось. Перезаписать существующее значение setdefault не может: оно всегда побеждает умолчание. Запомните это — многие ждут от метода обратного.

Сравнение с if-подходом

то же самое тремя строками
guests = [
    ('Казань', 'Анна'),
    ('Москва', 'Борис'),
    ('Казань', 'Вера'),
    ('Москва', 'Лев'),
]

trips = {}
for city, name in guests:
    if city not in trips:
        trips[city] = []
    trips[city].append(name)

print(trips)
Вывод
{'Казань': ['Анна', 'Вера'], 'Москва': ['Борис', 'Лев']}

Три строки вместо одной — и везде одна церемония: проверить, создать, дописать. Явный if незаменим, когда при отсутствии ключа надо сделать что-то необычное — напечатать предупреждение, подставить заглушку. Когда же умолчание стандартное, setdefault короче и не оставляет места опечатке в двух местах. А излишен он там, где ключ только читается: для чтения без вставок верните get из урока 4.

setdefault, get и in: кто что делает

Три инструмента чтения, которые легко перепутать, потому что все три вызываются на словаре и все три что-то возвращают. Разница — ровно в одном: меняют они словарь или нет.

ВызовКлюча нетКлюч естьМеняет словарь
d.get(k, 0)вернёт 0вернёт значениенет
k in dFalseTrueнет
d.setdefault(k, 0)запишет 0 и вернёт еговернёт значениеда, при промахе

Запомните эту таблицу — она снимает большинство вопросов на собеседованиях. get и in — только чтение, словарь после них остаётся прежним. setdefault — единственный из трёх, кто умеет вставлять: при промахе он записывает умолчание и возвращает его. Если вставка вам не нужна, а нужен только результат, — берите get.

одни и те же ключи, три ответа
d = {'чай': 2}

print(d.get('чай', 0))
print('чай' in d)
print(d.setdefault('чай', 0))

print(d.setdefault('кофе', 1))
print(d)
Вывод
2
True
2
1
{'чай': 2, 'кофе': 1}

Первые три строки — про существующий ключ 'чай': все три дают 2 или True, и словарь не меняется. Четвёртая — про отсутствующий 'кофе': setdefault записал единицу и вернул её. В выводе видно, что пара 'кофе' теперь есть — в отличие от get, который бы ничего не добавил.

Подводный камень: аргумент считается всегда

Вот ловушка, на которой попадаются даже опытные. Второй аргумент setdefault — обычное выражение, и Python вычисляет его при каждом вызове, независимо от того, есть ключ или нет. Формально это правильно: значение нужно, чтобы вернуть его при промахе. Практически это значит, что дорогое вычисление в аргументе будет выполняться впустую на каждой итерации цикла.

умолчание вычисляется зря
d = {'чай': 2}

def make_default():
    print('создаём заготовку')
    return []

d.setdefault('чай', make_default())
print(d)
Вывод
создаём заготовку
{'чай': 2}

Ключ 'чай' на месте, setdefault вернул 2 — но функция make_default всё равно отработала и напечатала своё сообщение. Заготовка создана и тут же выброшена. С пустым списком это ничтожно, а с чем-то вроде datetime.now() или тяжёлого запроса — уже заметная потеря. Правило простое: во второй аргумент кладите только то, что стоит создавать.

Сценарий: счётчик слов одной строкой

Классическое применение setdefault — счётчик, который раньше писали тремя строками. Теперь умолчание отдаёт ноль, к нему прибавляется единица, и результат пишется обратно в тот же ключ. Запись получается плотной, но её смысл стоит разобрать.

счётчик через setdefault
counts = {}
words = ['чай', 'кофе', 'чай', 'чай']

for w in words:
    counts[w] = counts.setdefault(w, 0) + 1

print(counts)
Вывод
{'чай': 3, 'кофе': 1}

Читается это так: взять значение по ключу w (ноль, если ключа нет), прибавить единицу, записать обратно. Никакой отдельной проверки if не нужно — умолчание делает её сама. Но честно скажем: такая запись читается хуже, чем counts.get(w, 0) + 1, и уступает Counter из урока 11 и defaultdict(int) из урока 12. Её стоит знать, но писать новый код на ней я бы не стал.

то же самое через get
counts = {}
words = ['чай', 'кофе', 'чай', 'чай']

for w in words:
    counts[w] = counts.get(w, 0) + 1

print(counts)
Вывод
{'чай': 3, 'кофе': 1}

Результат тот же, и разница только в одном: get ничего не вставляет. Пока ключ не существует, словарь пуст — а в варианте с setdefault под каждым встреченным ключом сразу появляется запись с нулём. Для счётчика это незаметно, но в других задачах разница принципиальна: get оставляет словарь чистым, setdefault — заполняет его.

Альтернатива: группировка по отсортированному списку

Ручная группировка через setdefault работает всегда, но требует прохода по всем записям и памяти под словарь, где у каждой группы лежит свой список. Если записи уже идут отсортированными по ключу — а выгрузки из баз часто именно такие, — можно обойтись вообще без словаря: модуль itertools из стандартной библиотеки умеет разбирать отсортированный список на группы.

groupby вместо словаря
from itertools import groupby

rows = [
    ('Казань', 'Анна'),
    ('Казань', 'Вера'),
    ('Москва', 'Борис'),
    ('Москва', 'Лев'),
]

for city, group in groupby(rows, key=lambda pair: pair[0]):
    print(city, [name for _, name in group])
Вывод
Казань ['Анна', 'Вера']
Москва ['Борис', 'Лев']

Код выглядит короче и не тратит память на словарь, но платит за это требованием: записи должны идти подряд и быть отсортированы. Перемешали список — получите не две группы, а четыре обрывка по одной записи, и никакой ошибки не будет. В проектах мы держим setdefault основным приёмом, а groupby берём только там, где сортировку гарантирует сам источник данных.

Что дальше

Метод setdefault разобран: он возвращает значение, при промахе создаёт умолчание и никогда не перезаписывает существующее. Дальше — два инструмента, которые делают то же самое, но удобнее: Counter в уроке 11 считает повторы сам, а defaultdict в уроке 12 создаёт значение по заданному типу без всяких цепочек.

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

Сначала предскажи ответ в голове — это главный навык программиста.

d = {'чай': 2}

print(d.setdefault('чай', 0))
print(d.setdefault('кофе', 1))
e = {}
print(e.setdefault('a', []).append(1))
print(e)
groups = {}
groups.setdefault('а', []).append('аист')
groups.setdefault('а', []).append('арбуз')
print(groups)
Проверь себя
0 / 8

1. Что делает d.setdefault(ключ, умолчание), если ключа в словаре нет?

2. Что делает setdefault, если ключ уже есть?

3. Дано d = {'чай': 2}. Что вернёт d.setdefault('чай', 0)?

4. Почему группировку пишут цепочкой d.setdefault(k, []).append(x), а не через присваивание результата?

5. Когда setdefault излишен?

6. Какой из трёх вызовов умеет менять словарь?

7. Что произойдёт, если во второй аргумент setdefault передать вызов дорогой функции?

8. Как получить счётчик слов одной строкой в цикле, не проверяя наличие ключа?

Карточки терминов
Запомнено: 0 / 8
Практика

Дан список words = ['аист', 'берёза', 'арбуз', 'банан']. Собери словарь groups: ключ — первая буква слова, значение — список слов на эту букву. Используй setdefault.

practice.py
Вопросы и ответы по уроку

Что делает setdefault в Python?

Возвращает значение по ключу, а если ключа нет — записывает умолчание в словарь и возвращает его: d.setdefault('кофе', 1) при отсутствии 'кофе' создаст пару со значением 1. Если ключ есть, метод просто вернёт то, что лежит, без перезаписи.

Урок 4: метод get

Чем setdefault отличается от get?

Только судьбой умолчания при промахе: get возвращает его, не трогая словарь, setdefault — вставляет в словарь. Для чистого чтения берите get, для группировки и ленивого заполнения — setdefault.

Как сгруппировать значения по ключу в словаре?

Цепочкой setdefault и append в цикле: trips.setdefault(city, []).append(name). При первом городе создастся пустой список, дальше имена будут дописываться в него.

Когда setdefault лучше явного if, а когда нет?

setdefault выигрывает, когда умолчание стандартное и логика одна — создать и дописать. Явный if not in нужен, когда при отсутствии ключа надо сделать что-то нестандартное: напечатать сообщение, подставить сложную заглушку.

Урок 12: defaultdict

Чем setdefault отличается от get?

Только судьбой умолчания при промахе. get возвращает его, не трогая словарь; setdefault сначала записывает умолчание в словарь, потом возвращает. Для чистого чтения берите get, для группировки — setdefault или defaultdict.

Почему во второй аргумент setdefault не кладут дорогой вызов?

Потому что это обычное выражение, и Python вычисляет его при каждом вызове — даже если ключ уже есть и умолчание будет выброшено. Для пустого списка это незаметно, для datetime.now() или сетевого запроса — уже потеря. Кладите туда только то, что дёшево создать.

Как одной строкой завести счётчик слов в цикле?

counts[w] = counts.setdefault(w, 0) + 1. setdefault отдаёт ноль для невиданного слова, поэтому проверка не нужна. Альтернативы проще: Counter(words) из collections или defaultdict(int) с записью counts[w] += 1.

Урок 11: Counter

Понравился урок? Сошлитесь на него

«setdefault делает ровно одну вещь: при первом обращении создаёт умолчание, а дальше просто возвращает то, что уже лежит в словаре.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.