资讯专栏INFORMATION COLUMN

流畅的python读书笔记-第十四章-可迭代的对象、迭代器和生成器

kohoh_ / 575人阅读

摘要:可迭代的对象迭代器和生成器理念迭代是数据处理的基石。可迭代的对象与迭代器的对比从可迭代的对象中获取迭代器标准的迭代器接口有两个方法。此外,也没有办法还原迭代器。最终,函数的定义体返回时,外层的生成器对象会抛出异常这一点与迭代器协议一致。

可迭代的对象、迭代器和生成器 理念

迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据
项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)。

看个例子

import re
import reprlib

RE_WORD = re.compile("w+")


class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __getitem__(self, index):
        return self.words[index]

    def __len__(self):
        return len(self.words)

    def __repr__(self):
        return "Sentence(%s)" % reprlib.repr(self.text)


s = Sentence(""The time has come," the Walrus said,")
print(s)

for world in s:
    print(world)
for 循环就是 用到了index, index在__getitem__被用到了
序列可以迭代的原因 iter函数

解释器需要迭代对象 x 时,会自动调用 iter(x)。

内置的 iter 函数有以下作用。

(1) 检查对象是否实现了 iter 方法,如果实现了就调用它,获取一个迭代器。

(2) 如果没有实现 iter 方法,但是实现了 getitem 方法,Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。

(3) 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“C object is not iterable”(C对象不可迭代),其中 C 是目标对象所属的类。

可迭代的对象与迭代器的对比

Python 从可迭代的对象中获取迭代器

标准的迭代器接口有两个方法。

next
  返回下一个可用的元素,如果没有元素了,抛出 StopIteration 异常。
iter
  返回 self,以便在应该使用可迭代对象的地方使用迭代器,例如在 for 循环中。

abc.Iterator 类

from  abc import abstractmethod

class Iterator(Iterable):
    __slots__ = ()

    @abstractmethod
    def __next__(self):
        "Return the next item from the iterator. When exhausted, raise StopIteration"
        raise StopIteration

    def __iter__(self):

        return self

    @classmethod
    def __subclasshook__(cls, C):

        if cls is Iterator:
            if (any("__next__" in B.__dict__ for B in C.__mro__) and
                    any("__iter__" in B.__dict__ for B in C.__mro__)):
                return True

        return NotImplemented
如何使用 next(...) 函数使用迭代器
import re
import reprlib

RE_WORD = re.compile("w+")


class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __getitem__(self, index):
        return self.words[index]

    def __len__(self):
        return len(self.words)

    def __repr__(self):
        return "Sentence(%s)" % reprlib.repr(self.text)


# s = Sentence(""The time has come," the Walrus said,")
# print(s)
#
# for world in s:
#     print(world)

## it就是构造后的迭代器
## iter(s3) 就是构建迭代器的可迭代对象。
s3 = Sentence("Pig and Pepper")
it = iter(s3)

print(next(it))
print(next(it))
print(next(it))

# 报错 StopIteration
# print(next(it))

print(list(it))

print(list(iter(s3)))
print(list(iter(s3)))

因为迭代器只需 nextiter 两个方法,所以除了调用 next() 方法,以及捕获 StopIteration 异常之外,没有办法检查是否还有遗留的元素。

此外,也没有办法“还原”迭代器。

如果想再次迭代,那就要调用 iter(...),传入之前构建迭代器的可迭代对象。

传入迭代器本身没用,因为前面说过 Iterator.__iter__ 方法的实现方式是
返回实例本身,所以传入迭代器无法还原已经耗尽的迭代器。

典型的迭代器
import re
import reprlib

RE_WORD = re.compile("w+")


class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return "Sentence(%s)" % reprlib.repr(self.text)

    def __iter__(self):
        return SentenceIterator(self.words)


class SentenceIterator:
    def __init__(self, words):
        self.words = words
        self.index = 0

    def __next__(self):
        try:
            word = self.words[self.index]
        except IndexError:
            raise StopIteration()

        self.index += 1
        return word

    def __iter__(self):
        return self

s = Sentence(""The time has come," the Walrus said,")
for word in s:
    print(word)

