[Python]深入理解容器、迭代器与生成器

一、什么是iterable?

理解生成迭代器和生成器之前,先了解可迭代对象。

1.官方文档的定义:

An object capable of returning its members one at a time. 可迭代的对象

2.可迭代的对象包含类型:

  • 所有的序列类型(sequence type):比如列表、字符串、元组等;
from collections import Iterable

print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance("hello python", Iterable))  # True
print(isinstance((1, 2, 3), Iterable))  # True
  • 一些无序列的类型( non-sequence type):比如字典、文件对象;
from collections import Iterable

print(isinstance({"language": "Python"}, Iterable))  # True
  • 自定义的类,但结构内需包含_iter()方法或者_getitem_()方法
from collections import Iterable

class NumberList():
    """定义一个数字类"""

    def __init__(self):
        super(NumberList, self).__init__()
        self.__number = list()

    def number_append(self, item):
        """向空列表中添加数字"""
        self.__number.append(item)

    def __iter__(self):
        pass


# 实例化
number_list = NumberList()
# 向列表中添加几个数字
number_list.number_append(1)
number_list.number_append(2)
number_list.number_append(3)
# 查看number_list是否是Iterable类型
print(isinstance(number_list, Iterable))    # 结果为True

3.取值方式:

  • 方式一:使用for循环可以遍历取值。
for i in [1, 2, 3]:
    print(i)
  • 方式二:使用方法iter()

说明1:当一个iterable对象作为参数传给iter()函数时,会返回一个iterator对象。

from collections import Iterable, Iterator

l = [1, 2, 3]
print(isinstance(l, Iterable))	# True
print(isinstance(l, Iterator))	# False

it = iter(l)  # 迭代器对象
print(isinstance(it, Iterable))	# True
print(isinstance(it, Iterator))	# True,此时it已经是一个Iterator对象了。

说明2:Iterator对象会有一个_next_()方法或者调用Python内置的函数方法next()。以下三个方法不要同时执行,运行一个注释其它两个。

# 方法1:for语句
for i in it:
    print(i)

# 方法2:while循环
while True:
    try:
        print(next(it))
    except StopIteration:
        break

# 方法3:__next__()方法
while True:
    try:
        print(it.__next__())
    except StopIteration:
        break

注意:当我们使用iterable对象时,通常没有必要调用iter()函数返回iterator对象或者自己去处理iterator对象。通常for就能解决这个问题,而且简洁明了。for语句内部机制会创建一个未命名的临时变量,用于接受迭代器对象。

二、什么是itreator?

1.官方文档的定义:

An object representing a stream of data.

iterator表现为数据流(stream of data)的形式的对象,而像list、tuple、dict等对象,表现为容器对象(container).

在上文,已经了解了iterable对象,如果要遍历取值的话,一般通过for语句的内部机制,将iterable对象转为iterator对象,再取值。其实,对于像列表、元组、字符串等这样的iterable对象只有__iter__()方法,而没有__next__()方法,所以说它们都是可迭代的,即具有迭代的天赋,但如果给它们用函数iter()升级一下,它们就变成了iterator对象,就具备了__next__()。所以两者区别之一,是否有__next__()方法。

注意1:

对container对象使用iter()方法或者使用for语句的时候,每次都是生成一个新的迭代器。也就是说,你每次对同一个container对象进行上述调用,返回的iterator对象是不一样的(内存中地址不一样)。
在这里插入图片描述
在这里插入图片描述
注意2:

对container对象使用iter()方法或者for语句生成的iterator操作,即使取尽了iterator中的所有值,也不会对container对象产生影响,只是让你看上去container好像空了,好比对某个列表进行上述操作,并不影响该列表里的值。

2.取值:

通过调用迭代器的__next__()方法或者把迭代器作为参数传给函数next(),这两种方法都会返回数据流中连续值。当数据流中的值取光后,再取值会rasie StopIteration。

