> 백엔드 개발 > 파이썬 튜토리얼 > Python의 슬라이싱에 대한 자세한 소개

Python의 슬라이싱에 대한 자세한 소개

不言
풀어 주다: 2019-01-01 09:59:38
앞으로
24512명이 탐색했습니다.

이 기사는 Python의 슬라이싱에 대한 자세한 소개를 제공합니다. 필요한 친구들이 참고할 수 있기를 바랍니다.

서론: 슬라이스 시리즈에 세 편의 글을 연속으로 썼는데, 이 글은 그 내용을 요약한 것입니다. 시퀀스 기사를 병합해야 하는 이유는 무엇입니까? 여기서는 이 글이 단순히 병합하는 것이 아니라는 점을 설명하겠습니다. 주로 몇 가지 심각한 오류(예: 사용자 정의 시퀀스 슬라이싱 부분)를 수정하고 텍스트 구조와 장 연결에도 많은 변경을 적용합니다. 이 기사의 구조 무결성과 내용의 품질은 잘 보장됩니다.

우리 모두 알고 있듯이 인덱스 값(또는 아래 첨자)을 통해 시퀀스 유형(예: 문자열, 목록, 튜플...)에서 단일 요소를 찾을 수 있으므로 요소를 얻으려면 어떻게 해야 합니까? 인덱스 범위?

Slice는 슬라이싱 기술의 도움으로 시퀀스 유형의 객체를 매우 유연하게 처리할 수 있는 인덱스 조각을 가로채는 기술입니다. 일반적으로 슬라이싱의 기능은 시퀀스 객체를 가로채는 것입니다. 그러나 시퀀스가 ​​아닌 객체의 경우 슬라이싱 작업을 수행할 수 있는 방법이 있습니까? 슬라이싱을 사용하는 과정에서 주의할 만한 핵심 사항은 무엇이며, 주의할 만한 기본 원칙은 무엇입니까? 이번 글에서는 이러한 내용을 주로 여러분과 함께 배우고 발전할 수 있기를 바랍니다.

1. 슬라이싱의 기본 사용법

리스트는 파이썬에서 매우 기본적이고 중요한 자료구조이자, 슬라이싱을 가장 잘 활용하는 자료구조이기 때문에 처음 두 절에서는 리스트를 예로 들어 소개하겠습니다. . 슬라이스의 몇 가지 일반적인 용도.

첫 번째는 슬라이스의 작성된 형식입니다. [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] # 取倒数第一个元素
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间隔过滤

# 切片的步长不可以为0
li[::0]  # 报错(ValueError: slice step cannot be zero)
로그인 후 복사
로그인 후 복사

위의 예 중 일부는 초보자(또는 많은 베테랑)에게는 이해하기 쉽지 않을 수 있지만 모두 슬라이싱의 기본 구문과 분리할 수 없으므로 편의상 기본 사용법으로도 분류하겠습니다. .

이 예에서는 개인적으로 두 가지 경험을 요약했습니다.

(1) [i : i+n : m] 수식을 염두에 두고 기본값이 나타나면 Imagine을 전달합니다. [i : i+n : m] ,当出现缺省值时,通过想象把公式补全;

(2)索引为负且步长为正时,按倒数计算索引位置;索引为负且步长为负时,先翻转列表,再按倒数计算索引位置。

2、切片的高级用法

一般而言,切片操作的返回结果是一个新的独立的序列(PS:也有例外,参见《Python是否支持复制字符串呢?》)。以列表为例,列表切片后得到的还是一个列表,占用新的内存地址。

当取出切片的结果时,它是一个独立对象,因此,可以将其用于赋值操作,也可以用于其它传递值的场景。但是,切片只是浅拷贝 ,它拷贝的是原列表中元素的引用,所以,当存在变长对象的元素时,新列表将受制于原列表。

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长度是否大于8
if(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]==[](2) 인덱스가 음수이고 스텝 크기가 양수이면 역수에 따라 인덱스 위치를 계산합니다. 인덱스가 음수이고 스텝 크기가 음수이면 먼저 목록을 뒤집은 다음 계산합니다. 역수에 따른 인덱스 위치.

2. 슬라이싱의 고급 사용법

일반적으로 슬라이싱 작업의 반환 결과는 새로운 독립 시퀀스입니다(PS: 예외가 있습니다. "파이썬은 문자열 복사를 지원합니까?"를 참조하세요). 목록을 예로 들면, 목록을 잘라낸 후에 얻는 것은 여전히 ​​목록이며 새 메모리 주소를 차지합니다.

슬라이스 결과를 꺼내면 독립된 개체이므로 할당 작업 및 기타 값 전달 시나리오에 사용할 수 있습니다. 하지만

슬라이싱은 얕은 복사본