❶ 与前一版相比,这里只多了一个 iter 方法。这一版没有 getitem 方法,为
的是明确表明这个类可以迭代,因为实现了 iter 方法。
❷ 根据可迭代协议,__iter__ 方法实例化并返回一个迭代器。

Sentence 类中,__iter__ 方法调用 SentenceIterator 类的构造方法创建一个迭代器并将其返回。
为什么 不写在一起

把Sentence变成迭代器:坏主意

构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。

要知道,可迭代的对象有个 iter 方法,每次都实例化一个新的迭代器;
而迭代器要实现 next 方法,返回单个元素,此外还要实现 iter 方法,返回迭代器本身。

因此,迭代器可以迭代,但是可迭代的对象不是迭代器。

除了 iter 方法之外,你可能还想在 Sentence 类中实现 next 方法,让
Sentence 实例既是可迭代的对象,也是自身的迭代器。

可是,这种想法非常糟糕。根据有大量 Python 代码审查经验的 Alex Martelli 所说,这也是常见的反模式。

python的正确解决之道

生成器函数

import re
import reprlib

RE_WORD = re.compile("w+")


class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return "Sentence(%s)" % reprlib.repr(self.text)

    def __iter__(self):
        for word in self.words:
            yield word
        return


s = Sentence(""The time has come," the Walrus said,")
for word in s:
    print(word)

❸ 这个 return 语句不是必要的;这个函数可以直接“落空”,自动返回。不管有没有
return 语句,生成器函数都不会抛出 StopIteration 异常,而是在生成完全部值之后会直接退出。
❹ 不用再多带带定义一个迭代器类!

迭代器其实是生成器对象,每次调用 iter 方法都会自动创建,因为这里的 iter 方法是生成器函数。
生成器函数的工作原理

只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂

def gen_123():  # ➊
    yield 1  # ➋
    yield 2
    yield 3

print(gen_123)

print(gen_123())

for i in gen_123():  # ➎
    print(i)

g = gen_123()  # ➏
print(next(g))
print(next(g))
print(next(g))

## 报错 StopIteration
# print(next(g))

❸ 仔细看,gen_123 是函数对象。
❻ 为了仔细检查,我们把生成器对象赋值给 g。
❽ 生成器函数的定义体执行完毕后,生成器对象会抛出 StopIteration 异常。

把生成器传给next(...) 函数时,生成器函数会向前,执行函数定义体中的下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂停。

最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。

惰性实现
import re
import reprlib

RE_WORD = re.compile("w+")


class Sentence:
    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return "Sentence(%s)" % reprlib.repr(self.text)

    def __iter__(self):
        # 返回一个迭代器
        for match in RE_WORD.finditer(self.text):
            yield match.group()

re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个生成
器,按需生成 re.MatchObject 实例。

如果有很多匹配,re.finditer 函数能节省大量内存。
我们要使用这个函数让第 4 版 Sentence 类变得懒惰,即只在需要时才生成下一个单词。

❶ 不再需要 words 列表。
❷ finditer 函数构建一个迭代器,包含 self.text 中匹配 RE_WORD 的单词,产出
MatchObject 实例。
❸ match.group() 方法从 MatchObject 实例中提取匹配正则表达式的具体文本。

生成器表达式

生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表,而是返回一个生成器,按需惰性生成元素。

def gen_AB():  # ➊
    print("start")
    yield "A"
    print("continue")
    yield "B"
    print("end.")


res1 = [x * 3 for x in gen_AB()]

for i in res1:  # ➌
    print("-->", i)

res2 = (x * 3 for x in gen_AB())  # ➍
print(res2)  # ➎

for i in res2:  # ➏
    print("-->", i)

❷ 列表推导迫切地迭代 gen_AB() 函数生成的生成器对象产出的元素:"A" 和 "B"。注意,下面的输出是 start、continue 和 end.。

❸ 这个 for 循环迭代列表推导生成的 res1 列表。

❹ 把生成器表达式返回的值赋值给 res2。只需调用 gen_AB() 函数,虽然调用时会返回
一个生成器,但是这里并不使用。

❺ res2 是一个生成器对象。
❻ 只有 for 循环迭代 res2 时,gen_AB 函数的定义体才会真正执行。
for 循环每次迭代时会隐式调用 next(res2),前进到 gen_AB 函数中的下一个yield 语句。