3.自定义类CharList迭代器:

from collections import Iterator


class CharList():
    """定义一个字符类"""

    def __init__(self):
        super(CharList, self).__init__()
        self.__char_list = list()
        self.__current_index = 0    # 默认下标为0

    def show_list(self):
        """查看list中的元素"""
        print(self.__char_list)

    def char_append(self, item):
        """向空列表中添加数字"""
        self.__char_list.append(item)

    def __iter__(self):
        """返回迭代器对象"""
        return self

    def __next__(self):
        """获取迭代器对象中下一个值"""
        if self.__current_index < len(self.__char_list):
            # 如果当前index在列表长度范围内
            self.__current_index += 1
            return self.__char_list[self.__current_index - 1]
        else:
            # 表示index越界,抛出停止迭代的异常
            raise StopIteration


# 实例化
char_list = CharList()
# 向列表中添加几个数字
char_list.char_append("Hello")
char_list.char_append("Python")
char_list.char_append("!")

# 查看char_list是否是Iterator类型
print(isinstance(char_list, Iterator))    # 结果为True

# 取出所有值
for item in char_list:
    print(item)

# 查看CharList中是否还有值
char_list.show_list()

执行结果:

True
Hello
Python
!
['Hello', 'Python', '!']

说明:在上述例子自定义类迭代器中,即使取尽了迭代器中的值,但char_list中的值还是没有改变,因为char_list就跟container对象一样,貌似看上去变成了空容器。

Attempting this with an iterator will just return the same exhausted iterator object used

in the previous iteration pass, making it appear like an empty container.(官方文档给出的解释)

4.使用迭代器完成斐波拉切数列:

class Fibonacci():
    """定义一个斐波拉切数列类"""

    def __init__(self, a, b, total):
        """
        初始化斐波拉切数列
        :param a: 第一个数字
        :param b: 第二个数字
        :param total: 需要产生的数字个数
        """
        self.a = a
        self.b = b
        self.total = total
        self.current_index = 0      # 初始index默认为0

    def __iter__(self):
        """返回迭代器对象"""
        return self

    def __next__(self):
        """获取下一个数"""
        # 判断index是否越界
        if self.current_index < self.total:
            self.a, self.b = self.b, self.a + self.b
            self.current_index += 1
            return self.a
        else:
            raise StopIteration


fibonacci = Fibonacci(0, 1, 5)

for i in fibonacci:
    print(i)

5.迭代器有什么用?

为什么要有迭代器呢?这个问题估计好多人没有想过,我查看了文档,也没有作出解释。如果将container、iterator、generator(生成器)这三个结合思考不难发现,container对象,比如list、dict、file这些对象,都是会实际存储数据的,有时候存的少,有时候存的多,这样会很占资源。如果需要使用时,会全部加载到内存,很形象地像容器一样可以装很多时候。

container存储的是实际数据,那iterator存储的是什么呢?是算法。比如,上面定义的两个类迭代器,都是先定义运算规则,并没有存储需要的实际数据,当你有需要的时候,只需要通过for语句或者next()方法取出来即可。而generator本质上也是iterator,是一种特殊的iterator,中文名翻译的也很形象,“生成器”,数据一个一个地“蹦出来”。

三、什么是generator?

1.什么是generator?什么是generator iterator?

generator官方文档定义:

A function which returns a generator iterator. It looks like anormal function except that it contains yield expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function.

generator iterator官方文档定义:

An object created by a generator function.

说明:前者表示函数,后者表示返回的特殊的迭代器对象。在不需要明确区分的时候,这两个概念很容易被混用,比如用generator表示生成器对象。

2.什么是generator expression?

An expression that returns an iterator.这是一种创建generator的方式之一。
在这里插入图片描述
比较上图三行代码可以发现,它们唯一的区别在于外面的括号类型不一样,使用方括号的是列表解析式,用花括号的是集合解析式,这两个返回的分别是列表、集合,这两个都是container对象。第三个用的是圆括号,返回的是generator的内存地址,这个就是generator expression,生成器表达式。生成器表达式,可以返回一个迭代器对象。

