Python 经验 - 迭代器与生成器

迭代器

  • 迭代器是访问集合内元素的一种方式(惰性),和以下标的访问方式不一样、不能返回访问;
  • 迭代协议:__iter____next__,对应Iterable和Iterator;
  • 可迭代对象只需要实现__iter__,要构造迭代器还需要实现__next__
from collections.abc import Iterable, Iterator
a = [1, 2]      # list是Iterable但不是Iterator
iter_rator = iter(a)
print (isinstance(a, Iterable))
print (isinstance(iter_rator, Iterator))

Iterable与Iterator

  • 调用iter时,首先判断是否实现__iter__,不存在则创建默认的迭代器,利用__getitem__进行遍历;
  • __iter__必须返回Iterator类型;
  • 返回迭代值是通过在Iterator类型在__next__实现,另外Iterator内部还需要维护一个索引值(不允许回退访问);
  • 要遍历某个自定义的类型,应该在其内部定义一个__iter__,再为它定义一个用于返回的迭代器对象(内部实现__next__)。

示例:

from collections.abc import Iterator

class Company(object):
    def __init__(self, employee_list):
        self.employee = employee_list
        
    def __iter__(self):
        return MyIterator(self.employee)

    # def __getitem__(self, item):
    #     return self.employee[item]

class MyIterator(Iterator):
    def __init__(self, employee_list):
        self.iter_list = employee_list
        self.index = 0

    def __next__(self):
        try:
            word = self.iter_list[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return word

if __name__ == "__main__":
    company = Company(["tom", "bob", "jane"])
    my_itor = iter(company)
    
    # while True:
    #     try:
    #         print (next(my_itor))
    #     except StopIteration:
    #         pass

    # next(my_itor)
    for item in company:
        print (item)

生成器

  • 只要存在yield关键字即为生成器函数,不能以一般函数的方式访问;
  • 生成器对象在python编译字节码的时候产生;
  • 惰性求值(不需要马上分配所有内存),每次访问都返回一个yield的值。

示例:

def fib(index):
    if index <= 2:
        return 1
    else:
        return fib(index-1) + fib(index-2)

def fib2(index):
    re_list = []
    n, a, b = 0, 0, 1
    while n<index:
        re_list.append(b)
        a, b = b, a+b
        n += 1
    return re_list

def gen_fib(index):
    n, a, b = 0, 0, 1
    while n < index:
        yield b
        a, b = b, a + b
        n += 1

for data in gen_fib(10):
    print (data)

if __name__ == "__main__":
    gen = gen_func()
    for value in gen:
        print (value)
    # re = func()

实现细节:

# python函数的工作原理
import inspect
frame = None

def foo():
    bar()

def bar():
    global frame
    frame = inspect.currentframe()

#
"""
    python.exe调用PyEval_EvalFramEx(c函数)执行foo函数时创建一个栈帧(stack frame)
    当foo调用子函数bar也会创建一个栈帧(python一切皆对象,栈帧对象,代码转化为字节码对象),
    所有的栈帧都分配在堆内存上,决定了栈帧可以独立于调用者存在。
"""
import dis
print(dis.dis(foo))

foo()
print(frame.f_code.co_name)             # bar

caller_frame = frame.f_back
print(caller_frame.f_code.co_name)      # foo


def gen_func():
    yield 1
    name = "ywh"
    yield 2
    age = 30
    return "aimei"

import dis
gen = gen_func()                # 生成器对象PyGenObject是对frame的封装(PyCodeObject + PyCodeObject)
print (dis.dis(gen))

print(gen.gi_frame.f_lasti)     # 可以在任何地方获取栈帧对象,暂停或恢复
print(gen.gi_frame.f_locals)
next(gen)
print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)
next(gen)
print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)

class company:
    def __getitem__(self, item):
        pass

from collections import UserList        # Python通过生成器实现的List(内部实现`__next__`,yield)

实例:利用生成器读取大文件

# 假设文件超大,而且只有一行(不能f.read())
def myreadlines(f, newline):
    buf = ""        # 缓存,处理已经读取的数据
    while True:
        while newline in buf:   # 缓存中的数据是否包含分隔符,避免把两个字符串读取到一个buf中
            pos = buf.index(newline)
            yield buf[:pos]     # 每次读取到分隔符的位置
            buf = buf[pos + len(newline):]
        chunk = f.read(4096)    # 每次读取4096个字符

        if not chunk:           # 文件读取完毕
            yield buf
            break
        buf += chunk

with open("input.txt") as f:
    for line in myreadlines(f, "{|}"):
        print(line)

生成器传值

