Подтвердить что ты не робот

Как написать __getitem__ чисто?

В Python при реализации типа последовательности я часто (относительно говоря) нахожу, что пишу код следующим образом:

class FooSequence(collections.abc.Sequence):
    # Snip other methods

    def __getitem__(self, key):
        if isinstance(key, int):
            # Get a single item
        elif isinstance(key, slice):
            # Get a whole slice
        else:
            raise TypeError('Index must be int, not {}'.format(type(key).__name__))

Код проверяет тип своего аргумента явно с помощью isinstance(). Это рассматривается как антипаттерн в сообществе Python. Как его избежать?

  • Я не могу использовать functools.singledispatch, потому что сознательно несовместим с методами (он попытается отправить self, что совершенно бесполезно, поскольку мы уже отправив на self через полиморфизм ООП). Он работает с @staticmethod, но что, если мне нужно получить материал из self?
  • Листинг int(), а затем захват TypeError, проверка на фрагмент и, возможно, повторное поднятие, все еще некрасиво, хотя, возможно, немного меньше.
  • Возможно, было бы проще преобразовать целые числа в одноэлементные срезы и обрабатывать обе ситуации с одним и тем же кодом, но у него есть свои проблемы (return 0 или [0]?).
4b9b3361

Ответ 1

Насколько мне кажется странным, я подозреваю, что так, как у вас есть, это лучший способ заниматься вещами. Шаблоны обычно существуют, чтобы охватить случаи общего использования, но это не означает, что их следует воспринимать как Евангелие, когда их следование усложняет жизнь. Основная причина, которую PEP 443 дает для исключения при явной проверке типов, заключается в том, что она "хрупка и закрыта для расширения". Однако это в основном относится к пользовательским функциям, которые в любое время используют разные типы. Из Документы Python на __getitem__:

Для типов последовательностей принятые ключи должны быть целыми и срезанными объектами. Обратите внимание, что специальная интерпретация отрицательных индексов (если класс хочет эмулировать тип последовательности) зависит от метода __getitem __(). Если ключ имеет неподходящий тип, TypeError может быть поднят; если из значения вне набора индексов для последовательности (после любой специальной интерпретации отрицательных значений), IndexError должен быть поднят. Для типов сопоставления, если отсутствует ключ (не в контейнере), KeyError должен быть поднят.

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

Если вы хотите избежать явной проверки типов, я бы указал вам на этот ответ SO. Он содержит краткую реализацию декоратора @methdispatch (а не мое имя, но я буду рулон с ним), который позволяет @singledispatch работать с методами, заставляя его проверять args[1] (arg), а не args[0] (self). Используя это, вы должны использовать индивидуальную отправку с помощью метода __getitem__.

Независимо от того, считаете ли вы, что любой из этих "pythonic" зависит от вас, но помните, что, хотя Zen of Python отмечает, что "особые случаи не являются достаточно сложными, чтобы нарушать правила", он сразу же отмечает, что "практичность превосходит чистоту". В этом случае просто проверка на наличие двух типов, которые явно указывается в документации, - это единственное, что поддерживает __getitem__, как практический способ для меня.

Ответ 2

Я не знаю, как избежать этого. Это просто компромисс с использованием динамически типизированного языка. Однако это не значит, что вы должны делать это снова и снова. Я бы разрешил его один раз, создав абстрактный класс с именами методов расщепления, затем наследуя от этого класса, а не непосредственно из Sequence, например:

class UnannoyingSequence(collections.abc.Sequence):

    def __getitem__(self, key):
        if isinstance(key, int):
            return self.getitem(key)
        elif isinstance(key, slice):
            return self.getslice(key)
        else:
            raise TypeError('Index must be int, not {}'.format(type(key).__name__))

    # default implementation in terms of getitem
    def getslice(self, key):
        # Get a whole slice

class FooSequence(UnannoyingSequence):
    def getitem(self, key):
        # Get a single item

    # optional efficient, type-specific implementation not in terms of getitem
    def getslice(self, key):
        # Get a whole slice

Это очищает FooSequence достаточно, чтобы я мог даже сделать это таким образом, если бы у меня был только один производный класс. Я удивлен, что стандартная библиотека уже не работает.

Ответ 3

Антипаттерн предназначен для проверки кода обычного пользовательского кода, особенно с помощью функции type() 1.

При работе с внутренними объектами могут потребоваться 2 проверки типов, и isinstance() является предпочтительным способом.

Другими словами, ваш код является совершенно Pythonic, и его единственная проблема - это сообщение об ошибке (в нем не упоминается slice).


Раскрытие информации: я разработчик ядра Python.


1 Когда это абсолютно необходимо, isinstance() - лучший выбор.

2 Особенно такие методы, как __getitem__

Ответ 4

Чтобы оставаться pythonic, у вас есть работа с семантикой, а не с типом объектов. Поэтому, если у вас есть какой-то параметр в качестве доступа к последовательности, просто используйте его так. Используйте абстракцию для параметра как можно дольше. Если вы ожидаете набор идентификаторов пользователей, не ожидайте набора, а скорее некоторую структуру данных с помощью метода add. Если вы ожидаете какой-то текст, не ожидайте объект unicode, а скорее некоторый контейнер для символов с encode и decode методами.

Я предполагаю, что в общем случае вы хотите сделать что-то вроде "Использовать поведение базовой реализации, если не предусмотрено какое-либо специальное значение. Если вы хотите реализовать __getitem__, вы можете использовать различие случаев, когда что-то другое происходит, если один специальный. Я бы использовал следующий шаблон:

class FooSequence(collections.abc.Sequence):
    # Snip other methods

    def __getitem__(self, key):
        try:
            if key == SPECIAL_VALUE:
                return SOMETHING_SPECIAL
            else:
                return self.our_baseclass_instance[key]
        except AttributeError:
            raise TypeError('Wrong type: {}'.format(type(key).__name__))

Если вы хотите различать одно значение (в терминологии perl "scalar" ) и последовательность (в терминологии Java "сбор" ), то это питонно отлично, чтобы определить, выполняется ли итератор. Вы можете использовать шаблон try-catch или hasattr, как и сейчас:

>>> a = 42
>>> b = [1, 3, 5, 7]
>>> c = slice(1, 42)
>>> hasattr(a, "__iter__")
False
>>> hasattr(b, "__iter__")
True
>>> hasattr(c, "__iter__")
False
>>>

Применяется к нашему примеру:

class FooSequence(collections.abc.Sequence):
    # Snip other methods

    def __getitem__(self, key):
        try:
            if hasattr(key, "__iter__"):
                return map(lambda x: WHATEVER(x), key)
            else:
                return self.our_baseclass_instance[key]
        except AttributeError:
            raise TypeError('Wrong type: {}'.format(type(key).__name__))

Динамические языки программирования, такие как python и ruby, используют утиную печать. И утка - это животное, которое ходит как утка, плавает, как утка и шарлатанцы, как утка. Не потому, что кто-то называет это "утка".