python可迭代对象

自己实现了迭代方法的对象称之为可迭代对象,可迭代对象特色:php

  • 支持每次返回本身所包含的一个成员的对象;
  • 对象实现了 __iter__ 方法:
  • 全部数据结构都是可迭代对象;
  • for 循环要求对象必须是一个可迭代对象;
  • 用户自定义的一些包含了 __iter__()__getitem__() 方法的类。

它与通常的序列类型(list, tuple 等)有什么区别呢?它一次只返回一个数据项,占用更少的内存,但它须要记住当前的状态,以便返回下一数据项。python

迭代器

迭代器(iterator)就是一种可迭代对象。所谓的 迭代器就是重复作一件事,它又称为游标(cursor),它是程序设计的软件设计模式,是一种可在容器物件(container,如列表等)上实现元素遍历的接口。迭代器是一种特殊的数据结构,在 python 中,它也是以对象的形式存在的。算法

简单来讲,在 python2 中存在 next 方法的可迭代对象是迭代器;而在 python3 中则变成了 __next__ 方法。所以迭代器同时具备 __iter____next__ 这两种方法。shell

经过 python 内置函数 iter 能够将一个可迭代对象转换成一个迭代器。为何要将可迭代对象转换成迭代器呢?由于只有迭代器才能使用 python 内置函数 next。编程

迭代器会保存一个指针,指向可迭代对象的当前元素。调用 next 函数的时候,会返回当前元素,并将指针指向下一个元素。当没有下一个元素的时候,它会抛出 StopIteration 异常。json

一个简单的迭代器用法:设计模式

lst = [['m', 2, 4, 5], ['x', 3, 4, 5]]

for x in lst:
    key = x[0]
    for v in x[1:]:
        print()

for x in lst:
    it = iter(x)
    key = next(it)
    for v in it:
        print()
复制代码

使用第二种循环也就是迭代器会比第一种更有效率,由于切片将列表复制一份,占用的内存更多。api

for 循环对于可迭代对象首先会调用 iter 方法将之转换为迭代器,而后不断的调用 next 方法,直到抛出 StopIteration 异常。数组

it = iter(itratable)
while True:
    try:
        next(it)
    except StopIteration:
        return
复制代码

生成器

生成器也是函数,函数中只要有 yield 关键字,那么它就是生成器函数,返回值为生成器。生成器存在 __iter____next__ 这两种方法,所以它是一个迭代器。生成器应用很是普遍,官方的异步 IO 基本上都是基于 yield 作的。当咱们在 async def 定义的函数中使用 yield,那么这个函数就被称为异步生成器。缓存

当咱们调用生成器函数的时候,它会返回一个生成器对象,咱们要使用一个变量去接受它,而后经过操做这个变量去操做生成器。生成器也是函数,函数都是从上到下执行,当执行到 yield 语句时,这个函数就中止了,而且会将这次的返回值返回。若是 yield 语句后没有任何值,那么它的返回值就是 None;若是有值,会将这个值返回给调用者。若是使用了生成器的 send 方法(下面会提到),那么返回值将是经过这个方法传递进去的值(前提是 yield 语句后没有任何值)。

全部的这些特性让生成器看起来和协程很是类似:能够屡次调用、有多个切入点、执行能够被暂停。惟一的区别是生成器函数没法控制 yield 以后应继续执行的位置,由于控制权在调用者的手中。

对于一个没有调用结束的生成器,咱们可使用 close 方法将其关闭,能够将其写在 try 的 finally 语句中。

当使用 yield from <expr> 时,它将提供的表达式视为子迭代器,该子迭代器生成的全部值直接传递给当前生成器函数的调用者。任何传递给 send() 的值和经过throw() 传入的异常都会传递给基础迭代器(若是它有适当的方法去接收)。若是不是这种状况,那么 send() 会引起 AttributeError 或 TypeError,而 throw() 会当即引起传入的异常。

定义一个生成器:

>>> def fn():
...     for i in range(10):
...         yield i
...
>>> fn() # 能够看到它是一个生成器
Out[3]: <generator object fn at 0x7f667fa5d0a0>
>>> f = fn() # 咱们得先接收这个生成器
>>> next(f) # 而后再对生成器进行操做
Out[6]: 0
>>> next(f)
Out[7]: 1
复制代码

