我应该如何处理Python中的包含范围?

How should I handle inclusive ranges in Python?

我正在一个领域中工作,在这个领域中,范围通常被包括在内地描述。我有人类可读的描述,如from A to B,它表示包含两个端点的范围,例如from 2 to 4表示2, 3, 4

在Python代码中使用这些范围的最佳方法是什么?以下代码用于生成整数的包含范围,但我还需要执行包含切片操作:

1
2
def inclusive_range(start, stop, step):
    return range(start, (stop + 1) if step >= 0 else (stop - 1), step)

我看到的唯一完整的解决方案是每次使用range或slice符号(如range(A, B + 1)l[A:B+1]range(B, A - 1, -1))时都显式使用+ 1- 1。这种重复真的是处理包含范围的最佳方法吗?

编辑:感谢L3Viathan的回答。写一个inclusive_slice函数来补充inclusive_range当然是一种选择,尽管我可能会写如下:

1
2
3
def inclusive_slice(start, stop, step):
    ...
    return slice(start, (stop + 1) if step >= 0 else (stop - 1), step)

这里的...表示处理负索引的代码,这在与切片一起使用时并不简单——例如,注意,如果slice_to == -1使用,l3viathan函数会给出错误的结果。

然而,一个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 == -1B == None。我们能减少使用这个函数的尴尬吗?

编辑3:我一直在考虑如何改进用法语法,目前它看起来像l[inclusive_slice(1, 5, 2)]。特别是,如果创建一个包含切片类似于标准切片语法,那将是很好的。为了实现这一点,可以使用一个函数inclusive代替inclusive_slice(start, stop, step),该函数将一个切片作为参数。inclusive的理想用法语法是行1

1
2
3
4
5
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不允许这样做,它只允许在[]中使用:语法。因此,必须使用语法23调用inclusive(其中s_的行为与numpy提供的版本类似)。

其他可能的方法是使inclusive成为一个带有__getitem__的对象,允许语法4,或仅将inclusive应用于切片的stop参数,如语法5。不幸的是,我不相信后者能奏效,因为inclusive需要了解step的价值。

在可使用的语法中(原始的l[inclusive_slice(1, 5, 2)],加上234哪一种最适合使用?还是有其他更好的选择?

最后编辑:谢谢大家的回复和评论,这很有意思。我一直很喜欢Python的"单向"哲学,但这个问题是由Python的"单向"和问题域禁止的"单向"之间的冲突造成的。在语言设计方面,我对Timtowtdi有一定的了解。

因为我给出了第一个和最高的投票结果,我把奖金授予了L3Viathan。


为包含切片编写一个额外的函数,并使用该函数而不是切片。虽然有可能,例如子类列表和实现对一个slice对象作出反应的__getitem__,但我建议不要这样做,因为您的代码的行为将与对除您以外的任何人的期望相反,而且可能在一年内对您也是如此。

inclusive_slice可以是这样的:

1
2
3
4
5
6
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])并做出很好的评价。


因为在python中,结束索引总是排他的,所以值得考虑在内部始终使用"python约定"值。这样,您就可以避免在代码中混淆这两者。

只有通过专用的转换子例程处理"外部表示法":

1
2
3
4
5
6
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)

或者,您可以用真正的匈牙利符号标记保存"异常"表示的变量。


如果您不想指定步骤的大小,而是要指定步骤的数量,那么可以选择使用numpy.linspace,其中包括起点和终点。

1
2
3
4
import numpy as np

np.linspace(0,5,4)
# array([ 0.        ,  1.66666667,  3.33333333,  5.        ])


如果不编写自己的类,函数似乎是前进的道路。我最多能想到的不是存储实际列表,而是返回您关心的范围内的生成器。既然我们现在讨论的是用法语法-下面是您可以做的

1
2
3
4
5
6
7
8
9
10
11
12
13
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

然后用作:

1
2
list(closed_range('1:5:2'))
[1,3,5]

当然,如果其他人要使用这个函数,您还需要检查是否有其他形式的错误输入。


我认为标准答案是只要在任何需要的地方使用+1或-1。

您不希望全局更改切片的理解方式(这将破坏大量代码),但另一种解决方案是为希望切片包含其中的对象构建类层次结构。例如,对于list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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,但对任何SequenceMutableSequence都有效。)


与创建非常规的API或扩展数据类型(如list)不同,最好是在内置的Slice上创建一个Slice函数包装器,以便您可以在任何需要切片的地方传递它。对于某些特殊情况,python支持这种方法,并且您所拥有的这种情况对于这种特殊情况是可以保证的。例如,一个包含的切片

1
2
3
4
def islice(start, stop = None, step = None):
    if stop is not None: stop += 1
    if stop == 0: stop = None
    return slice(start, stop, step)

你可以把它用于任何序列类型

1
2
3
4
5
6
>>> 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的包含范围来补充包含切片(用ops行编写)。

1
2
def irange(start, stop, step):
    return range(start, (stop + 1) if step >= 0 else (stop - 1), step)


关注您对最佳语法的请求,目标定位是什么?

1
l[1:UpThrough(5):2]

您可以使用__index__方法来实现这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

现在您甚至不需要专门的列表类(也不需要修改全局定义:

1
2
3
>>> l = [1,2,3,4]
>>> l[1:UpThrough(2)]
[2,3]

如果你用得多,你可以用较短的名字来命名upIncldownIncl甚至InInRev

您还可以构建这些类,这样,除了在slice中使用外,它们与实际索引类似:

1
2
def __int__(self):
    return self.stop


这是困难的,也许是不明智的超载这样的基本概念。对于新的包含列表类,len(l[a:b])在b-a+1中可能导致混乱。为了保持自然的python感觉,同时以基本样式提供可读性,只需定义:

1
2
3
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逻辑:

1
2
3
4
5
>>>>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


本来是要评论的,但写代码作为答案比较容易,所以…

我不会写一个重新定义切片的类,除非它非常清楚。我有一个用位切片表示int的类。在我的上下文中,"4:2"是非常明确的包含性,in t s对于切片没有任何用处,所以它(几乎)是可以接受的(imho,有些人会不同意)。

对于列表,您有这样的案例

1
2
list1 = [1,2,3,4,5]
list2 = InclusiveList([1,2,3,4,5])

稍后在你的代码中

1
if list1[4:2] == test_list or list2[4:2] == test_list:

这是一个很容易犯的错误,因为列表已经有了明确的用法。它们看起来是相同的,但行为却不同,因此调试时会非常混乱,特别是如果您没有编写它。

这并不意味着你完全迷路了…切片很方便,但毕竟它只是一个函数。您可以将该函数添加到类似的任何内容中,因此这可能是一种更简单的方法:

1
2
3
4
5
6
7
8
9
10
11
12
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]更难。实现这一点的唯一方法是将不同的内容传递给切片,这可能会引起不同的兴趣,这样用户就可以在阅读它时知道他们做了什么,而且它仍然很简单。

一种可能性…浮点数。它们是不同的,所以你可以看到它们,它们并不比"简单"语法难多少。它不是内置的,所以仍然有一些"魔法"的参与,但就句法而言,它并不坏……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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应该足以告诉任何一个python程序员"嘿,那里发生了不寻常的事情"…不一定是发生了什么,但至少不奇怪它的行为"怪异"。

请注意,列表中没有唯一的内容…您可以轻松地编写一个可以为任何类执行此操作的修饰符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

1
2
3
@inclusiveclass
class inclusivelist(list):
    pass

不过,第一种形式可能更有用。