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 在涉及到基於協程和生成器的併發編程中扮演着更加劇要的角色。

相關文章
相關標籤/搜索