从函数的执行流程中能够知道,函数执行完毕以后现场应该被销毁,可是生成器却并非这样。

执行流程剖析,先定义一个函数:

>>> def g1():
...     print('a')
...     yield 1
...     print('b')
...     yield 2
...     print('c')
...     return 3
...
>>> g = g1() # 没有输出 a,证实执行生成器函数的时候不会执行函数体
>>> g # 能够看出是一个生成器,证实 return 没有生效
Out[10]: <generator object g1 at 0x7f667e6fa990>
复制代码

经过 next 函数执行一把生成器:

>>> next(g) # 执行到第一个 yield 后,中止执行
a
Out[11]: 1
复制代码

再执行一次:

>>> next(g) # 从第一个 yield 以后执行,到第二个 yield 中止
b
Out[12]: 2
复制代码

继续执行:

>>> next(g) # 从第二个 yield 以后执行,当没有更多 yield 以后,抛出异常,异常的值正好是函数的返回值
c # 下面的语句仍是会执行的
Traceback (most recent call last):
  File "/usr/local/python/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-13-5f315c5de15b>", line 1, in <module>
    next(g)
StopIteration: 3
复制代码

生成器函数的特色:

  • 生成器函数执行的时候并不会执行函数体;
  • 当 next 生成器的时候,会从当前代码执行到以后的第一个 yield,会弹出值并暂停函数;
  • 当再次 next 生成器的时候,从上次暂停处开始向下执行;
  • 当没有多余 yield 的时候,会抛出 StopIteration 异常,异常的 Value 是函数的返回值。

生成器是惰性求值的。好比咱们能够定义一个计数器:

def make_inc():
    def counter():
        x = 0
        while True:
            x += 1
            yield x
    c = counter()
    return lambda: next(c)

>>> incr = make_inc()
>>> incr()
Out[9]: 1
>>> incr()
Out[10]: 2
复制代码

求斐波那契数列第 11 项:

def fib():
    a = 0
    b = 1
    while True:
        a, b = b, a+b
        yield a

>>> f = fib()
>>> for _ in range(10):
...     next(f)
...
>>> print(next(f))
89
复制代码

能够看到递归均可以经过生成器来解决,而且没有递归深度的限制,也没有递归慢的缺点,由于它不须要保存现场。

以上都只是生成器的普通用法,协程才是生成器的高级用法。

进程和线程的调度是经过操做系统完成的,可是协程的调度是由用户态,也就是用户进行的。一旦函数执行到 yield 以后,它会暂停,暂停也就意味着让出 cpu 了。那么接下来就由用户决定执行什么代码。

当咱们要对一个可迭代对象的前一项或几项作特殊处理时,若是直接对其进行循环的话,咱们还须要判断是否是其第一个元素,或许咱们还要在其外部定义一个计数器,这实际上是一种和古老和 low 的方式。有了生成器以后,咱们就能够在循环以前使用 next() 函数取出其中的第一个值,而后再对其进行 for 循环便可。若是没法对其直接使用 next 方法,那就调用它的 __iter__() 方法将其变成一个生成器后再继续。

yield

函数中一旦使用了 yield,这个函数就变成了生成器函数。但 yield 不能和 return 共存,而且 yield 只能定义在函数中。当咱们调用这个函数的时候,函数内部的代码并不当即执行,这个函数只是返回一个生成器对象。当咱们使用 for 对其进行迭代的时候,函数内的代码才会被执行。

python3 新增了 yield from 语法,它至关于 for + yield。好比:

yield from a()

# 等同于下面
for i in a():
    yield i
复制代码

yield 和 return 的区别:

return 的时候这个函数的局部变量都被销毁了;
全部 return 是获得全部结果以后的返回;
yield 是产生了一个能够恢复的函数(生成器),恢复了局部变量;
生成器只有在调用 .next() 时才运行函数生成一个结果。
复制代码

