Как реализовать метод __getitem__ в Python
Что проверяют
Понимаете ли вы протоколы данных Python: что делает интерпретатор при obj[key] и какие возможности появляются у объекта вместе с одним методом. Следом почти всегда просят добавить поддержку срезов или спрашивают, почему по вашему объекту вдруг заработал for.
Как отвечать
Выражение obj[key] превращается в type(obj).__getitem__(obj, key). Метод получает ровно один аргумент, но формы у него разные:
- целое число: обычная индексация. Для последовательности при выходе за границы нужно бросать
IndexError, при неверном типеTypeError; - срез:
obj[1:5:2]приходит как объектslice(1, 5, 2). Его границы нормализуетkey.indices(len(self)), а результатом разумно вернуть экземпляр своего же класса; - кортеж:
obj[1, 2]передаёт(1, 2), так устроена многомерная индексация в NumPy и pandas; - ключ отображения: для словарных классов при отсутствии ключа бросают
KeyError, а неIndexError.
Число лучше приводить через operator.index(key): так метод примет и numpy.int64, и всё, что реализует __index__, но отбросит float.
Дальше покажите связь с протоколами. Если у класса нет __iter__, цикл for и оператор in работают через __getitem__ с индексами 0, 1, 2 до первого IndexError, поэтому тип исключения здесь не формальность. Вместо ручной сборки удобнее унаследоваться от collections.abc.Sequence: достаточно __getitem__ и __len__, а __iter__, __contains__, __reversed__, index и count появятся сами. Для словарей аналог это collections.abc.Mapping.
Где спотыкаются
Забывают про срезы. Метод, который пропускает только isinstance(key, int), падает на obj[:3], хотя от последовательности этого ждут в первую очередь.
Бросают не то исключение. ValueError или свой класс вместо IndexError ломают неявную итерацию, и цикл for падает вместо того, чтобы закончиться.
Наследуются от dict и переопределяют только __getitem__. Методы get() и setdefault() встроенного словаря его не вызывают, и поведение расходится. Для этого есть collections.UserDict или __missing__, если нужен только ответ на отсутствующий ключ.