Как реализовать включительные диапазоны в Python?
Я пытаюсь реализовать пользовательский интерфейс, в котором диапазоны представляются включительно. У меня есть понятные человеко-читаемые описания, такие как от A до B
, которые представляют диапазоны, включающие обе границы. Например, от 2 до 4
означает значения 2, 3, 4
.
Я выяснил, что могу использовать следующий код для преобразования этих описаний в объекты range
, представляющие желаемые значения:
def inclusive_range(start, stop, step):
return range(start, (stop + 1) if step >= 0 else (stop - 1), step)
Однако мне также нужно выполнять операции срезов включительно. Есть ли способ избежать явных корректировок + 1
или - 1
каждый раз, когда я использую range
или нотацию срезов (например, range(A, B + 1)
, l[A:B+1]
, range(B, A - 1, -1)
)?
5 ответ(ов)
Вы можете создать дополнительную функцию для включающего среза и использовать её вместо обычного среза. Хотя можно было бы, например, создать подкласс для list
и реализовать метод __getitem__
, который будет реагировать на объект среза, я бы не советовал этого делать. Такое поведение может быть неожиданным для других разработчиков, а также для вас самих через год.
Функция inclusive_slice
может выглядеть следующим образом:
def inclusive_slice(myList, slice_from=None, slice_to=None, step=1):
if slice_to is not None:
slice_to += 1 if step > 0 else -1
if slice_to == 0:
slice_to = None
return myList[slice_from:slice_to:step]
Лично я бы просто использовал "комплексное" решение, о котором вы упомянули (например, range(A, B + 1)
и l[A:B+1]
) и хорошо его прокомментировал. Это наиболее понятный и предсказуемый подход.
Если вы не хотите указывать размер шага, а хотите задать количество шагов, вы можете воспользоваться функцией numpy.linspace
, которая включает начальную и конечную точки.
Вот пример:
import numpy as np
np.linspace(0, 5, 4)
# array([ 0. , 1.66666667, 3.33333333, 5. ])
В данном случае вызов np.linspace(0, 5, 4)
возвращает массив, состоящий из 4 равномерно распределенных значений от 0 до 5.
В данном случае, без написания собственного класса, использование функции действительно выглядит как наилучший вариант. Я бы предложил не хранить фактические списки, а просто возвращать генераторы для заданного диапазона. Мы уже говорим о синтаксисе использования, так что вот что вы могли бы сделать:
def closed_range(slices):
slice_parts = slices.split(':')
[start, stop, step] = map(int, slice_parts)
num = start
if start <= stop and step > 0:
while num <= stop:
yield num
num += step
# если шаг отрицательный
elif step < 0:
while num >= stop:
yield num
num += step
А затем использовать это так:
list(closed_range('1:5:2'))
# [1, 3, 5]
Разумеется, вам также нужно будет проверять и другие варианты неправильного ввода, если кто-то еще собирается использовать эту функцию.
Вопрос: Я собирался оставить комментарий, но проще написать код в ответ, итак...
Я бы не стал писать класс, который переопределяет срезы, если это не очень четко обосновано. У меня есть класс, который представляет целые числа с битовыми срезами. В моем контексте '4:2' очень явно инклюзивно, и целые числа уже не имеют другого использования для срезов, так что это (едва) приемлемо (на мой взгляд, хотя некоторые могут с этим не согласиться).
В случае со списками у вас возникает такая ситуация:
list1 = [1, 2, 3, 4, 5]
list2 = InclusiveList([1, 2, 3, 4, 5])
А позже в коде:
if list1[4:2] == test_list or list2[4:2] == test_list:
Это очень легкая ошибка, поскольку у списков уже есть хорошо определенное использование... они выглядят идентично, но ведут себя по-разному, и это будет очень запутанно при отладке, особенно если вы это не написали.
Это не означает, что вы полностью в потере... срезы удобны, но в конечном счете это всего лишь функция. И вы можете добавить эту функцию к чему угодно, так что, возможно, это будет проще:
class IncList(list):
def islice(self, start, end=None, dir=None):
return self.__getitem__(slice(start, end + 1, dir))
l2 = IncList([1, 2, 3, 4, 5])
l2[1:3]
# [3, 4]
l2.islice(1, 3)
# [3, 4, 5]
Однако это решение, как и многие другие, (кроме того, что оно неполное... я знаю) имеет ахиллесову пяту в том, что это просто не так просто, как простая нотация среза... это немного проще, чем передавать список в качестве аргумента, но все равно сложнее, чем просто [4:2]. Единственный способ сделать это возможным — передать что-то другое в срез, что могло бы быть истолковано иначе, чтобы пользователь, читая код, понимал, что он делает, и это все еще могло быть так же просто.
Одна из возможностей... числа с плавающей запятой. Они разные, так что их можно заметить, и они не намного сложнее, чем 'простая' нотация. Это не встроено, так что здесь есть немного 'магии', но с точки зрения синтаксического сахара это неплохо....
class IncList(list):
def __getitem__(self, x):
if isinstance(x, slice):
start, end, step = x.start, x.stop, x.step
if step is None:
step = 1
if isinstance(end, float):
end = int(end)
end = end + step
x = slice(start, end, step)
return list.__getitem__(self, x)
l2 = IncList([1, 2, 3, 4, 5])
l2[1:3]
# [2, 3]
l2[1:3.0]
# [2, 3, 4]
Число 3.0 должно быть достаточным, чтобы любой программист на Python заметил: 'эй, что-то необычное происходит'... не обязательно что происходит, но по крайней мере не будет сюрприза, что оно ведет себя 'странно'.
Обратите внимание, что нет ничего уникального в этом для списков... вы можете легко написать декоратор, который сможет сделать это для любого класса:
def inc_getitem(self, x):
if isinstance(x, slice):
start, end, step = x.start, x.stop, x.step
if step is None:
step = 1
if isinstance(end, float):
end = int(end)
end = end + step
x = slice(start, end, step)
return list.__getitem__(self, x)
def inclusiveclass(inclass):
class newclass(inclass):
__getitem__ = inc_getitem
return newclass
ilist = inclusiveclass(list)
или
@inclusiveclass
class InclusiveList(list):
pass
Первая форма, вероятно, более полезна.
Это сложно и, вероятно, неразумно перегружать такие базовые концепции, создавая новый класс inclusivelist с длиной len(l[a:b])
в b-a+1
, что может привести к путанице.
Чтобы сохранить естественное восприятие Python, обеспечивая читаемость в стиле BASIC, просто определите:
STEP=FROM=lambda x:x
TO=lambda x:x+1 if x!=-1 else None
DOWNTO=lambda x:x-1 if x!=0 else None
Тогда вы можете управлять так, как вам хочется, сохраняя естественную логику Python:
>>>>l=list(range(FROM(0),TO(9)))
>>>>l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>l[FROM(9):DOWNTO(3):STEP(-2)] == l[9:2:-2]
True
Этот подход позволяет избежать перегрузки базовых концепций языка и сохраняет чистоту кода.
Как использовать десятичное значение шага в range()?
В чем разница между функциями range и xrange в Python 2.X?
Вывод списка в обратном порядке с помощью функции range()
Есть ли эквивалент функции range(12) из Python в C#?
Создание и чтение временного файла