Python的切片表示法是如何工作的?也就是说:当我编写[x:y:z]、a[:]、a]::2]等代码时,我如何理解哪些元素最终会出现在切片中?请在适当的地方附上参考资料。
另请参见:为什么切片和范围上限是互斥的?
Python的切片表示法是如何工作的?也就是说:当我编写[x:y:z]、a[:]、a]::2]等代码时,我如何理解哪些元素最终会出现在切片中?请在适当的地方附上参考资料。
另请参见:为什么切片和范围上限是互斥的?
当前回答
已经有很多答案了,但我想添加一个性能比较
~$ python3.8 -m timeit -s 'fun = "this is fun;slicer = slice(0, 3)"' "fun_slice = fun[slicer]"
10000000 loops, best of 5: 29.8 nsec per loop
~$ python3.8 -m timeit -s 'fun = "this is fun"' "fun_slice = fun[0:3]"
10000000 loops, best of 5: 37.9 nsec per loop
~$ python3.8 -m timeit -s 'fun = "this is fun"' "fun_slice = fun[slice(0, 3)]"
5000000 loops, best of 5: 68.7 nsec per loop
~$ python3.8 -m timeit -s 'fun = "this is fun"' "slicer = slice(0, 3)"
5000000 loops, best of 5: 42.8 nsec per loop
因此,如果您重复使用同一个切片,使用切片对象将有益并提高可读性。然而,如果您只进行了几次切片,则应首选[:]表示法。
其他回答
在我看来,如果您按照以下方式查看Python字符串切片表示法(继续阅读),您将更好地理解和记忆它。
让我们使用以下字符串。。。
azString = "abcdefghijklmnopqrstuvwxyz"
对于不知道的人,可以使用符号azString[x:y]从azString创建任何子字符串
来自其他编程语言,这是常识受到损害的时候。x和y是什么?
为了寻找一种记忆技巧,我不得不坐下来运行了几个场景,帮助我记住x和y是什么,并帮助我在第一次尝试时正确地切分字符串。
我的结论是,x和y应该被视为围绕我们想要增加的字符串的边界索引。因此,我们应该将表达式视为azString[index1,index2],或者更清晰地视为azString[index_of_first_charactere,index_after_the_last_character]。
这是一个可视化的例子。。。
Letters a b c d e f g h i j ...
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
┊ ┊
Indexes 0 1 2 3 4 5 6 7 8 9 ...
┊ ┊
cdefgh index1 index2
因此,您所要做的就是将index1和index2设置为所需子字符串周围的值。例如,要获得子字符串“cdefgh”,可以使用azString[2:8],因为“c”左侧的索引是2,而“h”右侧的索引是8。
请记住,我们正在设置边界。这些边界是可以放置一些括号的位置,括号将像这样围绕子字符串。。。
a b[c d e f g h]i j公司
这个技巧一直有效,而且很容易记住。
上面的答案不讨论切片分配。为了理解切片分配,可以在ASCII艺术中添加另一个概念:
+---+---+---+---+---+---+
| P | y | t | h | o | n |
+---+---+---+---+---+---+
Slice position: 0 1 2 3 4 5 6
Index position: 0 1 2 3 4 5
>>> p = ['P','y','t','h','o','n']
# Why the two sets of numbers:
# indexing gives items, not lists
>>> p[0]
'P'
>>> p[5]
'n'
# Slicing gives lists
>>> p[0:1]
['P']
>>> p[0:2]
['P','y']
一种启发式方法是,对于从零到n的切片,思考:“零是开始,从开始开始,在列表中取n个项目”。
>>> p[5] # the last of six items, indexed from zero
'n'
>>> p[0:5] # does NOT include the last item!
['P','y','t','h','o']
>>> p[0:6] # not p[0:5]!!!
['P','y','t','h','o','n']
另一种启发式方法是,“对于任何一个切片,用零替换开头,应用前面的启发式方法获得列表的结尾,然后将第一个数字向后计数,以从开头删除项目”
>>> p[0:4] # Start at the beginning and count out 4 items
['P','y','t','h']
>>> p[1:4] # Take one item off the front
['y','t','h']
>>> p[2:4] # Take two items off the front
['t','h']
# etc.
切片分配的第一个规则是,由于切片返回一个列表,所以切片分配需要一个列表(或其他可迭代的):
>>> p[2:3]
['t']
>>> p[2:3] = ['T']
>>> p
['P','y','T','h','o','n']
>>> p[2:3] = 't'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
切片分配的第二个规则(您也可以在上面看到)是,无论切片索引返回列表的哪个部分,都是由切片分配更改的相同部分:
>>> p[2:4]
['T','h']
>>> p[2:4] = ['t','r']
>>> p
['P','y','t','r','o','n']
切片分配的第三条规则是,分配的列表(可迭代)不必具有相同的长度;索引切片被简单地切片,并被分配的任何内容整体替换:
>>> p = ['P','y','t','h','o','n'] # Start over
>>> p[2:4] = ['s','p','a','m']
>>> p
['P','y','s','p','a','m','o','n']
最难习惯的部分是分配给空切片。使用启发式1和2,很容易让你的头脑围绕空切片进行索引:
>>> p = ['P','y','t','h','o','n']
>>> p[0:4]
['P','y','t','h']
>>> p[1:4]
['y','t','h']
>>> p[2:4]
['t','h']
>>> p[3:4]
['h']
>>> p[4:4]
[]
然后,一旦您看到了这一点,将切片分配给空切片也是有意义的:
>>> p = ['P','y','t','h','o','n']
>>> p[2:4] = ['x','y'] # Assigned list is same length as slice
>>> p
['P','y','x','y','o','n'] # Result is same length
>>> p = ['P','y','t','h','o','n']
>>> p[3:4] = ['x','y'] # Assigned list is longer than slice
>>> p
['P','y','t','x','y','o','n'] # The result is longer
>>> p = ['P','y','t','h','o','n']
>>> p[4:4] = ['x','y']
>>> p
['P','y','t','h','x','y','o','n'] # The result is longer still
请注意,因为我们没有更改切片的第二个编号(4),所以插入的项目总是紧靠“o”堆叠,即使我们分配给空切片也是如此。因此,空切片分配的位置是非空切片分配位置的逻辑扩展。
稍微后退一点,当你继续进行我们的切片开始计数过程时会发生什么?
>>> p = ['P','y','t','h','o','n']
>>> p[0:4]
['P','y','t','h']
>>> p[1:4]
['y','t','h']
>>> p[2:4]
['t','h']
>>> p[3:4]
['h']
>>> p[4:4]
[]
>>> p[5:4]
[]
>>> p[6:4]
[]
通过切片,一旦你完成,你就完成了;它不会开始向后倾斜。在Python中,除非使用负数明确要求,否则不会获得负的步幅。
>>> p[5:3:-1]
['n','o']
“一旦你完成了,你就完成了”规则会产生一些奇怪的后果:
>>> p[4:4]
[]
>>> p[5:4]
[]
>>> p[6:4]
[]
>>> p[6]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
事实上,与索引相比,Python切片具有奇怪的防错误性:
>>> p[100:200]
[]
>>> p[int(2e99):int(1e99)]
[]
这有时会派上用场,但也会导致一些奇怪的行为:
>>> p
['P', 'y', 't', 'h', 'o', 'n']
>>> p[int(2e99):int(1e99)] = ['p','o','w','e','r']
>>> p
['P', 'y', 't', 'h', 'o', 'n', 'p', 'o', 'w', 'e', 'r']
根据您的应用程序,这可能。。。或者可能不。。。成为你在那里所希望的!
以下是我的原始答案。它对很多人都很有用,所以我不想删除它。
>>> r=[1,2,3,4]
>>> r[1:1]
[]
>>> r[1:1]=[9,8]
>>> r
[1, 9, 8, 2, 3, 4]
>>> r[1:1]=['blah']
>>> r
[1, 'blah', 9, 8, 2, 3, 4]
这也可以澄清切片和索引之间的区别。
Python教程对此进行了讨论(向下滚动一点,直到您了解到关于切片的部分)。
ASCII艺术图也有助于记住切片的工作方式:
+---+---+---+---+---+---+
| P | y | t | h | o | n |
+---+---+---+---+---+---+
0 1 2 3 4 5 6
-6 -5 -4 -3 -2 -1
记住切片工作方式的一种方法是将索引视为字符之间的指针,第一个字符的左边缘编号为0。然后,n个字符串的最后一个字符的右边缘具有索引n。
我有点沮丧,因为找不到一个准确描述切片功能的在线源代码或Python文档。
我接受了Aaron Hall的建议,阅读了CPython源代码的相关部分,并编写了一些Python代码,这些代码执行切片与CPython中的切片类似。我已经用Python 3对整数列表进行了数百万次随机测试。
您可能会发现我的代码中对CPython中相关函数的引用很有用。
def slicer(x, start=None, stop=None, step=None):
""" Return the result of slicing list x.
See the part of list_subscript() in listobject.c that pertains
to when the indexing item is a PySliceObject.
"""
# Handle slicing index values of None, and a step value of 0.
# See PySlice_Unpack() in sliceobject.c, which
# extracts start, stop, step from a PySliceObject.
maxint = 10000000 # A hack to simulate PY_SSIZE_T_MAX
if step is None:
step = 1
elif step == 0:
raise ValueError('slice step cannot be zero')
if start is None:
start = maxint if step < 0 else 0
if stop is None:
stop = -maxint if step < 0 else maxint
# Handle negative slice indexes and bad slice indexes.
# Compute number of elements in the slice as slice_length.
# See PySlice_AdjustIndices() in sliceobject.c
length = len(x)
slice_length = 0
if start < 0:
start += length
if start < 0:
start = -1 if step < 0 else 0
elif start >= length:
start = length - 1 if step < 0 else length
if stop < 0:
stop += length
if stop < 0:
stop = -1 if step < 0 else 0
elif stop > length:
stop = length - 1 if step < 0 else length
if step < 0:
if stop < start:
slice_length = (start - stop - 1) // (-step) + 1
else:
if start < stop:
slice_length = (stop - start - 1) // step + 1
# Cases of step = 1 and step != 1 are treated separately
if slice_length <= 0:
return []
elif step == 1:
# See list_slice() in listobject.c
result = []
for i in range(stop - start):
result.append(x[i+start])
return result
else:
result = []
cur = start
for i in range(slice_length):
result.append(x[cur])
cur += step
return result
我发现更容易记住它是如何工作的,然后我可以找出任何特定的开始/停止/步骤组合。
首先了解range()是很有启发性的:
def range(start=0, stop, step=1): # Illegal syntax, but that's the effect
i = start
while (i < stop if step > 0 else i > stop):
yield i
i += step
从起点开始,一步一步递增,不要到达终点。非常简单。
关于消极步骤,需要记住的一点是,停止总是被排除的终点,无论它是高还是低。如果您希望相同的切片以相反的顺序进行,则单独进行反转会更为简单:例如,“abcde”[1:-2][::-1]从左侧切下一个字符,从右侧切下两个字符,然后反转。(另请参见reversed()。)
序列切片是相同的,只是它首先规范了负索引,并且它永远不能超出序列:
TODO:当abs(step)>1时,下面的代码出现了一个错误:“从不超出序列”;我认为我修补了它是正确的,但很难理解。
def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
if start is None:
start = (0 if step > 0 else len(seq)-1)
elif start < 0:
start += len(seq)
if not 0 <= start < len(seq): # clip if still outside bounds
start = (0 if step > 0 else len(seq)-1)
if stop is None:
stop = (len(seq) if step > 0 else -1) # really -1, not last element
elif stop < 0:
stop += len(seq)
for i in range(start, stop, step):
if 0 <= i < len(seq):
yield seq[i]
不要担心“无”的细节——只需记住,省略开始和/或停止总是正确的做法,以提供整个序列。
首先规范化负索引允许开始和/或停止从结尾独立计数:'abcde'[1:-2]=='abcde'[1:3]=='bc',尽管范围(1,-2)==[]。标准化有时被认为是“对长度取模”,但注意它只增加了一次长度:例如,“abcde”[-53:42]只是整个字符串。