yield 会记住函数执行的位置,下次再次执行时会从上次的位置继续向下执行。而若是在函数中使用 return,函数就直接退出了,没法继续执行。定义一个生成器:

>>> def fun1(n):
...     for i in xrange(n):
...         yield i
...
复制代码

先执行一下:

>>> a = fun1(5)
>>> a.next()
0
复制代码

而后再对其进行循环会从以前的地方继续向下:

>>> for i in a:print i
...
1
2
3
4
复制代码

yield 的用处在于若是函数每次循环都会产生一个字串,若是想要将这些字串都传递给函数外的其余变量使用 return 是不行的,由于当函数第一次循环时碰到 return 语句整个函数就退出了,是不可能继续循环的,也就是说只能传递一个字串出去。这显然不符合咱们的要求,这时就能够经过 yield 搞定了。

实现xrange:

def xrange(n):
    start = 0
    while True:
        if start >= n:
            return
        yield start
        start += 1
复制代码

具体案例:

import csv
from pyzabbix import ZabbixAPI

zapi = ZabbixAPI('http://127.0.0.1/api_jsonrpc.php')
zapi.login('uxeadmin', 'Uxe(00456)AdmIN.^??')

with open('_zabbix.csv', 'w', encoding='gbk') as f:
    spamwriter = csv.writer(f)
    for i in zapi.host.get(output=["host"]):
        item_info = zapi.item.get(hostids=i['hostid'], output=["name", 'status']).__iter__()
        for j in item_info:
            if not int(j['status']):
                spamwriter.writerow([i['host'], j['name']])
                break
        for j in item_info:
            if not int(j['status']):
                spamwriter.writerow(['', j['name']])
复制代码

生成器方法

请注意,在生成器已经执行时调用下面的任何生成器方法会引起 ValueError 异常。

__next__

开始执行一个生成器或者从上一次 yield 语句后继续执行。当使用该方法继续(注意是继续而不是第一次执行)时,那么当前 yield 的返回值为 None,直到执行到下一次的 yield 语句时,yield 语句后的表达式的结果才会返回给调用者。当迭代器结束时会抛出 StopIteration 异常。

该方法会被 for 以及内置函数 next 隐式的调用。

send

继续执行生成器(注意是继续而不是第一次执行),并发送一个值到生成器函数。send 方法的参数是下一个 yield 语句的返回值,前提是 yield 语句中要事先接收它传递的参数。若是使用该方法启动(也就是第一次执行)生成器,必须使用 None 做为其参数,由于此时尚未 yield 可以接收它的值(毕竟接收该值的语句尚未开始执行)。

def fn():
    a = 0
    while True:
        a += 1
        r = yield # r 就是接收 send 参数的变量
        print('{} => {}'.format(a, r))

>>> f = fn()
>>> f.send('a') # 不传递 None 的后果
Traceback (most recent call last):
  File "/opt/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2910, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-31-6f758a7cad28>", line 1, in <module>
    f.send('a')
TypeError: can't send non-None value to a just-started generator
>>> next(f) # 也能够不传递 None 而是使用 next 执行,两种方式均可以
>>> f.send('a')
1 => a
>>> f.send('b')
2 => b
复制代码

throw

用法:

throw(type[, value[, traceback]])
复制代码

传递一个 type 类型的异常给生成器,在生成器暂停的时候抛出,而且返回下一次 yield 的值。

close

在生成器函数暂停的位置引起 GeneratorExit。若是生成器函数正常退出,已经关闭,或者引起 GeneratorExit(没有捕获该异常),关闭返回给调用者;若是生成器产生一个值,则引起一个 RuntimeError;若是生成器引起其余异常,则传播给调用者;若是生成器因为异常或正常退出而退出,则 close() 不执行任何操做。

示例

>>> def echo(value=None):
...     print("Execution starts when 'next()' is called for the first time.")
...     try:
...         while True:
...             try:
...                 value = (yield value) # 无论 yield 后面是否有表达式,value 的值都是 send 传递进来的参数
...             except Exception as e:
...                 value = e
...     finally:
...         print("Don't forget to clean up when 'close()' is called.")
...
>>> generator = echo(1)
>>> print(next(generator))
Execution starts when 'next()' is called for the first time.
1
>>> print(next(generator))
None
>>> print(generator.send(2))
2
>>> generator.throw(TypeError, "spam")
TypeError('spam',)
>>> generator.close()
Don't forget to clean up when 'close()' is called.
复制代码