启动生成器:

  • next:从生成器获取值(目的是将值向外部传出、供其他函数调用);
  • send:向生成器传传参,也可以重启生成器执行到下一个yield,返回yield的值;
  • 生成器启动、发送非None值之前,必须先预激:
    1. gen.send(None)
    2. next(gen)
  • 另外还有close(下次运行yield会抛出异常)、throw(执行函数主动抛出异常)方法。
def gen_func():
    html = yield "http://projectsedu.com"       # 获取gen.send(html)中的参数html
    print(html)         # 即send传入的值
    return "ywh"

gen = gen_func()
# url = gen.send(None)
url = next(gen)
html = "ywh"
gen.send(html)

gen.stop()                          # 生成器关闭(不能再执行yield)
gen.throw(Exception, 'message')     # 抛出特定异常

yield from

chain(合并处理可迭代对象),yield fromyield

from itertools import chain

li = [1, 2, 3]
di = {
    "ywh1": "1", "ywh2": "2"
}

for value in chain(li, di, range(5, 10)):
    print(value)
    
def my_chain(*args, **kwargs):
    for my_iterable in args:
        yield from my_iterable      # 对iterable解包,逐个yield出
        # for value in my_iterable:
            # yield value

yield from与yield的区别

def g1(iterable):                   # 不解包,直接yield range(10)
    yield iterable

def g2(iterable):                   # 对range(10)解包,逐个yield出
    yield from iterable

for value in g1(range(10)):
    print(value)
    
for value in g2(range(10)):
    print(value)

yield from还可以在调用方与子生成器之间建立一个双向通道(协程中调用另一个协程):

final_result = {}

def sales_sum(pro_name):
    total = 0
    nums = []
    while True:
        x = yield
        print(pro_name+"销量: ", x)
        if not x:
            break
        total += x
        nums.append(x)
    return total, nums
    

# 委托生成器(双向通道)
def middle(key):
    while True:
        final_result[key] = yield from sales_sum(key)
        print(key + "销量统计完成!!")

def main():
    data_sets = {
        "ywh一月收入": [1200, 1500, 3000],
        "ywh二月收入": [28, 55, 98, 108],
        "ywh三月收入": [280, 560, 778, 70],
    }
    for key, data_set in data_sets.items():
        print("start key:", key)
        m = middle(key)
        m.send(None)            # 预激middle协程
        for value in data_set:
            m.send(value)       # 给协程传递每一组的值
        m.send(None)
    print("final_result:", final_result)


if __name__ == '__main__':
    main()

实现原理

# pep380

# 1. RESULT = yield from EXPR可以简化成下面这样
# 一些说明
"""
_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象

"""

_i = iter(EXPR)  # EXPR是一个可迭代对象,_i其实是子生成器;
try:
    _y = next(_i)  # 预激子生成器,把产出的第一个值存在_y中;
except StopIteration as _e:
    _r = _e.value  # 如果抛出了`StopIteration`异常,那么就将异常对象的`value`属性保存到_r,这是最简单的情况的返回值;
else:
    while 1:  # 尝试执行这个循环,委托生成器会阻塞;
        _s = yield _y  # 生产子生成器的值,等待调用方`send()`值,发送过来的值将保存在_s中;
        try:
            _y = _i.send(_s)  # 转发_s,并且尝试向下执行;
        except StopIteration as _e:
            _r = _e.value  # 如果子生成器抛出异常,那么就获取异常对象的`value`属性存到_r,退出循环,恢复委托生成器的运行;
            break
RESULT = _r  # _r就是整个yield from表达式返回的值。

"""
1. 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法;
2. 如果子生成器支持.throw()和.close()方法,但是在子生成器内部,这两个方法都会抛出异常;
3. 调用方让子生成器自己抛出异常
4. 当调用方使用next()或者.send(None)时,都要在子生成器上调用next()函数,当调用方使用.send()发送非 None 值时,才调用子生成器的.send()方法;
"""
_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

总结:

  • 子生成器生产的值,都是直接传给调用方的;调用方通过send()发送的值都是直接传递给子生成器的;如果发送的是None,会调用子生成器的__next__()方法,如果不是 None,会调用子生成器的send()方法;
  • 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常;
  • yield from表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数;
  • 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的异常会向上 "冒泡";
  • 传入委托生成器的异常里,除了GeneratorExit之外,其他的所有异常全部传递给子生成器的throw()方法;如果调用throw()的时候出现了StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上“冒泡”;
  • 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的close()方法,没有的话就不调用。如果在调用close()的时候抛出了异常,那么就向上 "冒泡",否则的话委托生成器会抛出GeneratorExit异常。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容