Превратить ломтик в диапазон
Я использую 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!
Но это просто показать вам вашу проблему. Это не лучший подход.