Python學習之路day4-列表生成式、生成器、Iterable和Iterator

1、列表生成式

顧名思義,列表生成式就是用於生成列表的特殊語法形式的表達式。python

1.1 語法格式

[exp for iter_var in iterable]

工做過程:程序員

1.經過iter_var迭代iterable中的每一個元素算法

2.結合迭代的元素iter_var和exp表達式計算出結果編程

3.以列表形式返回每次迭代後exp表達式的計算值數組

因而可知咱們最終獲得的是一個列表,所以整個表達式是放在列表符號[]中的。安全

以上語法格式僅僅是最簡單的列表生成式形式,實際應用中還能夠增長條件判斷(過濾)和嵌套循環等稍微複雜一些的處理邏輯,相應增長的邏輯處理就是在 每次迭代時先對迭代的對象進行條件判斷和嵌套循環處理,符合條件或處理完嵌套循環邏輯後再經過exp表達式來得到當前迭代過程當中的計算值。對應的形式以下:多線程

  • 帶條件判斷的列表生成式
[exp for iter_var in iterable if_exp]
  • 帶嵌套循環的列表生成式
[exp for iter_var_A in iterable_A for iter_var_B in iterable_B]

1.2 應用場景

經過上述對列表生成式的語法形式不難看出,列表生成式是python提供的一種快速生成一個新的列表的簡潔方式(一條語句中能夠包含條件判斷和嵌套循環)。它最主要的應用場景是:根據已存在的可迭代對象推導出一個新的list(可迭代對象便可應用於for循環的對象,下文會有更詳解的介紹)。app

1.3 應用舉例

下面結合實例來對比下使用列表生成式和不使用列表生成式的狀況:函數

  • 生成一個簡單列表

生成一個從1到10的整數組成的列表:性能

(1)不使用列表生成式

  1 list1 = []
  2 for i in range(1,11):
  3     list1.append(i)

(2)使用列表生成式

  1 list1 = [i for i in range(1, 11)]
  • 生成一個帶條件判斷的列表

生成一個從1到10之間由偶數組成的列表:

(1)不使用列表生成式

  1 list1 = []
  2 for i in range(1,11):
  3     if i % 2 == 0:
  4         list1.append(i)

(2)使用列表生成式

  1 list1 = [i for i in range(1, 11) if i % 2 == 0]
  • 生成一個帶嵌套循環的列表

計算兩個的全排列,並將結果以元組形式保存到一個新的列表中

(1)不使用列表生成式

  1 key_list = ['Python', 'PHP', 'JAVA']
  2 value_list = ['coding', 'learning']
  3 new_list = []
  4 for i in key_list:
  5     for j in value_list:
  6         new_list.append((i,j))
  7 print(new_list)

(2)使用列表生成式

  1 key_list = ['Python', 'PHP', 'JAVA']
  2 value_list = ['coding', 'learning']
  3 new_list = []
  4 new_list = [(i, j) for i in key_list for j in value_list]
  5 print(new_list)

上述多個示例充分說明,使用列表生成式明顯要更方便簡潔。

2、生成器

2.1 生成器的誕生背景

通過上文對列表生成式的講解,咱們發現列表生成式彷佛很好很強大,但任何事物都有兩面性,仔細分析列表生成式的過程和本質就能夠看出列表生成式是直接生成一個新的列表,而後把全部的元素都一次性地存放在內存中,所以存在如下缺陷:

  • 內存容量老是有限的,所以列表容量有限(這點還能接受);
  • 當列表中的元素不少時,勢必會佔用大量的內存,而若是咱們偏偏僅僅須要訪問其中的部分元素甚至說前面幾個元素時,就會形成內存的極度浪費;
  • 與第二點相對應的是,此時系統反應很慢,生成須要的列表耗時很長。

所以當元素的數量達到必定的級別時,使用列表生成式就不太明智了。怎麼解決這些問題呢?