说明:上面这三种表达方式都很紧凑,在某些情况下使用能使代码更简洁。

如何取值?作为一种特殊的迭代器,同样可以使用next()方法或者for语句取值。
在这里插入图片描述
next()方法:

for语句:
在这里插入图片描述
说明:next()取完所有值后如果再取,会报错StopIteration,停止迭代。for语句内部有捕获异常的机制,而且for语句使用起来更方便。

3.什么是yield expressions?

上文使用了generator expression创建了一个生成器对象,这种方式虽说及其紧凑方便,但是很多情况不能用generator expression表示,这时候需要用到更为强大的yield expressions。

yield expressions可以用于一般的生成器函数(generator function)或者是异步生成器函数(asynchronous generator function,用于协程)。只要函数体内包含yield关键字,普通函数就变成了生成器函数。

使用yield,创建一个简单的生成器函数

def generator_func():
    yield 123


# 由于生成器是特殊的迭代器,生成器一样存储的是算法,需要的时候next()调用即可
g = generator_func()    
print(next(g))      # 123

4.比较return与yield的执行机制:

# 这是包含return的普通函数
def test_1(num):
    for i in range(num):
        print("--------%d--------" % i)
        return i
        print("++++++++%d++++++++" % i)


# 这是包含yield的生成器函数
def test_2(num):
    for i in range(num):
        print("--------%d--------" % i)
        yield i
        print("++++++++%d++++++++" % i)


f = test_1(3)
print(f)
print(type(test_1))     # <class 'function'>
print("=" * 50)

g = test_2(3)
print(g)        # <generator object test_2 at 0x7fc1658f7af0>
print(type(g))  # <class 'generator'>
for i in g:
    print(i)

运行结果:

--------0--------
0
<class 'function'>
==================================================
<generator object test_2 at 0x7fc1658f7af0>
<class 'generator'>
--------0--------
0
++++++++0++++++++
--------1--------
1
++++++++1++++++++
--------2--------
2
++++++++2++++++++

比较上面代码:定义的两个函数几乎一样,唯一不同之处,一个使用了return关键字,一个使用的是yield关键字。可是执行结果却有很大不同:①使用return定义的那个函数是普通函数,使用yield定义的函数是生成器函数;②只含return的函数,没有遍历所有值,return语句后的代码没有执行,直接向父函数提交返回值;含yield的函数,取到了生成器中的所有值。

以上两者的不同,只是从代码执行结果比较出来的,那么return与yield的内部执行机制有什么不同呢?

两者的对所在线程(Thread)的控制权是不一样的。在return所在的函数中,只要执行到return语句后,return之后的语句都不会执行,因为执行完return语句后,立即交出对线程的控制权,也就是说,在同一函数中,return之后的语句都不占用线程资源。

而yield生成器函数,在执行到yield语句后,会像return一样把返回值交给调用函数,不同的是,在提交完返回值之后yield整个生成器都处于暂停状态,这时候跟return一样会交出线程的控制权,yield后面的代码也不会执行,如果想执行yield后面的语句,只能再次占有线程。如果你不让它重新占有线程(即恢复运行状态)的话,它会一直处于暂停状态,yield语句后生成器就是这么“懒”,一点都不主动。你可能会有疑问,我既没有看到它暂停的状态,也没有看到将它从暂停状态恢复到运行状态?别忘了,生成器是特殊的迭代器,迭代器可以通过for语句或者next()函数进行取值,上面的代码正是有了for语句,而不需要你主动去恢复它运行状态。如果你next()函数一次一次取值的话,你就会发现它有多懒。

下面通过一个例子,来仔细看看yield的内部执行机制:

# import time


