优秀文案,第1时间送达!
众所周知,咱们能够经过索引值(或叫作下标)来查询序列类型(如字符串、列表、元组…)中的单个元素,那样,倘若要获取一个索引区间的元素该怎么办呢?
切片(slice)便是一种截取索引片段的技术,借助切片技术,咱们能够非常灵活地处理序列类型的对象。一般来讲,切片的功效便是截取序列对象,然而,针对非序列对象,咱们是不是有办法做到切片操作呢?在运用切片的过程中,有什么要点值得注重,又有什么底层原理值得关注呢?本文将重点跟大众一块来探讨这些内容,期盼我能与你一起学习进步。
1、切片的基本用法
列表是 Python 中极为基本且重要的一种数据结构,亦是最能发挥切片的用处的一种数据结构,因此在前两节,我将以列表为例介绍切片的有些平常用法。
首要是切片的书写形式:[i : i+n : m] ;其中,i 是切片的初始索引值,为列表首位时可省略;i+n 是切片的结束位置,为列表末位时可省略;m 能够不供给,默认值是1,不准许为0,当m为负数时,列表翻转。重视:这些值都能够大于列表长度,不会报越界。
切片的基本含义是:从序列的第i位索导致,向右取到后n位元素为止,按m间隔过滤。 li = [1, 4, 5, 6, 7, 9, 11, 14, 16]# 以下写法都能够暗示全部列表,其中 X >= len(li)li[0:X] == li[0:] == li[:X] == li[:]== li[::] == li[-X:X] == li[-X:]li[1:5] == [4,5,6,7] # 从1起,取5-1位元素li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤li[-1:] == [16] # 取倒数第1个元素li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素li[:-2] == li[-len(li):-2]== [1,4,5,6,7,9,11] # 从头起始,取-2-(-len(li))=7位元素# 步长为负数时,列表先翻转,再截取li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转全部列表li[::-2] == [16,11,7,5,1] # 翻转全部列表,再按2间隔过滤li[:-5:-1] == [16,14,11,9] # 翻转全部列表,取-5-(-len(li))=4位元素li[:-5:-3] == [16,9] # 翻转全部列表,取-5-(-len(li))=4位元素,再按3间隔过滤# 切片的步长不能够为0li[::0] # 报错(ValueError: slice step cannot be zero)
以上的某些例子针对初学者(乃至非常多老手)来讲,可能还欠好理解,然则它们都离不开切片的基本语法,因此为方便起见,我将它们亦归入基本用法中。
针对这些样例,我个人总结出两条经验:
(1)牢牢记住公式[i : i+n : m],当显现缺省值时,经过想象把公式补全;
(2)索引为负且步长为正时,按倒数计算索引位置;索引为负且步长为负时,先翻转列表,再按倒数计算索引位置。
2、切片的高级用法
通常而言,切片操作的返回结果是一个新的独立的序列。以列表为例,列表切片后得到的还是一个列表,占用新的内存位置。
当取出切片的结果时,它是一个独立对象,因此,能够将其用于赋值操作,亦能够用于其它传递值的场景。然则,切片只是浅拷贝,它拷贝的是原列表中元素的引用,因此,当存在变长对象的元素时,新列表将受制于原列表。 li = [1, 2, 3, 4
]
ls = li[::]
li == ls # True
id(li) == id(ls) # False li.append(li[2:4]) # [1, 2, 3, 4, [3, 4
]] ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4
] # 下例等价于判断li长度是不是大于8if(li[8
:]): print("not empty"
) else
: print("empty"
)
# 切片列表受制于原列表 lo = [1,[1,1],2,3
] lp = lo[:2] # [1, [1, 1
]] lo[1].append(1) # [1, [1, 1, 1], 2, 3
] lp # [1, [1, 1, 1
]]
因为可见,将切片结果取出,它能够做为独立对象运用,然则亦要重视,是不是取出了变长对象的元素。
切片既能够做为独立对象被“取出”原序列,亦能够留在原序列,做为一种占位符运用。
不久前,我介绍了几种拼接字符串的办法,其中三种格式化类的拼接办法(即 %、format()、template)便是运用了占位符的思想。针对列表来讲,运用切片做为占位符,一样能够实现拼接列表的效果。尤其必须重视的是,给切片赋值的必要是可迭代对象。 li = [1, 2, 3, 4]# 在头部拼接li[:0] = [0] # [0, 1, 2, 3, 4]# 在末尾拼接li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7]# 在中部拼接li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7]# 给切片赋值的必要是可迭代对象li[-1:-1] = 6 # (报错,TypeError: can only assign an iterable)li[:0] = (9,) # [9, 0, 1, 2, 3, 4, 5, 6, 7]li[:0] = range(3) # [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7]
以上例子中,若将切片做为独立对象取出,那你会发掘它们都是空列表,即 li[:0]==li[len(li):]==li[6:6]==,我将这种占位符叫作为“纯占位符”,对纯占位符赋值,并不会破坏原有的元素,只会在特定的索引位置中拼接进新的元素。删除纯占位符时,亦不会影响列表中的元素。
与“纯占位符”相对应,“非纯占位符”的切片是非空列表,对它进行操作(赋值与删除),将会影响原始列表。倘若说纯占位符能够实现列表的拼接,那样,非纯占位符能够实现列表的替换。 li = [1, 2, 3, 4]# 区别位置的替换li[:3] = [7,8,9] # [7, 8, 9, 4]li[3:] = [5,6,7] # [7, 8, 9, 5, 6, 7]li[2:4] = [a,b] # [7, 8, a, b, 6, 7]# 非等长替换li[2:4] = [1,2,3,4] # [7, 8, 1, 2, 3, 4, 6, 7]li[2:6] = [a] # [7, 8, a, 6, 7]# 删除元素del li[2:3] # [7, 8, 6, 7]
切片占位符能够带步长,从而实现连续跨越性的替换或删除效果。必须重视的是,这种用法只支持等长替换。 li = [1, 2, 3, 4, 5, 6
] li[::2] = [a,b,c] # [a, 2, b, 4, c, 6
] li[::2] = [0]*3 # [0, 2, 0, 4, 0, 6
] li[::2] = [w] # 报错,attempt to assign sequence of size 1 to extended slice of size 3del li[::2] # [2, 4, 6
]
3、自定义对象实现切片功能
切片是 Python 中最迷人最强大最 Amazing 的语言特性(几乎无之一),以上两小节虽然介绍了切片的基本用法与高级用法,但这些还不足以充分地展露切片的魅力,因此,在接下来的两章节中,咱们将聚焦于它的更高级用法。
前两节内容都是基于原生的序列类型(如字符串、列表、元组……),那样,咱们是不是能够定义自己的序列类型并让它支持切片语法呢?更进一步,咱们是不是能够自定义其它对象(如字典)并让它支持切片呢?
3.1、魔术办法:`getitem`
想要使自定义对象支持切片语法并不难,只必须在定义类的时候给它实现魔术办法 __getitem__就可。因此,这儿就先介绍一下这个办法。
语法:object.__getitem__(self, key)
官方文档释义:Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the__getitem__method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.
概括翻译一下:__getitem__办法用于返回参数 key 所对应的值,这个 key 能够是整型数值和切片对象,并且支持负数索引;倘若 key 不是以上两种类型,就会抛 TypeError;倘若索引越界,会抛 IndexError ;倘若定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。
3.2、自定义序列实现切片功能
接下来,咱们定义一个简单的 MyList ,并给它加上切片功能。(PS:仅作演示,不保准其它功能的完备性)。 import
numbers
class MyList:
def __init__(self, anylist): self.data
= anylist
def __len__(self): return len(self.data
)
def __getitem__(self, index): print("key is : "
+ str(index))
cls = type(self) if
isinstance(index, slice): print("data is : " + str(self.data
[index])) return cls(self.data
[index])
elif isinstance(index, numbers.Integral): return self.data
[index] else
: msg = "{cls.__name__} indices must be integers"
raise TypeError(msg.format(cls=cls)) l = MyList(["My", "name", "is", "Python猫"
])
### 输出结果: key is : 3
Python猫 key is : slice(None, 2
, None) data is : [My, name
] <__main__.MyList object at 0x0000019CD83A7A90
> key is
: hi
Traceback (most recent call last):
...
TypeError: MyList indices must be integers or slices
从输出结果来看,自定义的 MyList 既支持按索引查询,亦支持切片操作,这正是咱们的目的。
3.3、自定义字典实现切片功能
切片是序列类型的特性,因此在上例中,咱们不必须写切片的详细实现规律。然则,针对其它非序列类型的自定义对象,就得自己实现切片规律。以自定义字典为例(PS:仅作演示,不保准其它功能的完备性): class MyDict:def __init__
(self): self.data
= {}
def __len__(self): return len(self.data
)
def append(self, item): self.data
[len(self)] = item
def __getitem__(self, key): if
isinstance(key, int): return self.data
[key] if
isinstance(key, slice): slicedkeys = list(self.data
.keys)[key] return {k: self.data[k] for k in
slicedkeys} else
:
raise TypeError
d = MyDict d.append("My"
) d.append("name"
) d.append("is"
) d.append("Python猫"
) print(d[2
]) print(d[:2
]) print(d[-4:-2
]) print(d[hi
])
### 输出结果: is{0: My, 1: name
} {0: My, 1: name
}
Traceback (most recent call last):
...
TypeError
上例的关键点在于将字典的键值取出,并对键值的列表做切片处理,其妙处在于,不消担心索引越界和负数索引,将字典切片转换成为了字典键值的切片,最后实现目的。
4、迭代器实现切片功能
好了,介绍完通常的自定义对象怎样实现切片功能,这儿将迎来另一类非同通常的对象。
迭代器是 Python 中独特的一种高级对象,它本身不具备切片功能,然而若能将它用于切片,这便仿佛是锦上添花,能达到如虎添翼的效果。因此,本节将隆重地介绍迭代器怎样实现切片功能。
4.1、迭代与迭代器
首要,有几个基本概念要澄清:迭代、可迭代对象、迭代器。
迭代是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,咱们说迭代一个字符串“abc”,指的便是从左往右依次地、逐个地取出它的所有字符的过程。(PS:汉语中迭代一词有循环反复、层层递进的意思,但 Python 中此词要理解成单向水平线性的,倘若你不熟练它,我意见直接将其理解为遍历。)
那样,怎么写出迭代操作的指令呢?最通用的书写语法便是 for 循环。 # for
循环实现迭代过程 for char in "abc"
: print(char, end=" "
)
# 输出结果:a b c
for 循环能够实现迭代的过程,然则,并非所有对象都能够用于 for 循环,例如,上例中若将字符串“abc”换成任意整型数字,则会报错:int object is not iterable .
这句报错中的单词“iterable”指的是“可迭代的”,即 int 类型不是可迭代的。而字符串(string)类型是可迭代的,一样地,列表、元组、字典等类型,都是可迭代的。
那怎么判断一个对象是不是可迭代呢?为何它们是可迭代的呢?怎么让一个对象可迭代呢?
要使一个对象可迭代,就要实现可迭代协议,即必须实现__iter__魔术办法,换言之,只要实现了这个魔术办法的对象都是可迭代对象。
那怎么判断一个对象是不是实现了这个办法呢?除了以上的 for 循环外,我还晓得四种办法: # 办法1
:dir查看__iter__ dir(2
) # 无,略 dir("abc"
) # 有,略 # 办法2
:isinstance判断 import
collections isinstance(2
, collections.Iterable) # False isinstance("abc"
, collections.Iterable) # True # 办法3
:hasattr判断 hasattr(2,"__iter__"
) # False hasattr("abc","__iter__"
) # True # 办法4
:用iter查看是不是报错 iter(2) # 报错:int object is
not iterable iter("abc") # <str_iterator at 0x1e2396d8f28
>
### PS:判断是不是可迭代,还能够查看是不是实现__getitem__,为方便描述,本文从略。
这几种办法中最值得一提的是 iter 办法,它是 Python 的内置办法,其功效是将可迭代对象变成迭代器。这句话能够解析出两层意思:(1)可迭代对象跟迭代器是两种东西;(2)可迭代对象能变成迭代器。
实质上,迭代器必然是可迭代对象,但可迭代对象不必定是迭代器。两者有多大的区别呢?
如上图蓝圈所示,普通可迭代对象与迭代器的最关键区别可概括为:一同两区别,所说“一同”,即两者都是可迭代的(__iter__),所说“两区别”,就可迭代对象在转化为迭代器后,它会丢失有些属性(__getitem__),同期亦增多有些属性(__next__)。
首要瞧瞧增多的属性 __next__, 它是迭代器之因此是迭代器的关键,事实上,咱们正是把同期实现了__iter__办法 和__next__办法的对象定义为迭代器的。
有了多出来的这个属性,可迭代对象不必须借助外边的 for 循环语法,就能实现自我的迭代/遍历过程。我发明了两个概念来描述这两种遍历过程(PS:为了易理解,这儿叫作遍历,实质亦可叫作为迭代):它遍历指的是经过外边语法而实现的遍历,自遍历指的是经过自己办法实现的遍历。
借助这两个概念,咱们说,可迭代对象便是能被“它遍历”的对象,而迭代器是这里基本上,还能做到“自遍历”的对象。 ob1 = "abc"ob2 = iter("abc"
) ob3 = iter("abc"
)
# ob1它遍历 for i in
ob1: print(i, end = " "
) # a b c for i in
ob1: print(i, end = " "
) # a b c
# ob1自遍历 ob1.__next__ # 报错: str object has no attribute __next__
# ob2它遍历 for i in
ob2: print(i, end = " "
) # a b c for i in
ob2: print(i, end = " "
) # 无输出
# ob2自遍历
ob2.__next__ # 报错:StopIteration
# ob3自遍历
ob3.__next__ # a
ob3.__next__ # b
ob3.__next__ # c
ob3.__next__ # 报错:StopIteration
经过以上例子可看出,迭代器的优良在于支持自遍历,同期,它的特点是单向非循环的,一旦完成遍历,再次调用就会报错。
对此,我想到一个比方:普通可迭代对象就像是子弹匣,它遍历便是取出子弹,在完成操作后又装回去,因此能够反复遍历(即多次调用for循环,返回相同结果);而迭代器就像是装载了子弹匣且不可拆卸的枪,进行它遍历或自遍历都是发射子弹,这是消耗性的遍历,是没法复用的(即遍历会有尽头)。
写了这么多,稍微小结一下:迭代是一种遍历元素的方式,根据实现方式划分,有外边迭代与内部迭代两种,支持外部迭代(它遍历)的对象便是可迭代对象,而同期还支持内部迭代(自遍历)的对象便是迭代器;根据消费方式划分,可分为复用型迭代与一次性迭代,普通可迭代对象是复用型的,而迭代器是一次性的。
4.2、迭代器切片
前面说到了“一同两区别”,最后的区别是,普通可迭代对象在转化成迭代器的过程中会丢失有些属性,其中关键的属性是 __getitem__。在前一节中,我已然介绍了这个魔术办法,并用它实现了自定义对象的切片特性。
那样问题来了:为啥迭代器不继承这个属性呢?
首要,迭代器运用的是消耗型的遍历,这寓意着它充满不确定性,即其长度与索引键值对是动态衰减的,因此很难 get 到它的 item ,亦就再也不必须 __getitem__属性了。其次,若强行给迭代器加上这个属性,这并不恰当,正所说强扭的瓜不甜……
由此,新的问题来了:既然会丢失这么重要的属性(还包含其它未标识的属性),为何还要运用迭代器呢?
这个问题的答案在于,迭代器持有不可替代的强大的有用的功能,使得 Python 要如此设计它。限于篇幅,此处再也不展开,后续我会专门填坑此专题。
还没完,死缠烂打的问题来了:能否令迭代器持有这个属性呢,即令迭代器继续支持切片呢? hi = "欢迎关注公众号:Python猫"
it = iter(hi) # 普通切片hi[-7:] # Python猫# 反例:迭代器切片it[-7:] # 报错:str_iterator object is not subscriptable
迭代器由于缺少__getitem__,因此呢不可运用普通的切片语法。想要实现切片,无非两种思路:一是自己造轮子,写实现的规律;二是找到封装好的轮子。
Python 的 itertools 模块便是咱们要找的轮子,用它供给的办法可容易实现迭代器切片。
import itertools # 例1
:简易迭代器 s = iter("123456789"
) for x in itertools.islice(s, 2, 6
): print(x, end = " ") # 输出:3 4 5 6for x in itertools.islice(s, 2, 6
): print(x, end = " ") # 输出:9# 例2
:斐波那契数列迭代器 class
Fib: def __init__(self
): self.a, self.b = 1, 1def __iter__(self
): while
True: yield self
.a self.a, self.b = self.b, self.a + self
.b
f = iter(Fib) for x in itertools.islice(f, 2, 6
): print(x, end = " ") # 输出:2 3 5 8for x in itertools.islice(f, 2, 6
): print(x, end = " ") # 输出:34 55 89 144
itertools 模块的 islice 办法将迭代器与切片完美结合,最终回答了前面的问题。然而,迭代器切片跟普通切片相比,前者有非常多局限性。首要,这个办法不是“纯函数”(纯函数需遵守“相同输入得到相同输出”的原则);其次,它只支持正向切片,且不支持负数索引,这都是由于迭代器的损耗性所决定的。
那样,我不禁要问:itertools 模块的切片办法用了什么实现规律呢?下方是官网供给的源码:
def islice(iterable, *args): # islice(ABCDEFG, 2
) --> A B # islice(ABCDEFG, 2, 4
) --> C D # islice(ABCDEFG, 2, None
) --> C D E F G # islice(ABCDEFG, 0, None, 2
) --> A C E G
s = slice(*args) # 索引区间是[0,sys.maxsize],默认步长是1start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
it = iter(range(start, stop, step)) try
:
nexti = next(it)
except StopIteration:
# Consume *iterable* up to the *start* position. for i, element in
zip(range(start), iterable):
pass
return
try: for i, element in
enumerate(iterable): if
i == nexti: yield
element
nexti = next(it)
except StopIteration:
# Consume to *stop*. for i, element in zip(range(i + 1
, stop), iterable):
pass
islice 办法的索引方向是受限的,但它亦供给了一种可能性:即准许你对一个无穷的(在系统支持范围内)迭代器进行切片的能力。这是迭代器切片最具想象力的用途场景。
除此之外,迭代器切片还有一个很实在的应用场景:读取文件对象中给定行数范围的数据。
咱们晓得,从文件中读取内容重点有两种办法:read 适合读取内容较少的状况,或是必须一次性处理所有内容的状况;而 readlines 适用性更广,由于它是迭代地读取内容,既减少内存压力,又方便逐行对数据处理。
虽然 readlines 有迭代读取的优良,但它是从头到尾逐行读取,若文件有几千行,而咱们只想要读取少许特定行(例如第1000-1009行),那它还是效率太低了。思虑到文件对象天然便是迭代器,咱们能够运用迭代器切片先行截取,而后再处理,如此效率将大大地提高。
# test.txt 文件内容 猫Python猫python is a cat.this is the end.from itertoolsimport
islice with open(test.txt,r,encoding=utf-8) as
f: print(hasattr(f, "__next__"
)) # 判断是不是迭代器 content = islice(f, 2, 4
) for line in
content:
print(line.strip)
### 输出结果:
True python is
a cat. this is
the end.
本节内容较多,简单回顾一下:迭代器是一种特殊的可迭代对象,可用于它遍历与自遍历,但遍历过程是损耗型的,不具备循环复用性,因此呢,迭代器本身不支持切片操作;经过借助 itertools 模块,咱们能实现迭代器切片,将两者的优良相结合,其重点用途在于截取大型迭代器(如无限数列、超大文件等等)的片段,实现精细的处理,从而大大地提高性能与效率。
5、小结
最后总结一下,切片是 Python 的一种高级特性,常用于截取序列类型的元素,但并不局限于此,本文重点介绍了它的基本用法、高级用法(如占位符用法)、自定义对象切片、以及迭代器切片等运用内容。除此之外,切片还有更广阔多样的运用场景,例如 Numpy 的多维切片、内存视图切片、异步迭代器切片等等,都值得咱们去探索一番。
源自:Python猫
作者:豌豆花下猫
回复下方「关键词」,获取优秀资源
回复关键词「 pybook03」,立即获取主页君与小伙伴一块翻译的《Think Python 2e》电子版
回复关键词「入门资料」,立即获取主页君整理的 10 本 Python 入门书的电子版
回复关键词「m」,立即获取Python精选优秀文案合集
回复关键词「」,将数字替换成 0 及以上数字,有惊爱好礼哦~
|