[Python系列] Python函数及函数式编程(八)

第一节 切片

在Python中,最基本的数据结构是序列(sequence)。我们学过的列表,元祖,甚至字符串都属于序列的一种。

序列中每个元素都有编号,我们称之为索引。在之前,我们对序列的操作主要依靠的就是索引。今天我们来认识一种更为方便的操作方式-切片

切片

我们在取列表的元素值时,通常使用索引获取单个元素

str = "Hello World"
print(str[6])
# W

但是如果我们想要完整的将World取出来,虽然有办法,但是可能比较麻烦一些。

>>> [str[6],str[7],str[8],str[9],str[10]]

但是如果我们需要取前n个或者后n个呢?必须用循环吗?在这时我们要记住,简洁胜于复杂

切片可以帮助我们提取序列的一部分。我们只需要用两个索引即可

str = "Hello World"
print(str[6:10])
# Worl

这里需要注意,虽然d的索引是10,但是切片中的两个索引并不全是"包含"关系的范围,索引6~10只能取出部分内容。换句话说,切片需要通过两个索引来指定边界,其中

  • 第一个索引包含第一个元素的编号
  • 第二个索引表示的是切剩下的第一个元素的编号

切片让我们操作序列更为简单,它还有着更为绝妙的简写方式

切片的简写

假设我们要访问一个数字列表的最后三个元素,该怎么做呢?使用切片的话当然会变的很方便:

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[7:10])
# [8,9,10]

很好,这里我们运用了切片的基本概念,第二个索引写为10而不是9。但是相信大家也从头开始数了好久。

更快捷的方法,我们何不从后往前数?

最后三个元素,很明显是从倒数第三个开始。那么我们切片的起点就有了,-3!那么终点是哪个元素呢?

我们依次做尝试

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[-3:-1])
# [8,9]

这样无法包括所有的元素。因为到了倒数第一个,截取"终止"了。

这时我们会很自然的想到,如果截取的终值是0呢

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[-3:0])
# []

结果是一个空列表。但是原因也很清晰:索引-3在索引0后面,但是在做切片的时候,放在了起始位置

在做切片时,如果第一个索引指定的元素位于第二个索引指定的元素后面,结果就为空序列。

在这时候,我们就可以使用简写了:如果切片结束于序列末尾,可以省略最后一个索引

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[-3:])
# [8, 9, 10]

同样,如果切片开始于序列的开头,可省略第一个索引

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[:3])
# [1,2,3]

实际上,复制整个序列,可以将两个索引都省略

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[:])
# [1,2,3,4,5,6,7,8,9,10]

注意:切片复制整个序列,不同于直接赋值操作!我们将在接下来的深拷贝和浅拷贝知识中进行讲解。

步长

当我们执行切片操作时。通常默认对在两个索引之间的元素进行逐一复制。其实我们不仅可以逐一复制,还可以调整步长。默认情况下步长为1。

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[::1])
# [1,2,3,4,5,6,7,8,9,10]

这里我们通过切片的第三个参数,显式的指定了步长。如果我们想每隔2个选一个呢?步长明显为3

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[::3])
# [1, 4, 7, 10]

当然步长不能为0,否则无法向前移动,但可以是负数,表示从右向左提取元素

在提取元素的顺序改变之后,我们两个索引的前后顺序需要做出相应改变

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[1:8:-2])
# []

上述代码为何结果为空列表呢?很简单,从右往左读取元素,我们的索引读取顺序也发生了改变,应该为后面的元素作为开始,前面的元素作为终点。上述的书写方式符合在做切片时,如果第一个索引指定的元素位于第二个索引指定的元素后面,结果就为空序列这个原则,所以我们得到了空序列。

正确的写法如下

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[8:1:-2])
# [9, 7, 5, 3]

序列的结果也是相反的。

学到这里,我们应该学会了如何将一个序列中的元素位置颠倒了。

在这里我们需要注意一下两条不易懂的语句

numbers = [1,2,3,4,5,6,7,8,9,10]
print(numbers[5::-2])
# [6,4,2]
print(numbers[:5:-2])
# [10,8]

我们简单分析一下,对于第一个语句的执行,我们先排好顺序。

numbers = [1,2,3,4,5,6,7,8,9,10]
	                   ^
	                   numbers[5]

