Слайсы вместо ключей: словарь, который сам себя заполняет

admin · 19.08.2026 20:32 #python #dict
Слайсы вместо ключей: словарь, который сам себя заполняет

Зачем это, если есть 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

Разбор по строчкам:

  1. super().__getitem__(key) — сначала честно ищем ключ. Если нашли — отдаём, как обычный dict.
  2. Нет ключа — а вдруг нам дали слайс? s[1:'Hello'] раскладывается так: start = ключ (1), stop = дефолт ('Hello'), step = что положить вместо него (если отличается).
  3. Ключа нет → заносим дефолт в словарь (__setitem__).
  4. Возвращаем 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]), и он добудет свежее значение ровно тогда, когда в нём возникнет нужда. Лениво? Зато как удобно.

Комментарии

Пока нет комментариев.

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