生成器解析

python3 中的 range 函数就是一个典型的生成器,不管给它一个多么大的数,它占用内存始终很小。可是下面的代码会返回一个占用空间很大的列表:

[x ** 2 for x in range(100000)]
复制代码

当咱们想让它返回的结果也像生成器同样能够将中括号换成小括号:

>>> (x ** 2 for x in range(100000))
<generator object <genexpr> at 0x7fb246656620>
复制代码

使用 next 函数就能够查看里面的每一个值,固然 for 循环也能够。

所以将列表解析的中括号变成小括号就是生成器的语法。

生成器解析其实就是列表解析的扩展,当咱们明确须要使用小标访问的时候,使用列表解析。而若是只须要对结果进行迭代的时候,优先使用生成器解析。

还有一个场景,就是要对结果进行缓存的时候,就只能使用列表解析了。不过使用生成器解析的场景确实要比列表解析来的多。

暴露生成器内的对象

若是你想让你的生成器暴露外部状态给用户, 别忘了你能够简单的将它实现为一个类,而后把生成器函数放到 __iter__() 方法中过去。好比:

from collections import deque

class linehistory:
    def __init__(self, lines, histlen=3):
        self.lines = lines
        self.history = deque(maxlen=histlen)

    def __iter__(self):
        for lineno, line in enumerate(self.lines, 1):
            self.history.append((lineno, line))
            yield line

    def clear(self):
        self.history.clear()
复制代码

为了使用这个类,你能够将它当作是一个普通的生成器函数。然而,因为能够建立一个实例对象,因而你能够访问内部属性值,好比 history 属性或者是 clear() 方法。代码示例以下:

with open('somefile.txt') as f:
    lines = linehistory(f)
    for line in lines:
        if 'python' in line:
            for lineno, hline in lines.history:
                print('{}:{}'.format(lineno, hline), end='')
复制代码

若是行中包含了 python 这个关键字,那就打印该行和前三行的行号以及内容。

关于生成器,很容易掉进函数无所不能的陷阱。若是生成器函数须要跟你的程序其余部分打交道的话(好比暴露属性值,容许经过方法调用来控制等等),可能会致使你的代码异常的复杂。若是是这种状况的话,能够考虑使用上面介绍的定义类的方式。在 __iter__() 方法中定义你的生成器不会改变你任何的算法逻辑。因为它是类的一部分,因此容许你定义各类属性和方法来供用户使用。

一个须要注意的小地方是,若是你在迭代操做时不使用 for 循环语句,那么你得先调用 iter() 函数。好比:

>>> f = open('somefile.txt')
>>> lines = linehistory(f)
>>> next(lines)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: 'linehistory' object is not an iterator

>>> # Call iter() first, then start iterating
>>> it = iter(lines)
>>> next(it)
'hello world\n'
>>> next(it)
'this is a test\n'
>>>
复制代码

生成器切片

你想获得一个由迭代器生成的切片对象,可是标准切片操做并不能作到。函数 itertools.islice() 正好适用于在迭代器和生成器上作切片操做。好比:

>>> def count(n):
...     while True:
...         yield n
...         n += 1
...
>>> c = count(0)
>>> c[10:20]
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable

>>> # Now using islice()
>>> import itertools
>>> for x in itertools.islice(c, 10, 20):
...     print(x)
...
10
11
12
13
14
15
16
17
18
19
>>>
复制代码

迭代器和生成器不能使用标准的切片操做,由于它们的长度事先咱们并不知道(而且也没有实现索引)。函数 islice() 返回一个能够生成指定元素的迭代器,它经过遍历并丢弃直到切片开始索引位置的全部元素。而后才开始一个个的返回元素,并直到切片结束索引位置。

这里要着重强调的一点是 islice() 会消耗掉传入的迭代器中的数据。必须考虑到迭代器是不可逆的这个事实。因此若是你须要以后再次访问这个迭代器的话,那你就得先将它里面的数据放入一个列表中。

