将切片转换为范围

38
我正在使用Python 3.3。我想获取一个“slice”对象并使用它来创建一个新的“range”对象。
大致如下:
>>> class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.start, item.stop, item.step))

>>> a = A()
>>> a[1:5:2] # works fine
[1, 3]
>>> a[1:5] # won't work :(
Traceback (most recent call last):
  File "<pyshell#18>", line 1, in <module>
    a[1:5] # won't work :(
  File "<pyshell#9>", line 4, in __getitem__
    return list(range(item.start, item.stop, item.step))
TypeError: 'NoneType' object cannot be interpreted as an integer

很明显,这里的问题是 - range 不接受 None 作为一个值。
>>> range(1, 5, None)
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in <module>
    range(1, 5, None)
TypeError: 'NoneType' object cannot be interpreted as an integer

但是对我来说不明显的是解决方案。如何调用range以便在任何情况下都能正常工作呢?我正在寻找一种优雅的Python方式来完成这件事。


3
在Python 3中,您可以切片一个range对象以获得一个新的range对象,这有帮助吗? - asmeurer
4
对于那些寻求简单和更一般性答案的人,可以参考下面Larby Knossos的回答:range(item.start or 0, item.stop or len(self), item.step or 1)。如果没有定义__len__,则根据需要替换len(self) - lehiester
7个回答

25

有一种更简单的方法可以做到这一点(至少在3.4中,我目前没有3.3版本,并且在更新日志中也没有看到它)。

假设你的类已经有了一个已知长度,你只需要切片该长度的范围即可:

>>> range(10)[1:5:2]
range(1, 5, 2)
>>> list(range(10)[1:5:2])
[1, 3]

如果您不事先知道长度,则必须执行以下操作:

>>> class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.stop)[item])
>>> a = A()
>>> a[1:5:2]
[1, 3]
>>> a[1:5]
[1, 2, 3, 4]

2
这很棒。我想要注意的一件事是list(range(10)[0:20])不会抛出错误,这可能会有问题! - taper
3
slice(0, None, 1)的情况下会出现TypeError: 'NoneType' object cannot be interpreted as an integer的错误。你需要检查是否为None并处理这种情况。glglgl建议使用itertools.count() - Mathieu CAROFF
@MathieuCAROFF 兄弟,最初提问的人说他想要一个范围(range),而不是其他可迭代对象。他从未表达过希望能够处理没有结束点的切片的愿望。如果这是你想要的,那么请自己提出问题,而不是在评论中询问。 - CrazyCasta
抱歉,这只是我的个人观点,我认为这是最美的解决方案,但这是唯一的缺点。 - Mathieu CAROFF
此外,所有其他答案都非常错误(截至2019年1月6日)。 - Mathieu CAROFF
一个非常好的答案 :) - Vaidøtas I.

18

尝试

class A:
    def __getitem__(self, item):
        ifnone = lambda a, b: b if a is None else a
        if isinstance(item, slice):
            if item.stop is None:
                # do something with itertools.count()
            else:
                return list(range(ifnone(item.start, 0), item.stop, ifnone(item.step, 1)))
        else:
            return item

如果 .start.step 值为 None,则此方法会适当地重新解释它们。


还有一种选项是使用切片的 .indices() 方法。它接收元素数量作为参数,并将 None 重新解释为相应的值,并将负值转换为给定长度参数周围的值。

>>> a=slice(None, None, None)
>>> a.indices(1)
(0, 1, 1)
>>> a.indices(10)
(0, 10, 1)
>>> a=slice(None, -5, None)
>>> a.indices(100)
(0, 95, 1)

这取决于您打算如何使用负索引...


2
你也可以这样写:list(range(item.start or 0, item.stop, item.step or 1)) - LoveToCode
2
@LoveToCode 我认为这两种方法略有不同,也许后者并不是最佳实践。ifnone() 函数专门测试值是否为 None,而你的方法则替换任何被解释为 False 的值。 例如,如果你将 0 放入步骤中,使用 ifnone() 函数会保持不变,而 range() 函数会引发错误,这可能是正确的行为。但如果使用 item.step 或 1,则会将 0 替换为 item.step。 - 4xel
slice的.indices()方法正是我正在寻找的。谢谢! - Arty
如果 item.stop 为 None,为什么不把 stop 设置为长度? - Ger

11

问题:

一个切片由startstopstep参数组成,可以使用切片符号slice 内置函数创建。其中任意(或全部)startstopstep参数都可以是None

# valid
sliceable[None:None:None]

# also valid
cut = slice(None, None, None)
sliceable[cut]

然而,正如原问题所指出的,range函数不接受None参数。你可以通过各种方式解决这个问题...

解决方案

使用条件逻辑:

if item.start None:
    return list(range(item.start, item.stop))
