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

Как я должен обрабатывать включенные диапазоны в 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.

4b9b3361

Ответ 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, что сделало его включающим