既然是逆序读取,并且 numbers[5] 为起点。我们继续行走,一直可以走到原列表的开头,而开头是可以省略的。因此该切片的含义应该是逆序读取到原列表的开始,且不长为2.
					  

对于第二个语句的执行,更为复杂。我们同样排好顺序

numbers = [1,2,3,4,5,6,7,8,9,10]
	                   ^
	                   numbers[5]
	                   
逆序读取,只规定了终点,没有指定起点。因此起点默认是原列表的末尾。从10~6隔一个元素取一个元素
					  

深拷贝和浅拷贝

上文中我们提到过切片复制和赋值是不一样的,具体哪里不一样呢?在讲解深拷贝和浅拷贝之前,我们先探究两者的不同

a = [1,2,3]
b = a

上述的 a 和 b 相同吗?我们第一反应是相同,验证一下:

print(a is b)
# True

这也证明a 变量和 b 变量id相同

print(id(a)) # 4403429192
print(id(b)) # 4403429192

这表明,我们在赋值时,并没有创建一个新的列表,而是将原列表的地址给了 b ,a 和 b本质上存储的是同一个地址,即指向同一个列表,如图:

在这里插入图片描述

很显然,如果我们通过a改变列表的内容,b内容一定会发生改变

a.append(4)
print(a) # [1,2,3,4]
print(b) # [1,2,3,4]

那么,切片和赋值有什么不同呢?我们来做同样的实验:

a = [1,2,3]
b = a[:]

print(a is b)
# False

很明显,a 和 b 不再相同了。我们再次验证一下 变量 a 和 变量 b 的 id

print(id(a)) # 4549197640
print(id(b)) # 4549197704

两者明显不同。 我们推测出现在 a 和 b 指向的已经是不同的两个列表了-起码这两个列表所处的内存中的位置不同

a.append(4)
print(a) # [1,2,3,4]
print(b) # [1,2,3]

其实,在 a 做切片时,并不是将 a 所指向的列表的地址赋值给了 b ,而是创建了一个新的列表,将 a 指向的列表中的元素依次赋值给新列表,并把新列表的地址给了 b ,如下图:

在这里插入图片描述

这个过程其实就是拷贝,我们获得了 a 的一个副本。

那何为浅拷贝,何为深拷贝呢?我们再做拓展,将 a 的值略做变化

a = [1,[2,3]]
b = a[:]

b 是由 a 拷贝而来的,但是 a 有了一些变化,有一个元素为列表,现在我们验证一下 b 是否是 a 的一个完整的拷贝。

a = [1,[2,3]]
b = a[:]

print(id(a)) # 4467187592
print(id(b)) # 4469101576

a.append(4)
print(a) # [1, [2, 3], 4]
print(b) # [1, [2, 3]]

看上去 b 是 a 的一个副本。我们接着对 a[1]进行操作

a[1].append(4)
print(a) # [1, [2, 3, 4]]
print(b) # [1, [2, 3, 4]]

这是为什么呢?

其实,从a[1]b[1]的 id 值中我们能够初见端倪。

print(id(a[1])) # 4364521288
print(id(b[1])) # 4364521288

在 b 和 a 中的 列表元素竟然是同一个!

切片其实只对列表的第一层进行了复制,即是,在复制a[1]时,仅仅将a[1] 的地址给了b[1],而没有进一步的深入到a[1]内部进行复制。我们把这种拷贝的方式称为浅拷贝,如图:

在这里插入图片描述

换言之,深拷贝和浅拷贝的概念如下:

  • 浅拷贝:只拷贝最外层对象。
  • 深拷贝:不仅拷贝最外层,还将每一层中的对象进行拷贝。

那么深拷贝怎么实现呢?可以使用递归!这里我直接给答案,同学们自己研究一下:

def my_deepcopy(arr):
    rt = [];
    for item in arr:
        if isinstance(item,list):
            rt.append(my_deepcopy(item))
        else:
            rt.append(item)
    return rt

小结

  1. 切片的第一个索引位于第二个索引指定的元素之后,结果为空序列
  2. 切片结束于序列末尾,省略最后一个索引
  3. 切片开始于序列开头,省略第一个索引
  4. 复制整个序列,可以省略两个索引
  5. 步长不能为0,否则无法向前移动,但可以是负数,表示从右向左提取元素
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值