일 뿐입니다. 따라서 원본 목록에 있는 요소의 참조를 복사하므로 가변 길이 개체의 요소가 있는 경우 새 목록에는 원본 목록이 적용됩니다. 🎜
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]
로그인 후 복사
로그인 후 복사
🎜눈에 보이기 때문에 슬라이싱 결과를 꺼내면 독립된 개체로 활용도 가능하지만, 가변 길이 개체의 요소가 빠져 있는지도 주의 깊게 살펴봐야 합니다. 🎜🎜슬라이스를 원래 시퀀스에서 독립 개체로 "제거"하거나 원래 시퀀스에 남겨두고 자리 표시자로 사용할 수 있습니다. 🎜🎜얼마 전 문자열을 접합하는 몇 가지 방법을 소개했습니다(글 끝 부분에 링크). 그 중 형식 지정 클래스(예: %, format(), 템플릿)의 세 가지 접합 방법은 자리 표시자 아이디어를 사용합니다. . 목록의 경우 조각을 자리 표시자로 사용하면 목록을 연결하는 효과를 얻을 수도 있습니다. 특히, 슬라이스에 할당된 값은 반복 가능한 객체여야 한다는 점에 유의해야 합니다. 🎜
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 3

del li[::2] # [2, 4, 6]
로그인 후 복사
로그인 후 복사
로그인 후 복사
🎜위의 예에서 슬라이스를 독립된 객체로 꺼내보면 모두 빈 리스트, 즉 li[:0]==li[len(li):]=임을 알 수 있습니다. =li[6: 6]==[], 저는 이런 종류의 자리 표시자를 "🎜Pure 자리 표시자🎜"라고 부릅니다. 순수 자리 표시자에 값을 할당하면 원래 요소가 파괴되지 않고 새 요소에만 Splice됩니다. 인덱스 위치의 요소입니다. 순수 자리 표시자를 제거해도 목록의 요소는 영향을 받지 않습니다. 🎜🎜는 "순수 자리 표시자"에 해당합니다. "🎜비순수 자리 표시자🎜" 조각은 비어 있지 않은 목록입니다. 이에 대한 작업(할당 및 삭제)은 원래 목록에 영향을 미칩니다. 순수 자리 표시자가 목록 연결을 실현할 수 있다면 순수하지 않은 자리 표시자는 목록 교체를 실현할 수 있습니다. 🎜
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>
key is : hi
Traceback (most recent call last):
...
TypeError: MyList indices must be integers or slices</__main__.mylist>
로그인 후 복사
로그인 후 복사
로그인 후 복사
🎜Slice 자리 표시자는 연속적인 교체 또는 삭제 효과를 얻기 위해 단계 크기를 가질 수 있습니다. 이 사용법은 동일한 길이의 대체만 지원한다는 점에 유의해야 합니다. 🎜
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 3

del 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>
key is : hi
Traceback (most recent call last):
...
TypeError: MyList indices must be integers or slices</__main__.mylist>
로그인 후 복사
로그인 후 복사
로그인 후 복사

从输出结果来看,自定义的 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>

### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。</str_iterator>
로그인 후 복사
로그인 후 복사

这几种方法中最值得一提的是 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 6
for x in itertools.islice(s, 2, 6):
    print(x, end = " ")   # 输出:9

# 例2:斐波那契数列迭代器
class Fib():
    def __init__(self):
        self.a, self.b = 1, 1

    def __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 8
for 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],默认步长是1
    start, 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 itertools import 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猫 ”,以后我们慢慢学习之。

切片系列(原单篇):

Python进阶:切片的误区与高级用法

Python进阶:自定义对象实现切片功能

Python进阶:迭代器与迭代器切片

相关链接:

官方文档getitem用法:http://t.cn/EbzoZyp

切片赋值的源码分析:http://t.cn/EbzSaoZ

官网itertools模块介绍:http://t.cn/EbNc0ot

Python是否支持复制字符串呢?

来自Kenneth Reitz大神的建议:避免不必要的面向对象编程

给Python学习者的文件读写指南(含基础与进阶,建议收藏)

详解Python拼接字符串的七种方式

-----------------

本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。







svn

mysql
  • linux

  • django

    Python의 슬라이싱에 대한 자세한 소개

    # 🎜 🎜 #
  • ~#🎜🎜 ##### 🎜🎜#28 읽기                                                          읽는 데 46분이 걸립니다.                                                



# 🎜 🎜#                                     

2

                                                                                   

                                                                                                                                                                                                           


