Слайсы вместо ключей: словарь, который сам себя заполняет
Зачем это, если есть dict.get()?
Есть. И .setdefault() тоже есть. Но вопрос «зачем» в Python обычно означает «пиши дальше». Этот пост — про то, как занять выходные классом Sdict: обычный словарь, у которого слайс вместо ключа даёт значение по умолчанию, а заодно может это значение подложить в словарь — сразу или по явной команде.
Если коротко: забудь про три строчки default = d.get(k, fallback); d.setdefault(k, default) — теперь это d[k:fallback]. Красиво? Нет. Забавно? Абсолютно.
Настройка: Sdict.__getitem__ читает слайс
class Sdict(dict):
def __getitem__(self, key):
try:
return super().__getitem__(key)
except KeyError:
if type(key) is slice:
try:
return super().__getitem__(key.start)
except KeyError:
if key.stop is not None:
if key.step is not None:
self.__setitem__(key.start, key.step)
else:
self.__setitem__(key.start, key.stop)
return key.stop
raise
Разбор по строчкам:
super().__getitem__(key)— сначала честно ищем ключ. Если нашли — отдаём, как обычный dict.- Нет ключа — а вдруг нам дали слайс?
s[1:'Hello']раскладывается так: start = ключ (1), stop = дефолт ('Hello'), step = что положить вместо него (если отличается). - Ключа нет → заносим дефолт в словарь (
__setitem__). - Возвращаем
key.stop— то значение, которое «просили» вернуть.
Смотри работу вживую:
>>> s = Sdict()
>>> s[1] # ключа нет — обычный KeyError
KeyError: 1
>>> s[1:'Hello'] # нет → верни 'Hello'
'Hello'
>>> s[1] # …и запомни его
'Hello'
>>> s[2:'Hello':'World'] # верни 'Hello', но сохрани 'World'
'Hello'
>>> s[2]
'World'
Трюк со step: s[2:'Hello':'World'] возвращает 'Hello', а кладёт в словарь 'World'. То есть «ответь одно, закэшируй другое». Кому-нибудь пригодится. Наверное.
Вариант 2: дефолт, который не сохраняется сам по себе
Иногда молча писать в словарь не хочется — вдруг ключ на самом деле отсутствовал, и мы просто проверяли. Усложняем условие: теперь s[k:default] возвращает дефолт, но не сохраняет. Чтобы сохранить — надо явно попросить через ...:
class Sdict(dict):
def __getitem__(self, key):
try:
return super().__getitem__(key)
except KeyError:
if type(key) is slice:
try:
return super().__getitem__(key.start)
except KeyError:
if key.stop is not None:
if key.step is not None:
if key.step is Ellipsis:
self.__setitem__(key.start, key.stop)
else:
self.__setitem__(key.start, key.step)
return key.stop
raise
Демонстрация:
>>> s = Sdict()
>>> s[1]
KeyError: 1
>>> s[1:"Hello"] # вернули…
'Hello'
>>> s[1] # …но не запомнили
KeyError: 1
>>> s[1:"Hello":...] # «…» — то самое явное «сохрани»
'Hello'
>>> s[1] # теперь запомнен
'Hello'
>>> s[2:"Hello":"World"] # верни 'Hello', сохрани 'World'
'Hello'
>>> s[2]
'World'
Ключевая строка — if key.step is Ellipsis. ... в Python — это литерал Ellipsis. Тут он играет роль команды «сохрани дефолт». Интуитивно-понятно? Нет. Работает? Да.
Вариант 3: ленивый дефолт
А если дефолт не хочется готовить заранее? Пусть словарь сам соорудит его ровно в тот момент, когда ключа действительно нет. Тогда step перестаёт быть «значением про запас» и превращается в ленивую фабрику: вместо self.__setitem__(key.start, key.step) словарь зовёт key.step() — как конвейер, который включается строго по требованию:
class Sdict(dict):
def __getitem__(self, key):
try:
return super().__getitem__(key)
except KeyError:
if type(key) is slice:
try:
return super().__getitem__(key.start)
except KeyError:
v = key.stop
if key.step is not None:
v = key.step()
self.__setitem__(key.start, v)
return v
raise
Секрет — в строчке v = key.step(). Ключа нет → просим фабрику, зовём step() (вот и вся «ленивость»: значение рождается ровно в этот момент, не раньше), кладём результат в словарь и возвращаем его.
Демонстрация:
>>> s = Sdict()
>>> s[0]
KeyError: 0
>>> s[1:'Hello']
'Hello'
>>> s[2::list]
[]
>>> s[1]
'Hello'
>>> s[2]
[]
Здесь самое вкусное — s[2::list]. Слайс без stop, но со step=list: ключа 2 нет, словарь не растерялся и лениво вызвал list(), положив в словарь свеженький пустой []. Никаких общих мутабельных значений, о которые спотыкается defaultdict(list). Хотя defaultdict(list) тоже неплох. Но у нас же выходные!
Таблица поведений
| Вызов | Вариант 1 (простой) | Вариант 2 (с ...) |
Вариант 3 (ленивый) |
|---|---|---|---|
s[missing] |
KeyError |
KeyError |
KeyError |
s[k:default] |
вернёт default и сохранит |
вернёт default, не сохранит |
вернёт default и сохранит |
s[k:default:...] |
то же, что выше | вернёт и сохранит default |
не совместим: ...() не вызовешь |
s[k::factory] |
— | — | вызовет factory(), вернёт и сохранит результат |
s[k:ret:store] |
вернёт ret, сохранит store |
вернёт ret, сохранит store |
вызовет store(), вернёт и сохранит результат |
Вердикт
Практическая польза стремится к нулю, а вот такое на ревью запомнится надолго. Если вдруг захочется понять, что делает s[1:'Hello'], — теперь знаешь: «верни 'Hello', а ключу 1 пропиши это значение». А если s[2:'Hello':'World'] — «ответь 'Hello', но в словаре оставь 'World'». И не забывай ..., если словарь стесняется сохранять за тебя.
А третий вариант — для тех, кому дефолт лень даже придумывать: отдай словарю фабрику (s[2::list]), и он добудет свежее значение ровно тогда, когда в нём возникнет нужда. Лениво? Зато как удобно.
Комментарии
Пока нет комментариев.
Войдите, чтобы комментировать.