若是列表元素能夠按照某種既定的算法推算出來,那咱們是否能夠在循環的過程當中不斷推算出後續的元素呢?這樣就沒必要建立完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱爲生成器:generator。(這段文字源自傳送門

2.2 生成器的本質理解

從上面的論述能夠提煉出生成器的如下本質過程:
生成器按照指定的算法,結合循環不斷推算出後續的元素(每循環一次就推算一個),所以並非簡單地一上來就一股腦地生成出全部的數據,而是在調用(循環)時才生成。列表的長度在動態變化,可長可短(取決於循環的次數),所以很是有利於控制對內存的佔用,可節省大量內存空間(須要使用多少就申請分配多少)。
這也使得經過生成器生成的元素幾乎是沒有限制的,相應的操做返回時間也很理想。

須要注意生成器的一個特性是,在循環推算過程當中,它只能記錄的當前的位置,並日後推算,不能返回來往前「回顧」(即只能next,不能prev)。

好了,闡述這麼多,迴歸到目的用途上,生成器也是用來生成數據的--按照既定的某種算法不斷生成後續的新的數據,直到再也不循環(調用新數據)或者說知足了某個指定的條件後結束。

2.3 生成器的構造方式

可經過如下兩種方式來構造生成器:

  • 經過相似列表生成式的方式來構造,把列表生成式中的列表符號[]替換爲函數符號()便可(指定的算法須要經過函數來定義呀)
  • 使用包含yield關鍵字的函數來構造

若是循環的邏輯算法比較簡單,可直接使用第一種方式,反之算法比較複雜時(某些列表很難用列表生成式寫出來,可是用函數就很容易實現),就只能經過第二種包含yield的函數(這是生成器與普通函數在形式上的惟一區別)來構造生成器了。

仍是經過實例來形象理解吧。

(1) 經過相似列表生成式的方式來構造

  1 gen1 = ( n for n in range(11) if n % 2 == 0)
  2 print(type(gen1))
  3 print(gen1)
  4 
  5 輸出:
  6 <class 'generator'>
  7 <generator object <genexpr> at 0x0000000001DF0830>

(2) 經過函數來構造

  1 def gen2():
  2     for i in range(11):
  3         if i % 2 == 0:
  4             yield n
  5 print(type(gen2()))
  6 print(gen2())
  7 
  8 輸出:
  9 <class 'generator'>
 10 <generator object gen2 at 0x0000000001E20830>

從這裏能夠看出對於比較簡單的推算算法,若是經過相似列表生成式的方式和函數均可以構造,那麼用相似列表生成式的方式顯然更簡單快捷。
但對於相似下面複雜的情形,咱們只能選擇經過函數來構造生成器:
好比,著名的斐波拉契數列(Fibonacci),除第一個和第二個數外,任意一個數均可由前兩個數相加獲得:1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契數列用列表生成式寫不出來,可是,用函數把它打印出來卻很容易:

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         print(b)
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'
  8 
  9 '''注意,賦值語句:a, b = b, a + b至關於:
 10 t = (b, a + b) # t是一個tuple
 11 a = t[0]
 12 b = t[1]
 13 但沒必要顯式寫出臨時變量t就能夠賦值。
 14 '''
 15 

仔細觀察,能夠看出,fib函數其實是定義了斐波拉契數列的推算規則,能夠從第一個元素開始,推算出後續任意的元素,這種邏輯其實很是相似generator。
這裏把fib函數轉換成生成器:

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'

注意這裏把普通的函數轉換成生成器的時候只有一個變化: 把原來的print轉換成了yield.
一旦一個函數定義中包含yield關鍵字,那麼這個函數就再也不是一個普通函數,而是一個generator:
<generator object fib at 0x0000000001DF0830>

2.4 訪問生成器的數據

可經過如下兩種方式來訪問生成器中的數據:

  1. 經過__next__()方法
      1 gen1 = ( n for n in range(11) if n % 2 == 0)
      2 print(gen1.__next__())
      3 print(gen1.__next__())
      4 print(gen1.__next__())
      5 print(gen1.__next__())
      6 print(gen1.__next__())
      7 print(gen1.__next__())
      8 
      9 輸出:
     10 0
     11 2
     12 4
     13 6
     14 8
     15 10
     
      1 def fib(max):
      2     n, a, b = 0, 0, 1
      3     while n < max:
      4         yield b
      5         a, b = b, a + b
      6         n = n + 1
      7     return 'done'
      8 f = fib(10)
      9 print(f.__next__())
     10 print(f.__next__())
     11 print(f.__next__())
     12 print(f.__next__())
     13 print(f.__next__())
     14 
     15 輸出:
     16 1
     17 1
     18 2
     19 3
     20 5
  2. 經過for循環去迭代
    上文__next__()方法訪問生成器的數據例子中,訪問數據很麻煩,只能一個個地去next,對於可生成不少個元素的生成器而言,須要獲取全部的元素時,__next__()顯得無能爲力。此時簡單的for循環便可搞定。
    例 1:
      1 gen1 = ( n for n in range(11) if n % 2 == 0)
      2 for i in gen1:
      3     print(i)
      4 
      5 輸出:
      6 0
      7 2
      8 4
      9 6
     10 8
     11 10

    例2:
      1 def fib(max):
      2     n, a, b = 0, 0, 1
      3     while n < max:
      4         yield b
      5         a, b = b, a + b
      6         n = n + 1
      7     return 'done'
      8 f = fib(10)
      9 for i in f:
     10     print(i)
     11 
     12 輸出:
     13 1
     14 1
     15 2
     16 3
     17 5
     18 8
     19 13
     20 21
     21 34
     22 55

2.5 關於StopIteration

上文中在訪問生成器的數據時,沒有闡述StopIteration,這裏單獨列出來。
當生成器中的數據被訪問完畢後仍然嘗試訪問時,會拋出StopIteration異常,意思是不能再迭代了。經過__next__()方法訪問生成器中的數據時可能會觸發該異常,而經過for循環不會產生該異常,緣由是for循環訪問時有明確的循環結束條件。

1. 列表生成式生成器拋出StopIteration異常:

  1 gen1 = ( n for n in range(11) if n % 2 == 0)
  2 print(gen1.__next__())
  3 print(gen1.__next__())
  4 print(gen1.__next__())
  5 print(gen1.__next__())
  6 print(gen1.__next__())
  7 print(gen1.__next__())
  8 print(gen1.__next__())  #不斷地經過__next__()方法嘗試訪問生成器的數據,直到「越界」
  9 
 10 程序輸出:
 11 0
 12 2
 13 4
 14 6
 15 8
 16 10
 17 Traceback (most recent call last):
 18   File "D:/python/S13/Day4/1.py", line 26, in <module>
 19     print(gen1.__next__())
 20 StopIteration         #最後一次嘗試訪問拋出了異常


2. yield 函數生成器拋出StopIteration異常:

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'
  8 f = fib(5)
  9 for i in range(7):
 10     print(f.__next__())
 11     i += 1
 12 
 13 輸出:
 14 1
 15 1
 16 2
 17 3
 18 5
 19 Traceback (most recent call last):
 20   File "D:/python/S13/Day4/1.py", line 51, in <module>
 21     print(f.__next__())
 22 StopIteration: done

能夠看出yield 函數生成器同樣能夠拋出StopIteration異常,在引起StopIteration異常後return定義的返回值會打印輸出。換句話說,若是想得到生成器函數的返回值,只能經過不斷地訪問生成器的數據直到拋出StopIteration。

3. 捕獲StopIteration異常
Python也具有相應的異常處理機制,這裏咱們來捕獲StopIteration異常

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'
  8 f = fib(5)
  9 while True:
 10     try:
 11         x = f.__next__()
 12         print("f:",x)
 13     except StopIteration as e:
 14         print("Generator return value:",e.value)   #當try中預期須要執行的代碼塊執行出錯時,就會執行except中的代碼塊
 15         break
 16 
 17 程序輸出:
 18 f: 1
 19 f: 1
 20 f: 2
 21 f: 3
 22 f: 5
 23 Generator return value: done

這裏只是簡單演示下如何捕獲StopIteration異常,關於異常處理的更多細節,將在後續的筆記中深刻展開。

2.6 yield的特殊性
咱們已經知道yield關鍵字能夠把一個函數轉換爲生成器,yield語句用來代替普通函數中的return來返回結果,咱們每嘗試訪問一個生成器中的元素時,若是沒有拋出異常,yield就返回一次結果。這個過程當中存在一個特殊性:yield語句每次返回結果後就掛起函數的狀態,以便下次從離開它的地方繼續執行。

聽起來彷佛不太好理解,先來一段更詳細的闡述把(引用自 https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/?cmp=dwnpr&cpb=dw&ct=dwcon&cr=cn_51CTO_dl&ccy=cn):
簡單地講,yield 的做用就是把一個函數變成一個 generator,帶有 yield 的函數再也不是一個普通函數,Python 解釋器會將其視爲一個 generator,調用 fab(5) 不會執行 fab 函數,而是返回一個 iterable 對象!在 for 循環執行時,每次循環都會執行 fab 函數內部的代碼,執行到 yield b 時,fab 函數就返回一個迭代值,下次迭代時,代碼從 yield b 的下一條語句繼續執行,而函數的本地變量看起來和上次中斷執行前是徹底同樣的,因而函數繼續執行,直到再次遇到 yield。

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         print("繼續迭代,呵呵")
  6         a, b = b, a + b
  7         n = n + 1
  8     return 'done'
  9 f = fib(5)
 10 while True:
 11     try:
 12         x = f.__next__()
 13         print("f:", x)
 14         print("我已經循環一次了,插播點廣告把") #獲取一次返回值後能夠執行其餘的任務,下一次迭代後又能繼續回到原來中斷的位置
 15     except StopIteration as e:
 16         print("Generator return value:",e.value)
 17         break
 18 
 19 輸出:
 20 f: 1
 21 我已經循環一次了,插播點廣告把
 22 繼續迭代,呵呵
 23 f: 1
 24 我已經循環一次了,插播點廣告把
 25 繼續迭代,呵呵
 26 f: 2
 27 我已經循環一次了,插播點廣告把
 28 繼續迭代,呵呵
 29 f: 3
 30 我已經循環一次了,插播點廣告把
 31 繼續迭代,呵呵
 32 f: 5
 33 我已經循環一次了,插播點廣告把
 34 繼續迭代,呵呵
 35 Generator return value: done

再來一個展現得更清楚的簡單例子:

  1 def test(min, max):
  2     for n in range(min, max):
  3         yield n
  4         print('Break point')
  5 
  6 f = test(1, 4)
  7 print(f.__next__())
  8 print(f.__next__())  # 執行兩次__next__()方法訪問生成器的元素
  9 
 10 輸出:
 11 1
 12 Break point #結果只輸出了一個斷點,也就是yield後面的程序,說明只能是第二次訪問元素時輸出的
 13 2


上述程序中咱們經過兩次執行__next()__方法來訪問生成器的元素,結果只有一個break point的輸出,這是第二次的__next__()訪問的輸出,若是註釋掉第二個next,咱們會發現輸出中沒有break point,也就是yield後面的代碼。

這足以說明yield語句執行後程序處於中斷狀態,同時保留了程序執行的狀態和位置,當咱們執行其餘任務後繼續迭代時程序還能回到以前中斷的狀態和位置,這就給了咱們經過生成器進行並行計算的機會,哈哈。

2.7 send方法與生成器並行計算

前文已經提到,能夠經過__next__()方法來訪問生成器中的元素,其實質是__next__()方法喚醒了yield(yield返回一次後保持中斷狀態),yield再返回下一次迭代的數據。for循環訪問生成器的數據也能夠視爲經過循環喚醒yield返回數據。
除此以外生成器中還有一個send()方法可用來喚醒yield,其不一樣之處在於send()不只能喚醒yield,並且能給yield傳值(該值將成爲當前yield表達式的結果)

注意:
經過send()方法來訪問生成器的元素時,send()方法第一次傳入的參數必須爲None,或者在第一次傳入參數以前先調用__next__()方法,以便生成器先進入yield表達式,不然會報錯。

下面經過實例來演示下:

  1 def consumer(name):
  2     print("%s 準備吃包子啦!"%name)
  3 
  4     while True:
  5         baozi = yield
  6         print("包子[%s]來了,被[%s]吃了" % (baozi, name))
  7 
  8 c = consumer("Maxwell")
  9 c.__next__()  # 不使用__next__()方法會報錯,也能夠用c.send(None)替代
 10 c.send("肉鬆餡")   # 調用yield,同時給yield傳一個值
 11 c.send("韭菜餡")      # 再次調用yield,同時給yield傳一個值
 12 
 13 輸出:
 14 Maxwell 準備吃包子啦!
 15 包子[肉鬆餡]來了,被[Maxwell]吃了
 16 包子[韭菜餡]來了,被[Maxwell]吃了

從上面的示例程序能夠看出,本來yield只能返回None,但經過send()方法傳入參數後,該參數直接變成yield的值返回了。

總結下send()和__next__()方法的區別:
1. __next__()方法能夠喚醒yield,只能get yield的返回值,只讀訪問生成器當前迭代的返回值;
2.send()方法不能能夠喚醒yield,還能傳值給yield,而且set yield的返回值,至關於寫覆蓋方式訪問生成器當前迭代的返回值
3.第一次使用send以前須要確保生成器已經走到yield這一步(即已經中斷過一次),可經過先執行__next__()或send(None)來確保,不然會報錯,而__next__()沒有這個限制。

下面進入生成器經過協程進行並行計算的章節。
先了解下基本理論吧(如下文字引自 http://blog.csdn.net/dutsoft/article/details/54729480):
協程,又稱微線程。英文名Coroutine。
子程序,或者稱爲函數,在全部語言中都是層級調用,好比A調用B,B在執行過程當中又調用了C,C執行完畢返回,B執行完畢返回,最後是A執行完畢。因此子程序調用是經過棧實現的,一個線程就是執行一個子程序。

協程不一樣於線程,線程是搶佔式的調度,而協程是協同式的調度,協程須要本身作調度。
子程序調用老是一個入口,一次返回,調用順序是明確的。而協程的調用和子程序不一樣。協程看上去也是子程序,但執行過程當中,在子程序內部可中斷,而後轉而執行別的子程序,在適當的時候再返回來接着執行。

協程優點是極高的執行效率。由於子程序切換不是線程切換,而是由程序自身控制,所以,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優點就越明顯。用來執行協程多任務很是合適。

協程沒有線程的安全問題。一個進程能夠同時存在多個協程,可是隻有一個協程是激活的,並且協程的激活和休眠又程序員經過編程來控制,而不是操做系統控制的。
由於協程是一個線程中執行,那怎麼利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可得到極高的性能。

Python對協程的支持是經過generator實現的。在generator中,咱們不但能夠經過for循環來迭代,還能夠不斷調用next()函數獲取由yield語句返回的下一個值。可是Python的yield不但能夠返回一個值,它還能夠接收調用者發出的參數。

來一個生產者消費者的實際例子:

  1 import time
  2 
  3 def consumer(name):
  4     print("%s 準備吃包子啦!"%name)
  5 
  6     while True:
  7         baozi = yield
  8 
  9         print("包子[%s]來了,被[%s]吃了"%(baozi,name))
 10 
 11 
 12 def producer(name):
 13     c = consumer("A")
 14     c2 = consumer("B")
 15     c.__next__()
 16     c2.__next__()
 17     print("老子準備吃包子啦!")
 18     for i in range(5):
 19         time.sleep(1)
 20         print("作了一個包子,分兩半")
 21         c.send(i)
 22         c2.send(i)
 23 
 24 producer("Maxwell")
 25 
 26 程序輸出:
 27 A 準備吃包子啦!
 28 B 準備吃包子啦!
 29 老子準備吃包子啦!
 30 作了一個包子,分兩半
 31 包子[0]來了,被[A]吃了
 32 包子[0]來了,被[B]吃了
 33 作了一個包子,分兩半
 34 包子[1]來了,被[A]吃了
 35 包子[1]來了,被[B]吃了
 36 作了一個包子,分兩半
 37 包子[2]來了,被[A]吃了
 38 包子[2]來了,被[B]吃了
 39 作了一個包子,分兩半
 40 包子[3]來了,被[A]吃了
 41 包子[3]來了,被[B]吃了
 42 作了一個包子,分兩半
 43 包子[4]來了,被[A]吃了
 44 包子[4]來了,被[B]吃了

簡單分析下執行過程:消費者實際上是一個生成器,生產者作出包子後,經過send方法把值傳遞給消費者並調用切換到消費者執行,消費者又經過yield把結果返回,並回到生產者這裏。所以生產者不只僅是給消費者傳遞包子,還會等包子被吃了的消息返回後再繼續生產下一輪的包子,每次循環是一個輪迴,在一個線程內由生成者和消費者相互協做完成,故而稱之爲協程。

3、Iterable

可直接用於for循環的對象稱爲可迭代對象,即Iterable。
屬於可迭代的數據類型有:

  • 集合數據類型:如list、tuple、dict、set、str等
  • 生成器(generator)

可經過isinstance()來判斷一個對象是不是Iterable對象(注意後面的判斷類型是Iterable):

  1 >>> from collections import Iterable
  2 >>> isinstance((),Iterable)
  3 True
  4 >>> isinstance([],Iterable)
  5 True
  6 >>> isinstance({},Iterable)
  7 True
  8 >>> isinstance('python',Iterable)
  9 True
 10 >>> isinstance((x for x in range(10)),Iterable)
 11 True
 12 >>> isinstance(100,Iterable)
 13 False
 14 >>>

4、Iterator

能夠被__next()__函數調用並不斷返回下一個值的對象稱爲迭代器(Iterator)。生成器符合這必定義,所以生成器也是一種迭代器。

如何理解迭代器(Iterator):
實際上,Python中的Iterator對象表示的是一個數據流,Iterator能夠被__next__()函數調用並不斷返回下一個數據,直到沒有數據能夠返回時拋出StopIteration異常錯誤。能夠把這個數據流看作一個有序序列,但咱們沒法提早知道這個序列的長度。同時,Iterator的計算是惰性的,只有經過__next___()函數時纔會計算並返回下一個數據。(此段內容來自 這裏
p.s.:生成器徹底符合上述特徵。

isinstance()也能夠用來判斷一個對象是不是迭代器,但須要注意的是後面的判斷類型參數是Iterator

  1 >>> from collections import Iterator
  2 >>> list1=['Python', 'Java', 'PHP']
  3 >>> isinstance(list1,Iterator)
  4 False
  5 >>> print(list1.__next__())
  6 Traceback (most recent call last):
  7   File "<stdin>", line 1, in <module>
  8 AttributeError: 'list' object has no attribute '__next__'
  9 >>> gen1=(x for x in range(11) if x % 2 == 0)
 10 >>> print(type(gen1))
 11 <class 'generator'>
 12 >>> isinstance(gen1, Iterator)
 13 True
 14 >>> isinstance('abc', Iterator)
 15 False
 16 >>>
 17 

5、Iterable、Iterator與Generator之間的關係

  1. 生成器對象既是可迭代對象,又是迭代器
    生成器對象可直接用於for循環,同時能夠被__next()__函數調用並不斷返回下一個值,直到沒有數據能夠返回時拋出StopIteration異常錯誤。所以生成器同時符合可迭代對象和迭代器的定義。
  2. 迭代器必定是可迭代對象,反之則不必定
    迭代器可直接用於for循環,所以必定是可迭代對象,但可迭代對象不必定能被__next()__函數調用並不斷返回下一個值。例如list、dict、str等集合數據類型是可迭代對象,但不能經過__next__()函數調用,因此不是迭代器。可是它們能夠經過iter()函數轉換爲一個迭代器對象。
      1 >>> from collections import Iterator
      2 >>> list1=['Python', 'Java', 'PHP']
      3 >>> isinstance(iter(list1),Iterator)
      4 True
      5 >>> isinstance(list1,Iterator)
      6 False
      7 >>> print(list1.__next__())
      8 Traceback (most recent call last):
      9   File "<stdin>", line 1, in <module>
     10 AttributeError: 'list' object has no attribute '__next__'
     11 >>>
相關文章
相關標籤/搜索