跳过可迭代对象开始部分

你想遍历一个可迭代对象,可是它开始的某些元素你并不感兴趣,想跳过它们。itertools 模块中有一些函数能够完成这个任务。首先介绍的是 itertools.dropwhile() 函数。使用时,你给它传递一个函数对象和一个可迭代对象。它会返回一个迭代器对象,丢弃原有序列中直到函数返回 Flase 以前的全部元素,而后返回后面全部元素。

为了演示,假定你在读取一个开始部分是几行注释的源文件。好比:

>>> with open('/etc/passwd') as f:
... for line in f:
...     print(line, end='')
...
##
# User Database
#
# Note that this file is consulted directly only when the system is running
# in single-user mode. At other times, this information is provided by
# Open Directory.
...
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
...
>>>
复制代码

若是你想跳过开始部分的注释行的话,能够这样作:

>>> from itertools import dropwhile
>>> with open('/etc/passwd') as f:
...     for line in dropwhile(lambda line: line.startswith('#'), f):
...         print(line, end='')
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
...
>>>
复制代码

这个例子是基于根据某个测试函数跳过开始的元素。若是你已经明确知道了要跳过的元素的个数的话,那么可使用 itertools.islice() 来代替。好比:

>>> from itertools import islice
>>> items = ['a', 'b', 'c', 1, 4, 10, 15]
>>> for x in islice(items, 3, None):
...     print(x)
...
1
4
10
15
>>>
复制代码

在这个例子中,islice() 函数最后那个 None 参数指定了你要获取从第 3 个到最后的全部元素。若是 None 和 3 的位置对调,意思就是仅仅获取前三个元素,这个跟切片的相反操做 [3:][:3] 原理是同样的。

函数 dropwhile()islice() 其实就是两个帮助函数,为的就是避免写出下面这种冗余代码:

with open('/etc/passwd') as f:
    # Skip over initial comments
    while True:
        line = next(f, '')
        if not line.startswith('#'):
            break

    # Process remaining lines
    while line:
        # Replace with useful processing
        print(line, end='')
        line = next(f, None)
复制代码

跳过一个可迭代对象的开始部分跟一般的过滤是不一样的。好比,上述代码的第一个部分可能会这样重写:

with open('/etc/passwd') as f:
    lines = (line for line in f if not line.startswith('#'))
    for line in lines:
        print(line, end='')
复制代码

这样写确实能够跳过开始部分的注释行,可是一样也会跳过文件中其余全部的注释行。换句话讲,咱们的解决方案是仅仅跳过开始部分知足测试条件的行,在那之后,全部的元素再也不进行测试和过滤了。

最后须要着重强调的一点是,本节的方案适用于全部可迭代对象,包括那些事先不能肯定大小的,好比生成器,文件及其相似的对象。

展开嵌套的序列

你想将一个多层嵌套的序列展开成一个单层列表,能够写一个包含 yield from 语句的递归生成器来轻松解决这个问题。好比:

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types):
            yield from flatten(x)
        else:
            yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]
# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
    print(x)
复制代码

在上面代码中,isinstance(x, Iterable) 检查某个元素是不是可迭代的。若是是的话,yield from 就会返回全部子例程的值。最终返回结果就是一个没有嵌套的简单序列了。

额外的参数 ignore_types 和检测语句 isinstance(x, ignore_types) 用来将字符串和字节排除在可迭代对象外,防止将它们再展开成单个的字符。 这样的话字符串数组就能最终返回咱们所指望的结果了。好比:

>>> items = ['Dave', 'Paula', ['Thomas', 'Lewis']]
>>> for x in flatten(items):
...     print(x)
...
Dave
Paula
Thomas
Lewis
>>>
复制代码

以前提到的对于字符串和字节的额外检查是为了防止将它们再展开成单个字符。若是还有其余你不想展开的类型,修改参数 ignore_types 便可。

最后要注意的一点是,yield from 在涉及到基于协程和生成器的并发编程中扮演着更加剧要的角色。

相关文章
相关标签/搜索