Python словарь - бинарный поиск ключа?

Я хочу написать класс контейнера, который действует как словарь (на самом деле происходит от dict). Ключами для этой структуры будут даты.

Когда ключ (то есть дата) используется для извлечения значения из класса, если дата не существует, для возврата значения используется следующая доступная дата, которая предшествует ключу.

Следующие данные должны помочь объяснить концепцию далее:

Date (key)      Value
2001/01/01      123
2001/01/02       42
2001/01/03      100
2001/01/04      314
2001/01/07      312
2001/01/09      321

Если я попытаюсь получить значение, связанное с ключом (датой) '2001/01/05', я должен получить значение, сохраненное под ключом 2001/01/04, так как этот ключ встречается до того момента, когда ключ '2001/01/05 'будет, если он существует в словаре.

Чтобы сделать это, мне нужно иметь возможность выполнять поиск (в идеале двоичный, а не наивный цикл через каждый ключ в словаре). Я искал поиск ключей словаря bsearch в словарях Python - но не нашел ничего полезного.

В любом случае, я хочу написать такой класс, который инкапсулирует это поведение.

Это то, что у меня до сих пор (не так много):

#
class NearestNeighborDict(dict):
#
"""
#
a dictionary which returns value of nearest neighbor 
if specified key not found
#
"""

def __init__(self, items={}):
    dict.__init__(self, items)


def get_item(self, key):
    # returns the item stored with the key (if key exists)
    # else it returns the item stored with the key

Ответ 1

Вы действительно не хотите подкласса dict, потому что вы не можете повторно использовать какую-либо его функциональность. Скорее, подкласс абстрактного базового класса collections.Mapping (или MutableMapping, если вы также хотите изменить экземпляр после создания), выполните необходимые для этой цели специальные методы, и вы получите другие dict -подобные методы "бесплатно" от ABC.

Методы, необходимые для кода: __getitem____setitem__ и __delitem__, если вы хотите изменить), __len__, __iter__ и __contains__.

Модуль bisect стандартной библиотеки дает вам все, что вам нужно, чтобы эффективно реализовать их поверх сортированного списка. Например...:

import collections
import bisect

class MyDict(collections.Mapping):
  def __init__(self, contents):
    "contents must be a sequence of key/value pairs"
    self._list = sorted(contents)
  def __iter__(self):
    return (k for (k, _) in self._list)
  def __contains__(self, k):
    i = bisect.bisect_left(self._list, (k, None))
    return i < len(self._list) and self._list[i][0] == k
  def __len__(self):
    return len(self._list)
  def __getitem__(self, k):
    i = bisect.bisect_left(self._list, (k, None))
    if i >= len(self._list): raise KeyError(k)
    return self._list[i][1]

Вероятно, вы захотите поиграть __getitem__ в зависимости от того, что вы хотите вернуть (или хотите поднять) для различных угловых случаев, таких как "k больше, чем все клавиши в self".

Ответ 2

Модуль sortedcontainers предоставляет тип SortedDict, который поддерживает ключи в отсортированном порядке и поддерживает разделение по этим ключам. Модуль представляет собой версии pure-Python и fast-as-C со 100% -ным охватом тестирования и часами стресса.

Например:

from sortedcontainers import SortedDict

sd = SortedDict((date, value) for date, value in data)

# Bisect for the index of the desired key.
index = sd.bisect('2001/01/05')

# Lookup the real key at that index.
key = sd.iloc[index]

# Retrieve the value associated with that key.
value = sd[key]

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

Ответ 3

Я бы расширил dict и переопределил метод __getitem__ и __setitem__, чтобы сохранить отсортированный список ключей.

from bisect import bisect

class NearestNeighborDict(dict):
    def __init__(self):
        dict.__init__(self)
        self._keylist = []

    def __getitem__(self, x):
        if x in self:
            return dict.__getitem__(self, x)

        index = bisect(self._keylist, x)
        if index == len(self._keylist):
            raise KeyError('No next date')

        return dict.__getitem__(self, self._keylist[index])

    def __setitem__(self, x, value):
        if x not in self:
            index = bisect(self._keylist, x)
            self._keylist.insert(index, value)

        dict.__setitem__(self, x, value)

Истинно, вам лучше унаследовать от MutableMapping, но принцип тот же, и приведенный выше код можно легко адаптировать.

Ответ 4

Почему бы просто не сохранить отсортированный список из dict.keys() и выполнить поиск? Если вы подклассифицируете dict, вы даже можете создать двоичную вставку в этом списке при добавлении значений.