Превратить ломтик в диапазон

Я использую Python 3.3. Я хочу получить объект slice и использовать его для создания нового объекта range.

Это выглядит примерно так:

>>> class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.start, item.stop, item.step))

>>> a = A()
>>> a[1:5:2] # works fine
[1, 3]
>>> a[1:5] # won't work :(
Traceback (most recent call last):
  File "<pyshell#18>", line 1, in <module>
    a[1:5] # won't work :(
  File "<pyshell#9>", line 4, in __getitem__
    return list(range(item.start, item.stop, item.step))
TypeError: 'NoneType' object cannot be interpreted as an integer

Что ж, проблема здесь очевидна - range не принимает значение None в качестве значения:

>>> range(1, 5, None)
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in <module>
    range(1, 5, None)
TypeError: 'NoneType' object cannot be interpreted as an integer

Но то, что не очевидно (для меня), является решением. Как я буду называть range чтобы он работал в каждом случае? Я ищу хороший питонический способ сделать это.

Ответ 1

Try

class A:
    def __getitem__(self, item):
        ifnone = lambda a, b: b if a is None else a
        if isinstance(item, slice):
            if item.stop is None:
                # do something with itertools.count()
            else:
                return list(range(ifnone(item.start, 0), item.stop, ifnone(item.step, 1)))
        else:
            return item

Это будет интерпретировать .start и .step соответственно, если они None.


Другим вариантом может быть метод .indices() среза. Он вызывается с количеством записей и переинтерпретирует None с соответствующими значениями и обертывает отрицательные значения вокруг параметра заданной длины:

>>> a=slice(None, None, None)
>>> a.indices(1)
(0, 1, 1)
>>> a.indices(10)
(0, 10, 1)
>>> a=slice(None, -5, None)
>>> a.indices(100)
(0, 95, 1)

Это зависит от того, что вы намерены делать с отрицательными индексами...

Ответ 2

Там более простой способ сделать это (по крайней мере, в 3.4, у меня сейчас нет 3.3, и я не вижу его в списке изменений).

Предполагая, что ваш класс уже имеет известную длину, вы можете просто нарезать диапазон этого размера:

>>> range(10)[1:5:2]
range(1, 5, 2)
>>> list(range(10)[1:5:2])
[1, 3]

Если вы не знаете длину априори, вам придется делать:

>>> class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.stop)[item])
>>> a = A()
>>> a[1:5:2]
[1, 3]
>>> a[1:5]
[1, 2, 3, 4]

Ответ 3

Проблема:

Срез состоит из параметров start, stop и step и может быть создан с помощью фрагментации нотации или с помощью slice встроенный. Любой (или все) параметров start, stop и step может быть None.

# valid
sliceable[None:None:None]

# also valid
cut = slice(None, None, None)
sliceable[cut]

Однако, как указано в исходном вопросе, функция range не принимает аргументы None. Вы можете обойти это по-разному...

Решения

С условной логикой:

if item.start None:
    return list(range(item.start, item.stop))
return list(range(item.start, item.stop, item.step))

... который может оказаться излишне сложным, поскольку любой или все параметры могут быть None.

С условными переменными:

start = item.start if item.start is None else 0
step = item.step if item.step is None else 1
return list(range(item.start, item.stop, item.step))

... который является явным, но немного подробным.

С условностями непосредственно в инструкции:

return list(range(item.start if item.start else 0, item.stop, item.step if item.step else 1))

... который также излишне подробен.

С помощью функции или оператора лямбда:

ifnone = lambda a, b: b if a is None else a
range(ifnone(item.start, 0), item.stop, ifnone(item.step, 1)

..., что может быть трудно понять.

С 'или':

return list(range(item.start or 0, item.stop or len(self), item.step or 1))

Я использую or, чтобы назначить разумные значения по умолчанию самым простым. Он явный, простой, понятный и лаконичный.

Завершение реализации

Для завершения реализации вы также должны обрабатывать целые индексы (int, long и т.д.), установив isinstance(item, numbers.Integral) (см. int vs numbers.Integral).

Определите __len__, чтобы разрешить использование len(self) для значения остановки по умолчанию.

Наконец, поднимите соответствующий TypeError для недопустимых индексов (например, строк и т.д.).

TL; DR;

class A:
    def __len__(self):
        return 0

    def __getitem__(self, item):
        if isinstance(item, numbers.Integral):  # item is an integer
            return item
        if isinstance(item, slice):  # item is a slice
            return list(range(item.start or 0, item.stop or len(self), item.step or 1))
        else:  # invalid index type
            raise TypeError('{cls} indices must be integers or slices, not {idx}'.format(
                cls=type(self).__name__,
                idx=type(item).__name__,
            ))

Ответ 4

Я бы использовал специальный ветвь item.step is None:

def __getitem__(self, item):
    if isinstance(item, slice):
        if item.step is None:
            return list(range(item.start, item.stop))
        return list(range(item.start, item.stop, item.step))

и вы будете обрабатывать диапазоны, которые должны правильно подсчитываться.

Ответ 5

В последнем примере a[1:5], item.step == None и вы пытаетесь сделать range(1, 5, None), что, конечно, вызывает ошибку. Быстрый способ исправления:

class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.start, item.stop, item.step if item.step else 1)) #Changed line!

Но это просто показать вам вашу проблему. Это не лучший подход.