注意,gen_AB 函数的输出与 for 循环中 print 函数的输出夹杂在一起。

何时使用生成器表达式

根据我的经验,选择使用哪种句法很容易判断:如果生成器表达式要分成多行写,我倾向
于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用。

8 另一个示例:等差数列生成器
class ArithmeticProgression:
    def __init__(self, begin, step, end=None):  # ➊
        self.begin = begin
        self.step = step
        self.end = end  # None -> 无穷数列


def __iter__(self):
    result = type(self.begin + self.step)(self.begin)  # ➋
    forever = self.end is None  # ➌
    index = 0
    while forever or result < self.end:  # ➍
        yield result  # ➎
    index += 1
    result = self.begin + self.step * index  # ➏

❸ 为了提高可读性,我们创建了 forever 变量,如果 self.end 属性的值是 None那么forever 的值是 True,因此生成的是无穷数列。
❹ 这个循环要么一直执行下去,要么当 result 大于或等于 self.end 时结束。如果循环退出了,那么这个函数也随之退出。

在示例 14-11 中的最后一行,我没有直接使用 self.step 不断地增加 result,

而是选择使用 index 变量,把 self.begin 与 self.step 和 index 的乘积加,计算 result 的各个值,

以此降低处理浮点数时累积效应致错的风险。

标准库里有大量的生成器轮子 page408 深入分析iter函数

可是,iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调
用的对象创建迭代器。

这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;

第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。

iter 函数掷骰子,直到掷出 1 点为止

from random import randint

def d6():
    return randint(1, 6)

d6_iter = iter(d6, 1)

print(d6_iter)

for roll in d6_iter:
    print(roll)
总结

迭代器 都有 iter这个方法(函数), (每个都调用__getitem__做兼容) next()到底.

for每次迭代都是next()

对付大文件,大内存. 先返回迭代器对象, 在执行迭代器的具体 操作

python 用yield 作为生成器的特征

send可以发送给 那个状态的生成器

iter别的用法 有两个参数 第二个是哨符 (遇到就停了)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/42044.html

相关文章

  • 《java 8 实战》读书笔记 -第十四章 函数式编程技巧

    摘要:但是,最好使用差异化的类型定义,函数签名如下其实二者说的是同一件事。后者的返回值和初始函数的返回值相同,即。破坏式更新和函数式更新的比较三的延迟计算的设计者们在将引入时采取了比较特殊的方式。四匹配模式语言中暂时并未提供这一特性,略。 一、无处不在的函数 一等函数:能够像普通变量一样使用的函数称为一等函数(first-class function)通过::操作符,你可以创建一个方法引用,...

    nemo 评论0 收藏0
  • 流畅python读书笔记-第十六章-携(协)程

    摘要:当前状态可以使用函数确定,该函数会返回下述字符串中的一个。解释器正在执行。打印消息,然后协程终止,导致生成器对象抛出异常。实例运行完毕后,返回的值绑定到上。 协程 协程可以身处四个状态中的一个。 当前状态可以使用inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个。 GEN_CREATED  等待开始执行。 GEN_RUNNING  解...

    wanglu1209 评论0 收藏0
  • 流畅python读书笔记-第一章Python 数据模型

    摘要:第一章数据类型隐式方法利用快速生成类方法方法通过下标找元素自动支持切片操作可迭代方法与如果是一个自定义类的对象,那么会自己去调用其中由你实现的方法。若返回,则会返回否则返回。一个对象没有函数,解释器会用作为替代。 第一章 python数据类型 1 隐式方法 利用collections.namedtuple 快速生成类 import collections Card = collec...

    tomener 评论0 收藏0
  • 流畅python读书笔记-第一章Python 数据模型

    摘要:第一章数据类型隐式方法利用快速生成字典方法方法通过下标找元素自动支持切片操作可迭代方法与如果是一个自定义类的对象,那么会自己去调用其中由你实现的方法。若返回,则会返回否则返回。一个对象没有函数,解释器会用作为替代。 第一章 python数据类型 1 隐式方法 利用collections.namedtuple 快速生成字典 import collections Card = coll...

    FullStackDeveloper 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<