return list(range(item.start, item.stop, item.step))

...由于任何或所有参数都可能为 None ,因此可能会变得不必要复杂。

使用条件变量:

start = item.start if item.start is None else 0
step = item.step if item.step is None else 1
return list(range(item.start, item.stop, item.step))

...这很明确,但有点冗长。

在语句中直接使用条件语句:

return list(range(item.start if item.start else 0, item.stop, item.step if item.step else 1))

...这也过于冗长。

使用函数或lambda语句:

ifnone = lambda a, b: b if a is None else a
range(ifnone(item.start, 0), item.stop, ifnone(item.step, 1)

...这可能很难理解。

使用'或':

return list(range(item.start or 0, item.stop or len(self), item.step or 1))

我认为使用or来分配合理的默认值是最简单的。它明确、简单、清晰、简洁。

为了完善实现,您还应通过检查isinstance(item,numbers.Integral)(请参见int vs numbers.Integral)来处理整数索引(intlong等)。

定义__len__以允许使用len(self)作为默认的停止值。

最后,对于无效的索引(例如字符串等),引发适当的TypeError

class A:
    def __len__(self):
        return 0

    def __getitem__(self, item):
        if isinstance(item, numbers.Integral):  # item is an integer
            return item
        if isinstance(item, slice):  # item is a slice
            return list(range(item.start or 0, item.stop or len(self), item.step or 1))
        else:  # invalid index type
            raise TypeError('{cls} indices must be integers or slices, not {idx}'.format(
                cls=type(self).__name__,
                idx=type(item).__name__,
            ))

1
/!\ 如果 item.step 为 0。 - Mathieu CAROFF
1
“[或]是明确的”。并不是所有的值都被隐式地解释为False,这可能会导致问题,如果你真的只想测试None。在那个例子中,将切片步长设置为0应该会引发错误,但实际上没有。更糟糕的是,你不能使用0作为item.stop的值,它会被长度self覆盖。 - 4xel
非常好的答案,谢谢。但是它不能用于负数切片,对吗? - Carol Eisen
“With conditional variables:” 的答案是错误的。应该是 item.start if item.start is not None else 0 - Ger

5

这里所有其他的答案都完全错了。

通常情况下,您根本无法将一个slice转换为一个range

信息不足。 您需要知道要切片的列表(或其他序列类型)的长度。

一旦您知道了长度,您可以在Python 3中使用slice.indices()轻松创建一个范围。

按照您提供的示例:

class A:
    def __init__(self, mylist):
        self.mylist = mylist

    def __getitem__(self, item):
        if isinstance(item, slice):
            mylen = len(self.mylist)
            return list(range(*item.indices(mylen)))

mylist = [1, 2, 'abc', 'def', 3, 4, None, -1]
a = A(mylist)
a[1:5]  # produces [1, 2, 3, 4]

1

我会特别处理item.step is None分支:

def __getitem__(self, item):
    if isinstance(item, slice):
        if item.step is None:
            return list(range(item.start, item.stop))
        return list(range(item.start, item.stop, item.step))

而且您将处理需要正确倒数的范围。


1
这很酷,但 item.start 也可能是 None,我认为代码中太多的 if 可能会让它看起来相当丑陋,所以我在另一个答案中建议使用的 indices 方法。 - slallum
@slallum:start = item.start if item.start is not None else 0 这个条件表达式可能足够好用(然后使用 range(start, item.stop ..)。 - Martijn Pieters

0
在你的最后一个例子中,a[1:5]item.step == None,你试图执行range(1, 5, None),这当然会导致错误。快速修复的方法是:
class A:
    def __getitem__(self, item):
        if isinstance(item, slice):
            return list(range(item.start, item.stop, item.step if item.step else 1)) #Changed line!

但这只是为了向你展示问题,不是最佳方法。


1
如果 stop 小于 start 怎么办?那么步长应该是 -1 - Martijn Pieters
1
@MartijnPieters - 那么它就不会返回任何东西,就像列表中的切片操作一样。 - slallum
@slallum:但这是一个范围。列表可能会将切片解释为空列表,但在范围内,停止低于开始会导致自动的“-1”步骤。 - Martijn Pieters
1
@MartijnPieters - 你确定吗?我在python3.3和python2.7中都试过 list(range(7,2)),结果都返回一个空列表。 - slallum
2
@slallum:哇,不确定我是怎么想到它会自动处理正确的“步长”的。这说明我很少使用负范围.. - Martijn Pieters

0

这样的东西怎么样?

>>> class A:
def __getitem__(self, item):
    if isinstance(item, slice):
        return list(range(item.start, item.stop, item.step if item.step else 1))

>>> a = A()
>>> a[1:5:2] # works fine
[1, 3]
>>> a[1:5] # works as well :)
[1, 2, 3, 4]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接