소개: 저는 3개의 연속된 기사를 썼습니다. 슬라이스 시리즈. 이 기사는 그 내용을 요약한 것입니다. 시퀀스 기사를 병합해야 하는 이유는 무엇입니까? 여기서는 이 글이 단순히 병합하는 것이 아니라는 점을 설명하겠습니다. 주로 몇 가지 심각한 오류(예: 사용자 정의 시퀀스 슬라이싱 부분)를 수정하고 텍스트 구조와 장 연결에도 많은 변경을 적용합니다. 이 기사의 구조 무결성과 내용의 품질은 잘 보장됩니다. Python의 슬라이싱에 대한 자세한 소개우리 모두 알고 있듯이 인덱스 값(또는 아래 첨자)을 사용하여 시퀀스 유형(예: 문자열, 목록, 튜플...)에서 단일 요소를 찾을 수 있습니다. 그런 다음 원하는 경우 색인을 얻으려면 범위의 요소로 무엇을 해야 합니까?

슬라이스는 인덱스 조각을 가로채는 기술입니다. 슬라이싱 기술을 사용하면 시퀀스 유형 개체를 매우 유연하게 처리할 수 있습니다. 일반적으로 슬라이싱의 기능은 시퀀스 객체를 가로채는 것입니다. 그러나 시퀀스가 ​​아닌 객체의 경우 슬라이싱 작업을 수행할 수 있는 방법이 있습니까? 슬라이싱을 사용하는 과정에서 주의할 만한 핵심 사항은 무엇이며, 주의할 만한 기본 원칙은 무엇입니까? 이번 글에서는 이러한 내용을 주로 여러분과 함께 배우고 발전할 수 있기를 바랍니다.

1. 슬라이싱의 기본 사용법

리스트는 파이썬에서 매우 기본적이고 중요한 자료구조이자, 슬라이싱을 가장 잘 활용하는 자료구조이기도 하므로 먼저 두 섹션에서는 목록을 예로 들어 슬라이싱의 몇 가지 일반적인 용도를 소개하겠습니다.

첫 번째는 슬라이스의 작성된 형식입니다. [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] # 取倒数第一个元素
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间隔过滤

# 切片的步长不可以为0
li[::0]  # 报错(ValueError: slice step cannot be zero)
로그인 후 복사
로그인 후 복사

위 예제 중 일부는 초보자(또는 베테랑)에게는 이해하기 쉽지 않을 수 있지만 모두 슬라이싱의 기본 구문과 불가분의 관계가 있으므로 편의상 설명하겠습니다. 기본 사용량에도 포함됩니다. 이러한 예에서는 개인적으로 두 가지 경험을 요약했습니다.

(1) 수식

을 명심하세요. 기본값이 나타나면 상상을 완료하세요. 공식

(2) 인덱스가 음수이고 스텝 크기가 양수이면 역수에 따라 인덱스 위치를 계산합니다. 인덱스가 음수이고 스텝 크기가 음수이면 먼저 뒤집습니다. 목록을 누른 다음 역 계산 인덱스 위치를 누릅니다.

2、切片的高级用法

一般而言,切片操作的返回结果是一个新的独立的序列(PS:也有例外,参见《Python是否支持复制字符串呢?》)。以列表为例,列表切片后得到的还是一个列表,占用新的内存地址。

当取出切片的结果时,它是一个独立对象,因此,可以将其用于赋值操作,也可以用于其它传递值的场景。但是,切片只是浅拷贝 ,它拷贝的是原列表中元素的引用,所以,当存在变长对象的元素时,新列表将受制于原列表。

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长度是否大于8
if(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 3

del 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>
key is : hi
Traceback (most recent call last):
...
TypeError: MyList indices must be integers or slices</__main__.mylist>
로그인 후 복사
로그인 후 복사
로그인 후 복사

从输出结果来看,自定义的 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>

### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。</str_iterator>
로그인 후 복사
로그인 후 복사

这几种方法中最值得一提的是 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 6
for x in itertools.islice(s, 2, 6):
    print(x, end = " ")   # 输出:9

# 例2:斐波那契数列迭代器
class Fib():
    def __init__(self):
        self.a, self.b = 1, 1

    def __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 8
for 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],默认步长是1
    start, 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 itertools import 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猫 ”,以后我们慢慢学习之。

切片系列(原单篇):

Python进阶:切片的误区与高级用法

Python进阶:自定义对象实现切片功能

Python进阶:迭代器与迭代器切片

相关链接:

官方文档getitem用法:http://t.cn/EbzoZyp

切片赋值的源码分析:http://t.cn/EbzSaoZ

官网itertools模块介绍:http://t.cn/EbNc0ot

Python是否支持复制字符串呢?

来自Kenneth Reitz大神的建议:避免不必要的面向对象编程

给Python学习者的文件读写指南(含基础与进阶,建议收藏)

详解Python拼接字符串的七种方式

-----------------

이 기사는 원본이며 WeChat 공개 계정 [Python Cat]에 처음 게시되었습니다. 백그라운드에서 "Love Learning"이라고 대답하고 20개 이상의 엄선된 전자책을 무료로 받으세요.

  • Python의 슬라이싱에 대한 자세한 소개


관심이 있으실 수도 있습니다



댓글 ~ ~ 발진

위 내용은 Python의 슬라이싱에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:segmentfault.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