def test_1():
    for i in range(5):
        yield i
        # time.sleep(0.5)


def test_2():
    for i in range(5, 10):
        yield i
        # time.sleep(0.5)


g1 = test_1()
g2 = test_2()

while True:
    try:
        print(next(g1))
        print(next(g2))
    except StopIteration:
        break

运行结果:

0
5
1
6
2
7
3
8
4
9

定义了两个yield生成器函数,执行结果,并没有按照0-9的顺序依次显示出来,而是交替显示的。为什么呢?因为在同一线程下,这两个生成器在同一时候只能有一个函数拥有线程资源。当g1拥有线程控制权(ge只能“伺机而动”),执行到yield语句提交返回值的同时,交出了线程的控制权。这时候g2生成器立刻意识到了线程处于空闲状态,取得线程的控制权。就这样,交替执行。由于CPU执行速度太快,你根本察觉不到从暂停到唤起的时间差,但是你能从上个例子的结果看到代码执行的顺序。

那为什么说yield生成器函数强大?有什么用呢?

yield生成器函数可以实现异步,用于协程(Coroutine)多任务。在cpython解释器下的Python由于有一个GIL(全局解释器锁)的存在,使用多线程实现的多任务其实是伪多任务,因为始终是在单核CPU上执行的,不能充分发挥多核CPU。尽管协程处于线程内,但是协程比线程占用更少的资源,Python中进程+协程的组合会比进程+线程的组合更节省资源。

5.generator-iterator对象有哪些方法?

生成器对象像Python中其他object一样,也有一些方法。

  • generator.next():

作用:该方法的作用与next()函数一样,用于取迭代器下一个值

说明:可以start(开启)或者resume(恢复)生成器的执行状态;也就是说可以用它取第一个值,或者从暂定状态处恢复执行状态并向下取一个值。

  • generator.send(value):

作用:当生成器处于暂停状态时,像生成器传一个值,也具有resume功能。

注意:当还没有start生成器(还没有调用过generator.next()或者next()函数时),是不能直接使用send()方法传参数的,只能先调用一下next()方法(或者使用send(None),两者等价)。

  • generator.throw(type[, value[, traceback]]):

作用:抛出各种类型的异常

  • generator.close()

作用:当generator处于暂停状态时,可以使用该状态主动关闭生成器。当再次使用该生成器对象时,会抛出停止迭代的异常,可以清理干净内存中的生成器对象。

四、小结

1.container对象:像list、tuple、str、dict、file等对象,在数据存储的形式就像容器一样;

2.iterable对象:Python提供了不少Iterable对象,包括所有的序列类型(如列表),一些无序列类型(如字典、文件等),同时也可以通过自定义类的方法实现可迭代的类对象。所有的iterable对象,都具有共同的一个方法__iter__()

3.iterator对象:迭代器对象,不同于container的数据存储形式,iterator只存储算法的内存地址,iterator对象与iterable对象不同之处在于,iterator同时具有__iter__()和__next__()方法。尽管像列表、元祖这样的对象不具有__next__()方法,但是可以通过iter()方法或者for语句为它们返回一个迭代器对象。

4.generator对象:生成器对象是特殊的迭代器,具有迭代器一切的特点。创建generator对象一般有两种方法,一种是generator expression生成器表达式,形式上与列表、集合解析式很像,实质却完全不同;另一种创建方式yield expressions,通过自定义generator function,能够发挥生成器更大的作用。yield expressions的灵活准确应用,需要建立在深刻理解yield关键字的内部执行机制上。

5.yield表达式的异步执行,可以用于协程,实现多任务。

最后,作为一个IT的过来人,我自己整理了一些学习资料,希望对你们有帮助。

在学习python中有任何困难不懂的可以微信扫描下方CSDN官方认证二维码加入python交流学习
多多交流问题,互帮互助,这里有不错的学习教程和开发工具。

python兼职资源+python全套学习资料

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值