Как я должен обрабатывать включенные диапазоны в Python?
Я работаю в области, в которой диапазоны обычно описываются включительно. У меня есть понятные человеку описания, такие как from A to B
, которые представляют диапазоны, которые включают обе конечные точки - например, from 2 to 4
означает 2, 3, 4
.
Каков наилучший способ работы с этими диапазонами в коде Python? Следующий код работает для генерации инклюзивных диапазонов целых чисел, но мне также необходимо выполнить инклюзивные операции срезов:
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)
). Это повторение действительно лучший способ работать с инклюзивными диапазонами?
Изменить: Спасибо L3viathan за ответ. Написание функции inclusive_slice
для дополнения inclusive_range
, безусловно, вариант, хотя я, вероятно, написал бы ее следующим образом:
def inclusive_slice(start, stop, step):
...
return slice(start, (stop + 1) if step >= 0 else (stop - 1), step)
...
здесь представляет код для обработки отрицательных индексов, которые непросты при использовании со слайсами - обратите внимание, например, что функция L3viathan дает неверные результаты, если slice_to == -1
.
Однако кажется, что использовать функцию inclusive_slice
было бы неудобно - действительно ли l[inclusive_slice(A, B)]
лучше, чем l[A:B+1]
?
Есть ли лучший способ обработки инклюзивных диапазонов?
Изменить 2: Спасибо за новые ответы. Я согласен с Фрэнсисом и Корли, что изменение значения операций срезов, глобально или для определенных классов, приведет к значительной путанице. Поэтому я сейчас склоняюсь к написанию функции inclusive_slice
.
Чтобы ответить на мой собственный вопрос из предыдущего редактирования, я пришел к выводу, что использование такой функции (например, l[inclusive_slice(A, B)]
) было бы лучше, чем ручное добавление/вычитание 1 (например, l[A:B+1]
), поскольку это позволило бы получить крайние случаи ( такие как B == -1
и B == None
) для обработки в одном месте. Можем ли мы уменьшить неловкость при использовании функции?
Изменить 3: Я думал о том, как улучшить синтаксис использования, который в настоящее время выглядит как l[inclusive_slice(1, 5, 2)]
. В частности, было бы хорошо, если бы создание включающего слайса напоминало стандартный синтаксис слайса. Для этого вместо inclusive_slice(start, stop, step)
может быть функция inclusive
, которая принимает срез в качестве параметра. Идеальным синтаксисом использования для inclusive
была бы строка 1
:
l[inclusive(1:5:2)] # 1
l[inclusive(slice(1, 5, 2))] # 2
l[inclusive(s_[1:5:2])] # 3
l[inclusive[1:5:2]] # 4
l[1:inclusive(5):2] # 5
К сожалению, это не разрешено Python, который позволяет использовать только синтаксис :
в []
. Поэтому inclusive
должен вызываться с использованием синтаксиса 2
или 3
(где s_
действует как версия, предоставленная numpy).
Другие возможности состоят в том, чтобы превратить inclusive
в объект с __getitem__
, разрешающим синтаксис 4
, или применить inclusive
только к параметру stop
слайса, как в синтаксисе 5
. К сожалению, я не верю, что последнее можно заставить работать, поскольку inclusive
требует знания значения step
.
Из возможных синтаксисов (оригинал l[inclusive_slice(1, 5, 2)]
, плюс 2
, 3
и 4
), какой из них лучше всего использовать? Или есть другой, лучший вариант?
Окончательное редактирование: Спасибо всем за ответы и комментарии, это было очень интересно. Я всегда был поклонником философии Python "один способ сделать это", но эта проблема была вызвана конфликтом между Python "один путь" и "один путь", запрещенным проблемной областью. Я определенно оценил TIMTOWTDI в языковом дизайне.
За то, что дал первый и получивший наибольшее количество голосов ответ, я присуждаю награду L3viathan.
Ответы
Ответ 1
Напишите дополнительную функцию для инклюзивного среза и используйте это вместо резки. Хотя можно было бы, например, список подкласса и реализовать ответ __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]
) и хорошо комментируете.
Ответ 2
Так как в Python конечный индекс всегда является исключительным, стоит подумать о том, чтобы всегда использовать значения "Python-convention" внутри. Таким образом, вы избавитесь от смешивания двух в своем коде.
Только иметь дело с "внешним представлением" через специальные подпрограммы преобразования:
def text2range(text):
m = re.match(r"from (\d+) to (\d+)",text)
start,end = int(m.groups(1)),int(m.groups(2))+1
def range2text(start,end):
print "from %d to %d"%(start,end-1)
В качестве альтернативы вы можете пометить переменные, имеющие "необычное" представление, с истинным венгерским обозначением.
Ответ 3
Если вы не хотите указывать размер шага, а скорее количество шагов, есть возможность использовать numpy.linspace
, который включает начальную и конечную точку
import numpy as np
np.linspace(0,5,4)
# array([ 0. , 1.66666667, 3.33333333, 5. ])
Ответ 4
Я считаю, что стандартный ответ заключается в том, чтобы просто использовать +1 или -1 везде, где это необходимо.
Вы не хотите, чтобы глобально изменялось понимание уровня срезов (это приведет к разрыву большого количества кода), но другим решением будет построение иерархии классов для объектов, для которых вы хотите, чтобы срезы были инклюзивными. Например, для list
:
class InclusiveList(list):
def __getitem__(self, index):
if isinstance(index, slice):
start, stop, step = index.start, index.stop, index.step
if index.stop is not None:
if index.step is None:
stop += 1
else:
if index.step >= 0:
stop += 1
else:
if stop == 0:
stop = None # going from [4:0:-1] to [4::-1] since [4:-1:-1] wouldn't work
else:
stop -= 1
return super().__getitem__(slice(start, stop, step))
else:
return super().__getitem__(index)
>>> a = InclusiveList([1, 2, 4, 8, 16, 32])
>>> a
[1, 2, 4, 8, 16, 32]
>>> a[4]
16
>>> a[2:4]
[4, 8, 16]
>>> a[3:0:-1]
[8, 4, 2, 1]
>>> a[3::-1]
[8, 4, 2, 1]
>>> a[5:1:-2]
[32, 8, 2]
Конечно, вы хотите сделать то же самое с __setitem__
и __delitem__
.
(я использовал list
, но это работает для любого Sequence
или MutableSequence
.)
Ответ 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
# if negative step
elif step < 0:
while num >= stop:
yield num
num += step
И затем используйте как:
list(closed_range('1:5:2'))
[1,3,5]
Конечно, вам нужно будет также проверить другие формы плохого ввода, если кто-то еще собирается использовать эту функцию.
Ответ 6
Собирался комментировать, но проще писать код в качестве ответа, поэтому...
Я бы не писал класс, который переопределяет срез, если только он НЕ ОЧЕНЬ ясен. У меня есть класс, который представляет ints с битовой срезкой. В моих контекстах "4: 2" очень ясно включительно, и ints уже не имеет никакого использования для нарезки, поэтому он (едва ли приемлемый) (imho, а некоторые не согласятся).
Для списков у вас есть случай, что вы сделаете что-то вроде
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 inc_list(list):
def islice(self, start, end=None, dir=None):
return self.__getitem__(slice(start, end+1, dir))
l2 = inc_list([1,2,3,4,5])
l2[1:3]
[0x3,
0x4]
l2.islice(1,3)
[0x3,
0x4,
0x5]
Однако это решение, как и многие другие (помимо того, что я неполно... я знаю), имеет ахиллесовую пяту, поскольку это просто не так просто, как простая нотация фрагмента... это немного проще, чем прохождение как аргумент, но все же сложнее, чем просто [4: 2]. Единственный способ сделать это - передать что-то другое в срез, который можно было бы переусердствовать по-другому, чтобы пользователь знал, читая его, что они сделали, и он все равно может быть таким простым.
Одна возможность... чисел с плавающей запятой. Они разные, поэтому вы можете их видеть, и они не слишком сложны, чем "простой" синтаксис. Он не встроен, поэтому есть еще какая-то "магия", но, насколько синтаксический сахар, это неплохо....
class inc_list(list):
def __getitem__(self, x):
if isinstance(x, slice):
start, end, step = x.start, x.stop, x.step
if step == None:
step = 1
if isinstance(end, float):
end = int(end)
end = end + step
x = slice(start, end, step)
return list.__getitem__(self, x)
l2 = inc_list([1,2,3,4,5])
l2[1:3]
[0x2,
0x3]
l2[1:3.0]
[0x2,
0x3,
0x4]
3.0 должно быть достаточно, чтобы рассказать программисту на языке питона "эй, что-то необычное происходит"... не обязательно, что происходит, но, по крайней мере, не удивительно, что он действует "странно".
Обратите внимание, что в списке нет ничего уникального... вы можете легко написать декоратор, который мог бы сделать это для любого класса:
def inc_getitem(self, x):
if isinstance(x, slice):
start, end, step = x.start, x.stop, x.step
if step == 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
Первая форма, вероятно, более полезна.
Ответ 7
Это сложно и, вероятно, неразумно перегружать такие базовые понятия.
с новым классом 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
тогда вы можете управлять, как хотите, сохраняя естественную логику питона:
>>>>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
Ответ 8
Фокусировка на вашем запросе на лучший синтаксис, как насчет таргетинга:
l[1:UpThrough(5):2]
Это можно сделать с помощью метода __index__
:
class UpThrough(object):
def __init__(self, stop):
self.stop = stop
def __index__(self):
return self.stop + 1
class DownThrough(object):
def __init__(self, stop):
self.stop = stop
def __index__(self):
return self.stop - 1
Теперь вам даже не нужен специализированный класс списка (и его не нужно изменять
глобальное определение):
>>> l = [1,2,3,4]
>>> l[1:UpThrough(2)]
[2,3]
Если вы используете много, вы можете использовать более короткие имена upIncl
, downIncl
или даже
In
и InRev
.
Вы также можете построить эти классы, чтобы, кроме использования в срезе, они
действовать как фактический индекс:
def __int__(self):
return self.stop
Ответ 9
Вместо создания API, который не является обычным или распространяет типы данных, такие как список, было бы идеальным создать функцию Slice
оболочку по встроенному Slice
, чтобы вы могли передавать ее через любой требуется нарезка.
Python поддерживает этот подход для некоторых исключительных случаев, и случай, который у вас есть, может оправдать этот случай исключения. Например, включительный срез будет выглядеть как
def islice(start, stop = None, step = None):
if stop is not None: stop += 1
if stop == 0: stop = None
return slice(start, stop, step)
И вы можете использовать его для любых типов последовательностей
>>> range(1,10)[islice(1,5)]
[2, 3, 4, 5, 6]
>>> "Hello World"[islice(0,5,2)]
'Hlo'
>>> (3,1,4,1,5,9,2,6)[islice(1,-2)]
(1, 4, 1, 5, 9, 2)
Наконец, вы также можете создать инклюзивный диапазон под названием irange
, чтобы дополнить инклюзивный срез (записанный в строках OP).
def irange(start, stop, step):
return range(start, (stop + 1) if step >= 0 else (stop - 1), step)
Ответ 10
Я не уверен, что это уже охватывалось, вот как я обработал это, чтобы проверить, находится ли моя переменная в определенном диапазоне:
my var=10 # want to check if it is in range(0,10) as inclusive
limits = range(0,10)
limits.append(limits[-1]+1)
if(my_var in limits):
print("In Limit")
else:
print("Out of Limit")
Этот код будет возвращать "In Limit", так как я расширил свой диапазон на 1, что сделало его включающим