How should I handle inclusive ranges in Python?
我正在一个领域中工作,在这个领域中,范围通常被包括在内地描述。我有人类可读的描述,如
在Python代码中使用这些范围的最佳方法是什么?以下代码用于生成整数的包含范围,但我还需要执行包含切片操作:
1 2 | def inclusive_range(start, stop, step): return range(start, (stop + 1) if step >= 0 else (stop - 1), step) |
我看到的唯一完整的解决方案是每次使用
编辑:感谢L3Viathan的回答。写一个
1 2 3 | def inclusive_slice(start, stop, step): ... return slice(start, (stop + 1) if step >= 0 else (stop - 1), step) |
号
这里的
然而,一个
有没有更好的方法来处理包含范围?
编辑2:感谢您提供新的答案。我同意弗朗西斯和科利的观点,改变切片操作的含义,无论是全局的还是对某些类的,都会导致严重的混乱。因此,我现在倾向于编写一个
为了回答我之前编辑的问题,我得出的结论是,使用这种函数(如
编辑3:我一直在考虑如何改进用法语法,目前它看起来像
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不允许这样做,它只允许在
其他可能的方法是使
在可使用的语法中(原始的
最后编辑:谢谢大家的回复和评论,这很有意思。我一直很喜欢Python的"单向"哲学,但这个问题是由Python的"单向"和问题域禁止的"单向"之间的冲突造成的。在语言设计方面,我对Timtowtdi有一定的了解。
因为我给出了第一个和最高的投票结果,我把奖金授予了L3Viathan。
为包含切片编写一个额外的函数,并使用该函数而不是切片。虽然有可能,例如子类列表和实现对一个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] |
我个人要做的就是使用你提到的"完整"的解决方案(
因为在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) |
。
或者,您可以用真正的匈牙利符号标记保存"异常"表示的变量。
如果您不想指定步骤的大小,而是要指定步骤的数量,那么可以选择使用
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。
您不希望全局更改切片的理解方式(这将破坏大量代码),但另一种解决方案是为希望切片包含其中的对象构建类层次结构。例如,对于
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] |
号
当然,您希望对
(我用的是
与创建非常规的API或扩展数据类型(如list)不同,最好是在内置的
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) |
最后,您还可以创建一个名为
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] |
您可以使用
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] |
。
如果你用得多,你可以用较短的名字来命名
您还可以构建这些类,这样,除了在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 |
不过,第一种形式可能更有用。