0

Как реализовать включительные диапазоны в Python?

82

Я пытаюсь реализовать пользовательский интерфейс, в котором диапазоны представляются включительно. У меня есть понятные человеко-читаемые описания, такие как от 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 ответ(ов)

0

Вы можете создать дополнительную функцию для включающего среза и использовать её вместо обычного среза. Хотя можно было бы, например, создать подкласс для 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]) и хорошо его прокомментировал. Это наиболее понятный и предсказуемый подход.

0

Если вы не хотите указывать размер шага, а хотите задать количество шагов, вы можете воспользоваться функцией 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.

0

В данном случае, без написания собственного класса, использование функции действительно выглядит как наилучший вариант. Я бы предложил не хранить фактические списки, а просто возвращать генераторы для заданного диапазона. Мы уже говорим о синтаксисе использования, так что вот что вы могли бы сделать:

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]

Разумеется, вам также нужно будет проверять и другие варианты неправильного ввода, если кто-то еще собирается использовать эту функцию.

0

Вопрос: Я собирался оставить комментарий, но проще написать код в ответ, итак...

Я бы не стал писать класс, который переопределяет срезы, если это не очень четко обосновано. У меня есть класс, который представляет целые числа с битовыми срезами. В моем контексте '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

Первая форма, вероятно, более полезна.

0

Это сложно и, вероятно, неразумно перегружать такие базовые концепции, создавая новый класс 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

Этот подход позволяет избежать перегрузки базовых концепций языка и сохраняет чистоту кода.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь