生成器和迭代器
迭代是处理数据的重要环节,基本上对大量数据的处理上,我们都需要对数据进行迭代操作,如何在节省内存开销且高效地去对数据迭代,这就是生成器存在的意义。
迭代器实现了next方法,返回序列中的下一个元素;如果没有元素了,那么抛出 StopIteration 异常。另外迭代器实现了iter方法,用于返回迭代器本身。
所有生成器都是迭代器,会生成传给 yield 关键字的表达式的值。调用生成器函数返回生成器,而生成器可以产出值。同样,当没有值可以产出时,会抛出 StopIteration 异常。
yield from iterator
这个语法多用于嵌套生成器。它有两种用法:一,从生成器中读取数据;二,创建通道,把内层生成器直接与外层生成器联系起来,把生成器当协程使用。
先看看 yield from 的基本用法,通过将生成器拆分为多个生成器,可以轻松地对其进行重构。
版本一:不使用 yield from
def generator2():
for i in range(10):
yield i
def generator3():
for j in range(10, 20):
yield j
def generator():
for i in generator2():
yield i
for j in generator3():
yield j
版本二:我们用 yield from 来改写上面的 generator():
def generator():
yield from generator2() # 对for循环进行重构,这个版本使用`yield from`减少了手动循环
yield from generator3()
如果你想在生成器中调用其他生成器作为子例程,yield from 这个时候非常有用。
如果你不使用它的话,那么就必须写额外的 for 循环了。
当生成器有了 send 方法
生成器对象有几个重要的 API,send,throw,close,这些在协程中
先从一个简单的协程例子讲起:
def accumulator():
total = 0
while True:
print("Total is ", total)
# 生成器的调用方可以使用`.send(...)`方法发送数据,发送的数据会成为`yield`表达式的值。
# 所以,这里的input是从send传进来的值,而非yield产出的值
# yield total相当于函数return total, 只不过这个函数不是真正的return,而是在这个位置挂起等待下一次调用。
input = yield total
print("Send: ", input)
total += input
print("Adding %d ... => Total is %d"%(input, total))
>>> gen = accumulator()
>>> next(gen) # 激活协程,计算停在`yield`,也可以使用gen.send(None)激活协程
Total is 0
0
>>> gen.send(1) # 从刚刚停留的位置开始,传入数据开始计算
Send: 1
Adding 1 ... => Total is 1
Total is 1 # 运行又停在`yield`处,等待传入新数值
0 # 这是yield total的产生结果,就相当于函数最后的return,比方说,你去调用一个用return返回的函数,最后都会输出return的结果。
>>> gen.send(12)
Send: 12
Adding 12 ... => Total is 13
Total is 13
13
>>> try:
... gen.throw(ValueError) # 或调用gen.close()终止协程
...except ValueError:
... pass
>>> gen.send(12)
StopIteration
使用协程的基本步骤为:
- 创建协程对象
- 调用 next 函数,激活协程
- 调用.send(...) 方法,推动协程执行并产出
- 调用方可以通过.close(...) 或.throw() 方法终止协程,如果继续推进协程会抛出 StopIteration。
生成器的调用方可以使用 .send(...) 方法发送数据,它的参数会成为暂停的 yield 整个表达式 的值。
协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。
前面提到 yield from 主要是用于嵌套的生成器,所以,把它当做协程的时候,它的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。
子生成器
def accumulator():
total = 0
while True:
input = yield
if input is None: # 如果调用方传入None,这里就跳出循环,total重新置零
return total # 子生成器可以返回结果给外层生成器middleware
total += input
yield from
所在的函数相当于一个管道,将调用方client和子生成器accumulator串联起来
它的职责是负责传递信息以及异常处理,而子生成器就专职做自己该做的事。
def middleware(results):
while True:
result = yield from accumulator()
results.append(result)
# 调用方(客户端)
def client():
results = [] # 用于收集结果
counter = middleware(results)
next(counter) # 激活协程
for i in range(5):
counter.send(i)
counter.send(None) # 关闭当前的生成器对象,协程重置
for i in range(3):
counter.send(i)
counter.send(None)
print(results)
>>> client()
[10, 3]
内置函数 iter ()
和iter函数用法不一样的地方在于,参数形式变了,而且你可以去控制迭代。
iter(obj) 传进去是一个可迭代对象,而 iter(func, sentinel) 的第一个参数是可调用对象,多数情况下即函数对象,第二个参数是一个哨符,用于指示迭代器去抛出 StopIteration。如果可迭代对象的返回值等于这个哨符,那么迭代器停止。
>>> from random import randint
>>> def get_number(): # 无参
... return randit(1, 6) # 随机返回一个在1~6范围内的整数
>>> iter_num = iter(get_number, 2) # iter的第一个参数是一个可调用对象
>>> for num in iter_num: # 直到get_number()返回2,停止循环
... print(num)
魔法函数call怎么使用
使用 Python 的魔术方法,可以以一种简单的方法来让对象可以表现的像内置类型一样。比如如果一个类定义了名为getitem() 的方法,并且x为该类的一个实例,则x[i]基本就等同于 type(x).getitem(x, i)。也就是说,你需要在自定义的类中实现一些接口,但是你可以只实现部分接口,这样你就可以去对新序列对象访问单个元素,迭代,in运算。同样,你希望你的新类型创建的实例可以像函数对象一样被调用,那么就必须实现call。这是魔法方法的最大优势。
掌握一些基本的魔法方法,可以让你创建出与其他 Python 特性无缝集成的类,这是非常有必要的。
魔法函数call可以模拟可调用对象,这个方法在实例作为一个函数被 “调用” 时被调用;如果定义了此方法,则 x(arg1, arg2, ...) 就相当于 x.call(arg1, arg2, ...) 的快捷方式。
魔法方法非常强大,但是一般情况下,我们是不需要直接调用魔法方法,只有在定制的类中,你可以去重写它们。
class MyClass:
def __call__(self, *args):
print(*args)
>>> obj = MyClass()
>>> obj(123) # 可以像调用函数一样去调用obj对象
对于模拟其它内置类型需要使用到的魔法方法,可以参考官方文档 - 魔法方法,这里就不一一展开了。特别提一下几个非常常用的:
__new__
-
__str__
,__repr__
__iter__
-
__getitem__
,__setitem__
,__delitem__
-
__getattr__
,__setattr__
,__delattr__
__call__
网友评论