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

Posted

技术标签:

【中文标题】我应该如何处理 Python 中的包含范围?【英文标题】:How should I handle inclusive ranges in Python? 【发布时间】:2015-06-18 05:10:37 【问题描述】:

我在一个通常包含范围的领域中工作。我有人类可读的描述,例如 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)

我看到的唯一完整的解决方案是每次使用range 或切片表示法(例如range(A, B + 1)l[A:B+1]range(B, A - 1, -1))时都明确使用+ 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)

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

但是,inclusive_slice 函数似乎很难使用 - l[inclusive_slice(A, B)] 真的比 l[A:B+1] 更好吗?

有没有更好的方法来处理包含范围?

编辑 2:感谢您提供新答案。我同意 Francis 和 Corley 的观点,即改变切片操作的含义,无论是全局还是某些类,都会导致严重的混乱。因此,我现在倾向于编写 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 行:

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 必须使用语法23 调用(其中s_ 的作用类似于the version provided by numpy)。

其他可能性是使用__getitem__inclusive 变成一个对象,允许语法4,或者仅将inclusive 应用于切片的stop 参数,如语法5。不幸的是,我不相信后者可以工作,因为inclusive 需要了解step 值。

在可行的语法(原来的l[inclusive_slice(1, 5, 2)],加上234)中,哪个最好用?还是有其他更好的选择?

最终编辑:感谢大家的回复和 cmets,这非常有趣。我一直是 Python 的“one way to do it”哲学的粉丝,但是这个问题是由 Python 的“one way”和问题域所禁止的“one way”之间的冲突引起的。我对TIMTOWTDI 在语言设计方面肯定获得了一些赞赏。

为了给出第一个和最高投票的答案,我将赏金奖励给 L3viathan。

【问题讨论】:

注意你写的函数不正确。第二个参数stop + step 有可能将包含范围扩大到超出应有的范围。应该是stop + 1。例如range(0, 7, 3)[0, 3, 6] 但你的函数会给出 [0, 3, 6, 9] 范围内的元素在您的域中是否总是整数?我的意思是例如from 2 to 4 可以表示 [2,3,4]2:00,2:01,...,4:00 @qarma - 是的,就像内置的 range() 函数一样,我只需要处理整数范围。 另一种可能性...继承list 并扩展其__getitem__ 功能以处理序列/元组。然后,您可以传入一个范围作为您的提取对象。例如现在,l = [0,1,2,3,4]l[2] 给出 2,但 l[2,3,4] 是一个错误。添加支持以使l[2,3,4] 提供[2,3,4] 不会破坏任何现有功能(我不认为),并且如果有帮助的话,可以让您执行l[inclusive_range(2,4)] 之类的操作。 您在 range 函数中明确表示 step - 这是否意味着您的输入也可以指定一个 step?例如:“从 10 到 20 每 2”表示 [10, 12, 14, 16, 18, 20]?如果是这样的话,如果它说每 3 个从 10 到 20 会发生什么?还是永远不会发生? 【参考方案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])并评论好。

【讨论】:

您可以在此处轻松添加默认 step=1, 参数。 从 a[n:-1] 切片将转换为 a[n:0] ,这意味着完全不同的东西 同样 slice_to 应该能够使用 None 而不会因 TypeError 而崩溃 @wim 不再是一个不错的两行代码,但已修复。 __slice__ 方法的实现是什么意思?你能指出一个文件,我没有找到它。此外,对于负面步骤,您当前的解决方案不起作用,与 OP 的相反。【参考方案2】:

我相信标准答案是在需要的地方使用 +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,但它适用于任何SequenceMutableSequence。)

【讨论】:

我认为使用这样的类比它的帮助更令人困惑。例如,我可能期望InclusiveList(range(11)) 包含 11 好吧 range(11) 不包括 11 并且仅用于初始化列表。我知道这里使用range 会让人感到困惑,我已经更改了示例。【参考方案3】:

如果您不想指定步长而是指定步数,可以选择使用numpy.linspace,其中包括起点和终点

import numpy as np

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

【讨论】:

