我应该如何处理 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 == -1
和B == 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
必须使用语法2
或3
调用(其中s_
的作用类似于the version provided by numpy)。
其他可能性是使用__getitem__
将inclusive
变成一个对象,允许语法4
,或者仅将inclusive
应用于切片的stop
参数,如语法5
。不幸的是,我不相信后者可以工作,因为inclusive
需要了解step
值。
在可行的语法(原来的l[inclusive_slice(1, 5, 2)]
,加上2
、3
和4
)中,哪个最好用?还是有其他更好的选择?
最终编辑:感谢大家的回复和 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
,但它适用于任何Sequence
或MutableSequence
。)
【讨论】:
我认为使用这样的类比它的帮助更令人困惑。例如,我可能期望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]
如果您经常使用,您可以使用较短的名称upIncl
、downIncl
甚至
In
和 InRev
。
您还可以构建这些类,这样,除了在 slice 中使用之外,它们 表现得像实际的索引:
def __int__(self):
return self.stop
【讨论】:
您将UpThrough
和DownThrough
定义为类而不是简单函数有什么原因吗?
原因将是使用__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】:此解决方案适用于整数以及负数和浮点数,使用 math
和 numpy
:
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 中的包含范围?的主要内容,如果未能解决你的问题,请参考以下文章