感谢提醒linspace :但是有没有一个版本可以在给定的范围内返回 整数 递增一?否则,与其他一些答案一样,这个问题需要做很多转换工作。 @javadba linspace(0, 5, 4, dtype="int")array([0, 1, 3, 5])。请注意,这会截断而不是四舍五入,如果您需要四舍五入,请尝试linspace(0, 5, 4).round().astype("int")array([0, 2, 3, 5])【参考方案4】:

由于在 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)

或者,您可以使用true Hungarian notation 标记具有“异常”表示的变量。

【讨论】:

我不同意。因为它今天这样做并不意味着它不应该在未来这样做。许多语言具有包容性和排他性,因为它通常正是您所需要的。 (Ruby & Swift & Perl 立刻浮现在脑海) @uchuugaka 在可预见的未来,这种变化实际上是不可能发生的。 1)Python 开发人员非常注意向后兼容性。除了过渡到下一个主要版本时,他们绝对不会改变这一点。 2)Python 的重点是简单性和可维护性——这将是一个潘多拉的盒子。 3)基本计算概念(冯诺依曼体系结构,二进制系统)表明 0...N-1 索引比 1..N 更方便,几乎没有任何意义。因此,根据 YAGNI 原则,您不必在 Python 解决方案中担心这一点。 为了让字符串愤怒,我建议:gist.github.com/raczben/76cd1229504d82115e6427e00cf4742c【参考方案5】:

本来打算发表评论,但写代码作为答案更容易,所以...

我不会写一个重新定义切片的类,除非它非常清楚。我有一个用位切片表示整数的类。在我的上下文中,'4:2' 显然具有包容性,并且 int 已经没有任何用于切片的用途,因此它(几乎)可以接受(恕我直言,有些人会不同意)。

对于列表,您可能会执行类似的操作

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:

这是一个很容易犯的错误,因为 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 应该足以告诉任何 python 程序员'嘿,那里发生了一些不寻常的事情'......不一定发生了什么,但至少它的行为并不奇怪'奇怪”。

请注意,列表并没有什么独特之处...您可以轻松编写一个可以为任何类执行此操作的装饰器:

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

第一种形式可能更有用。

【讨论】:

【参考方案6】:

不编写自己的类,函数似乎是要走的路。我最多能想到的不是存储实际列表,而是返回您关心的范围的生成器。由于我们现在正在讨论使用语法 - 这是您可以做的事情

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]

当然,如果其他人要使用此功能,您还需要检查其他形式的错误输入。

【讨论】:

【参考方案7】:

重载这些基本概念是困难的,而且可能是不明智的。 使用 b-a+1 中的新包含列表类 len(l[a:b]) 可能会导致混淆。 为了保留自然的 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

【讨论】:

不能正确处理负数【参考方案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]

如果您经常使用,您可以使用较短的名称upIncldownIncl 甚至 InInRev

您还可以构建这些类,这样,除了在 slice 中使用之外,它们 表现得像实际的索引:

def __int__(self):
    return self.stop

【讨论】:

您将UpThroughDownThrough 定义为类而不是简单函数有什么原因吗? 原因将是使用__int__ 和/或__float__,以便int(UpThrough(5)) == 5 但在切片中生成正确的索引。您还可以添加算术运算符。【参考方案9】:

与其创建非传统 API 或扩展数据类型(如列表),不如创建一个 Slice 函数作为内置 slice 的包装器,这样您就可以在任何地方传递它,a切片是必需的。 对于某些例外情况,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)

您可以将它用于任何sequence types

>>> 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 从而使其具有包容性

【讨论】:

【参考方案11】:

也许inclusive 包会很有帮助。

【讨论】:

【参考方案12】:

此解决方案适用于整数以及负数和浮点数,使用 mathnumpy

def irange(start, stop=None, step=1):
    if stop is None:
        start, stop = 0, start
    return list(start + numpy.arange(floor((stop - start) / step) + 1) * step)

【讨论】:

以上是关于我应该如何处理 Python 中的包含范围?的主要内容,如果未能解决你的问题,请参考以下文章

我应该如何处理逻辑编程中的重复更新?

我应该如何处理数据库项目中的用户?

python中的all()如何处理空列表

python:如何处理if语句中的NaN数据[重复]

如何处理python系列中的多种日期字符串格式

如何处理python scikit NMF中的缺失值