你是如何深入理解 Python 的 list comprehension 就是 generator 這一點的?

此外map filter 得到的是iterator. 這一點設計的哲學如何理解呢?

另外generator 其實是generator iterators. 返回的是一個iterators . 但自己卻是一個function. iterator 卻是一個object . Python 設計的深處水好深啊


一句句回答你的疑問

此外map filter 得到的是iterator. 這一點設計的哲學如何理解呢?

就是使用了 iterator 模式啊,可以更介面化的拿數據,不暴露內部實現,容易解耦,同時有時還能節省內存等等,好處很多。好的就要用就是這個哲學哈。

另外generator 其實是generator iterators. 返回的是一個iterators . 但自己卻是一個function.

首先需要搞明白在 Python 中 iterator 和 generator 是不同的。

iterator 的定義是

The essence of the Iterator Factory method Pattern is to "Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

在 Python 中 iterator 是這樣定義的。

An object representing a stream of data. Repeated calls to the iterator』s next() method return successive items in the stream. When no more data are available a StopIteration exception is raised instead.

而 generator 的定義是

A function which returns an iterator. It looks like a normal function except that it contains yield statements for producing a series a values usable in a for-loop or that can be retrieved one at a time with the next() function.

iterator 是一種常見的設計模式在 Python 中恰好可以用 generator 這種 function 實現而已。iterator 有其他的實現方式,例如自己在類里定義__iter__ 和 next 方法。generator 也有其它很多的功能,例如模擬 Coroutines(PEP 342 -- Coroutines via Enhanced Generators)。

iterator 卻是一個object

Python 中所有的東西都是 object。

參考資料

Python Types and Objects

PEP 255 -- Simple Generators

5. Built-in Types

5. Built-in Types

Glossary — Python 2.7.8 documentation

Design Patterns


暫時還沒有用到python3,如樓上所說,在python2中map和filter返回的是list,如果python3返回iterator的話,那可能是一些設計上的改進。其實map/filter在python2.5中用的比較多,一般稱這類用法為「以數據為中心」的編程,或者「函數式」編程。此後,List Comprehension/Generator Expression成了更為推薦的寫法,據說也是借鑒於函數式編程語言Haskell,但寫法上相比於map/filter更加Pythonic。

OK,言歸正傳,鑒於對python3不是很了解,我就僅僅針對python2中的實現談談自己的理解吧。

樓上有答主已經提到:

iterator是一種設計模式,在python中恰好可以用generator function來實現

撇開「模式」這樣的詞語,用更加Pythonic的話來說就是:

在python中iterator可以視為一種協議(protocol),或者規則(regulation)

我會在下文解釋什麼是「協議」,且耐心聽我娓娓道來。

1. 碼字千萬,栗子先行

既然要討論迭代器,那就先來看看迭代器在python中的一個重要的用途:迭代

我們知道list, tuple, dict都是可迭代對象(iterable object),換句話說,我們可以通過for...in...語句塊,遍歷其中的每一個元素。比如,遍歷一個list中的元素,將它們的值列印出來:

&>&>&> alist = [1, 2, 3, 4, 5, 6, 7, 8]
&>&>&> for elem in alist:
... print elem,
...
1 2 3 4 5 6 7 8
&>&>&>

應用在tuple上,幾乎是照葫蘆畫瓢的寫法:

&>&>&> atup = (1, 2, 3, 4, 5, 6, 7, 8)
&>&>&> for elem in atup:
... print elem,
...
1 2 3 4 5 6 7 8
&>&>&>

甚至迭代一個字典,寫法也十分類似:

&>&>&> adict = {"name":"Jhon", "age":22, "job":"stu"}
&>&>&> for elem in adict:
... print elem,
...
job age name
&>&>&>

慢著,好像有哪裡不對。迭代字典的時候,只遍歷了其中的key,而沒有迭代value,這是為什麼呢?

原因是,for...in...其實是python的一顆「語法糖」。實際上,解釋器首先調用了內建的 iter() 方法,將in後的對象轉換成了迭代器,然後逐次調用迭代器的next()方法(python3中是__next__()方法), 從而遍歷每一個元素:

iter(...)
iter(collection) -&> iterator
iter(callable, sentinel) -&> iterator

我們不妨手動調用一下iter()方法,看看其中的端倪:

&>&>&> iter(alist)
& &>&>&> iter(atup)
&
&>&>&> iter(adict)
&
&>&>&>

看到 iter(adict) 的返回結果是 dictionary-keyiterator,可見返回的是一個「鍵迭代器」,因此for...in...只迭代了字典的鍵,而沒有迭代值。

有經驗的python程序員會使用字典的 iteritems() 方法來迭代「鍵-值對」:

&>&>&> for key, value in adict.iteritems():
... print key, value
...
job stu
age 22
name Jhon

再來看看 iteritems() 方法返回了什麼:

&>&>&> adict.iteritems()
&
&>&>&>

如我們所料,返回了一個 dictionary-itemiterator,即「鍵-值對迭代器」,自然既可以迭代鍵,也可以迭代值。

2. 「鴨子「類型 協議

不知題主是否注意到,iterator似乎有很多種?前面已經看到的,包括listiterator/tupleiterator/dictionary-keyiterator/dictionary-itemiterator,它們有一個共同的行為(上文我在解釋for...in...語法糖的時候已經提到了):

可以逐次調用迭代器的next()方法(python3中是__next__()方法), 從而遍歷每一個元素

實際上,一個標準的迭代器實現大概是下面這個樣子的:

class IteratorName(object):
def __init__(self, *args, **kwargs):
pass

def __iter__(self):
return self

def next(self):
pass

它包含三個主要的方法:

  • __init__()方法:用於傳入一些初始化參數,以及設置一開始的計數器位置;

  • __iter__()方法:往往都是返回自身;

  • next()方法:最為重要的一個方法,用於返回下一個迭代的元素(事實上每次調用會返回當前計數器所在的位置,同時計算下一個位置);

題主如果有自己實現過一些python的內置數據結構(比如list),那對這種形式的迭代器應該不陌生,舉很久以前我寫過的一個栗子(實現的是一個Array,可以看作是大小固定的list):

class _ArrayIterator(object):
def __init__(self, theArray):
self._arrayRef = theArray
self._curNdx = 0 #一開始的計數器位置

def __iter__(self):
return self

def next(self):
if self._curNdx &< len(self._arrayRef): item = self._arrayRef[self._curNdx] self._curNdx += 1 return item else: raise StopIteration

只需要在Array的實現中,定義__iter__()方法,並且返回一個_ArrayIterator的實例,Array的實例就成為了一個可迭代對象:

class Array(object):
def __init__(self, size):
assert size &> 0, "Array size must be &> 0"
self._size = size
PyArrayType = ctypes.py_object * size
self._elements = PyArrayType()
self.clear(None)

[...] #other code

def __iter__(self):
return _ArrayIterator(self._elements) #返回一個_ArrayIterator的實例

而如果你需要對一棵二叉搜索樹執行廣度優先遍歷,那迭代器的實現可能又是另外一副模樣:

class _BSTIter(object):
def __init__(self, curr):
self._curr = curr
self._queue = []

def __iter__(self):
return self

def next(self):
if self._curr == None:
raise StopIteration

_result = self._curr._nodeValue

if self._curr._nodeLeft != None:
self._queue.append(self._curr._nodeLeft)
if self._curr._nodeRight != None:
self._queue.append(self._curr._nodeRight)

if len(self._queue) &> 0:
self._curr = self._queue.pop(0)
else:
self._curr = None

return _result

一個_ArrayIterator實例是一個迭代器,一個_BSTIter實例也是一個迭代器。儘管他們的具體實現可能有所不同,但是都包含那三個方法,而三個方法對應呈現的行為又是一致的

尤其是next()方法,總是返回當前位置的元素,並且計算下一個迭代位置,直到沒有可迭代元素時拋出StopIteration異常

是時候引出「鴨子」類型(duck typing)的概念了,這個概念來自於一句諺語:

如果看起來像鴨子、叫起來像鴨子、走起路來也像鴨子,那它就是鴨子

看起來是一句很傻的話,卻是python很重要的設計哲學,它涉及到一些動態綁定的知識,同時又有很多python程序員充分利用這一哲學實現更好的解耦,這裡就不展開討論了。

看到這兒,不知題主是否產生了一些想法?其實我已經在很多地方給出了暗示哦,甚至將一些關鍵的詞語進行了加粗顯示,比如本節一開始那段:共同的行為,以及向上幾行:行為又是一致的這樣的辭彙。

如果題主還是沒有想到什麼,那讓我們套用剛才的那句諺語,給迭代器下個定義:

如果看起來像迭代器、叫起來像迭代器、走起路來也像迭代器,那它就是迭代器

換言之:

如果一個對象具備迭代器所具有的所有行為特徵,那它就是迭代器了

不過問題是,怎麼能具備相同的行為特徵呢?這就是靠「協議」來實現的。

協議:其實就是一些介面規範,換句話說,就是一些實現了指定的功能的方法。當你在某個類中實現了這樣的一種方法,那麼該類的實例就是滿足這種協議的。

還是拿next()方法舉例,它需要滿足一些特定的條件:返回當前值、計算下一位置、適當的時候拋出StopIteration異常。滿足了這些功能,具體你怎麼實現它就是你自己的事兒啦。

反過來思考一下,當你把實現這些功能的next()方法應用到某個對象時,就使得這個對象具備了迭代器的一些行為特徵。實際上你就是在構造一個迭代器了。

3. 迭代器 v.s. 生成器

如果題主看到這裡,對迭代器還是雲里霧裡的感覺,而對「鴨子」類型、協議這樣的詞語更是摸不著頭腦。那不妨類比一下數據結構中ADT(Abstract Data Type,抽象數據類型)的概念。

在實現某種數據結構之前,我們往往會先給出這種數據結構的完整的抽象定義,包括它所要包含的方法,以及這寫方法應該執行的操作。比如一個Stack的ADT定義可能是這樣的:

//Pseudocode

ADT Stack{
[...] //other code
push(x); //入棧
pop(); //出棧
peek(); //獲取棧頂元素
size(); //獲取棧中元素的數量
isempty(); //判空
isfull(); //判滿
[...] //other code
}

ADT給出了棧這種數據結構所應該遵守的規則(包含哪些方法及方法該做的事情),遵照這種規則實現的對象就被稱為棧。思考下,迭代器不也是這樣么?

迭代器並非某種具體的內置類型實例化得到的對象。

為了說明這一點,讓我們來看一下types模塊,這個模塊中給出了所有python解釋器認可的內嵌類型:

&>&>&> import types, pprint
&>&>&> pprint.pprint(dir(types))
["BooleanType",
"BufferType",
"BuiltinFunctionType",
"BuiltinMethodType",
"ClassType",
"CodeType",
"ComplexType",
"DictProxyType",
"DictType",
"DictionaryType",
"EllipsisType",
"FileType",
"FloatType",
"FrameType",
"FunctionType",
"GeneratorType", #生成器
"GetSetDescriptorType",
"InstanceType",
"IntType",
"LambdaType",
"ListType",
"LongType",
"MemberDescriptorType",
"MethodType",
"ModuleType",
"NoneType",
"NotImplementedType",
"ObjectType",
"SliceType",
"StringType",
"StringTypes",
"TracebackType",
"TupleType",
"TypeType",
"UnboundMethodType",
"UnicodeType",
"XRangeType",
"__builtins__",
"__doc__",
"__file__",
"__name__",
"__package__"]
&>&>&>

我們並沒有找到任何單獨標識迭代器的類型。

但你一定注意到了GeneratorType這一行,沒錯,不同於迭代器,生成器是一種實實在在的內嵌類型:

&>&>&> import types
&>&>&>
&>&>&> def gen(N): #gen只是生成器函數
... for i in range(N):
... yield i,
...
&>&>&> g = gen(10) #注意這一行,g才是生成器對象
&>&>&> type(g)
&
&>&>&>
&>&>&> type(g) == types.GeneratorType #不是某種協議或規則,而是一種實際的類型
True
&>&>&>

因此,樓主在問題中提到說 generator function 返回iterator,這樣的表述並不準確。官方文檔中對生成器有如下這樣一段描述:

When you call a generator function, it doesn』t return a single value; instead it returns a generator object that supports the iterator protocol

這裡面有三個地方需要注意,我用粗體+下劃線標識出來了。分別進行說明:

  • 生成器函數就是生成器函數,和其他函數一樣,它是一個函數對象,類型是FunctionType。不要和生成器對象混淆;

  • 生成器函數返回的是生成器對象,這是一個具體的類型實例化得到的,對應GeneratorType;

  • 生成器對象支持iterator protocol也就是上文反覆提到的迭代器協議,它支持迭代器的行為,根據duck typing的原則,你可以將生成器認為是迭代器,並且完全當作是迭代器使用;

特別的第三點,為了找到iterator protocol的證據,讓我們看看生成器對象都包含了哪些東西:

&>&>&> pprint.pprint(dir(g))
["__class__",
"__delattr__",
"__doc__",
"__format__",
"__getattribute__",
"__hash__",
"__init__", #[1]
"__iter__", #[2]
"__name__",
"__new__",
"__reduce__",
"__reduce_ex__",
"__repr__",
"__setattr__",
"__sizeof__",
"__str__",
"__subclasshook__",
"close",
"gi_code",
"gi_frame",
"gi_running",
"next", #[3]
"send",
"throw"]
&>&>&>

跟隨我的標號[1]、[2]、[3],看到那熟悉的三個方法了么?

顯然生成器和迭代器並不是同一個東西,它們有一些概念重疊的地方,但生成器還有很多迭代器不具備的能力,例如對coroutine的部分支持,如果樓主有接觸過Tornado對這點的體會就會很深厚了。

此外,關於生成器,官方文檔裡面還有另外一段描述,值得一提:

Any function containing a yield keyword is a generator function; this is detected by Python』s bytecode compiler which compiles the function specially as a result

生成器是由python的位元組碼編譯器識別並進行特殊編譯的。這種處理方式也是有別於一般的迭代器的。

這意味著生成器對象雖然包含了我們在迭代器中見到的三個方法,但這是有位元組碼編譯器處理得到的,並沒有人為去定義,我們在實現時只是使用了yield關鍵字。因此,不能將它們等價於迭代器中的對應方法。前面有關iterator protocol證據的描述,只是為了更形象的解釋。實際使用時請區別對待。

4. 一切皆對象

題主對iterator是object表示了疑惑,說明題主並沒有理解」python中一切皆對象「的道理。

雖然前面我似乎反覆在說iterator是某種概念性的東西、是協議定義的一種抽象類型或者如上一位答主說的是一種模式。但是具體到任何一種具體的實現,標準定義的類也好、或者通過生成器函數來實現,得到的實例對象和生成器對象都是對象。

不僅如此,函數也是對象、模塊也是對象,"hello,world"、[1, 2, 3]、(1, 2, 3)、{"name":"Jhon"}...,都是對象。甚至類本身也是對象,這就涉及到元類的知識了,題主可以自己去查找相關資料。

所以,毋庸置疑,任何一個具體的iterator實例都是一個object。

5. 迭代器 v.s 可迭代對象

例如你定義了一個list:

&>&>&> alist = [1, 2, 3, 4, 5]
&>&>&>

它雖然可以支持迭代,比如應用於for...in...語句塊。但是它卻不是一個迭代器,而是一個可迭代對象(iterable object)。

list類型的實例也包含一個__iter__()方法,不過不是返回的自身,而是返回了一個可以用於迭代的迭代器實例:

&>&>&> alist.__iter__()
& &>&>&>

是不是很熟悉呢?沒錯,內建的iter()就是調用了對象的__iter__()方法,返回的迭代器對象。

如果題主不是很明白我所描述的__iter__()方法,可以退回到前面,研究一下Array那個例子。

6. 題外話(duck typing的補充):類文件對象

當我們用open()函數打開一個文件,就得到了一個文件對象:

&>&>&> f = open("/home/chiyu/auto/auto.py", "r")
&>&>&>

接下來,我們就可以調用相應的方法,進行讀(read)、寫(write)、調整文件指針(seek)、獲取文件指針位置(tell)等一系列應用於文件的操作,最後我們調用close()方法,將文件關閉。

而這一系列方法一樣可以應用於StringIO對象:

&>&>&> import StringIO
&>&>&> s = StringIO.StringIO()
&>&>&> s.write("hello,world")
&>&>&> s.tell()
11
&>&>&> s.seek(-11, 2)
&>&>&> s.read()
"hello,world"
&>&>&> s.close()
&>&>&>

完全可以像操作文件一樣操作一個StringIO對象,因此,我們稱StringIO對象為」類文件「對象。這是duck typing的一個典型例子。

7. 總結

  • python中一切皆對象。沒錯,就是你所見到的一切;

  • iterator是一個抽象定義:

準確的說:滿足iterator protocol的對象就可以被認為是一個iterator;

簡單粗暴的解釋:就是包涵了__init__(),__iter__(),next()三個方法,而且實現了這三個方法對應的指定的功能的對象;

  • generator是一種具體的類型,它支持iterator protocol,因此可以被認為是迭代器,並且可以被當作迭代器使用,這完全得益於python的duck typing哲學。但是它的功能更強大,不能被簡單的認為就是iterator;

以上是我個人的一些理解,未必都準確,還需要題主自己去思考和辨別。


首先,Python 2里map和filter得到的是list。Python 3改成iterator是主要是為了省內存吧。

第二,只有一部分generator是iterator。以Python 3為例,generator的介面是send, throw這兩個函數。iterator的介面是 __next__。generator里的__next__相當於是 send(None)

&>&>&> def a():
... x = yield 1
... print(x)
...
&>&>&> g = a()
&>&>&> g.send(None)
1
&>&>&> g.send(2)
2
Traceback (most recent call last):
File "&", line 1, in &
StopIteration
&>&>&> g = a()
&>&>&> g.__next__()
1
&>&>&> g.__next__()
None
Traceback (most recent call last):
File "&", line 1, in &
StopIteration
&>&>&>

list compresion會被編譯成一個generator,是因為generator已經能表示iterator了,沒必要在bytecode里特別為iterator定義一個magic number。


謝邀.

從來沒考慮過這個問題,我也不用 Python3(我就不用我就不用我就不用)

從沒區分過 iterator 和 generator. (我估計我說的 generator 就是你說的 generator iterator,你說的 generator 我都是當成 function 的,只不過返回的是一個 generator(就是你說的 generator iterator))

我只知道,列表解析方括弧改成圓括弧舊成了生成器了. 其實 map filter 返回的是不是 generator iterator 都沒有關係,想用的時候有 itertools 啊,裡邊返回的都是 generator iterator.

PS:我覺得 bhuztez 說的挺好的- -,看來我有必要好好區分一下 iterator 和 generator.


推薦閱讀:

python楊輝三角代碼過程看不懂?
這段python代碼的意思如何理解?
如何使用爬蟲監控一系列網站的更新情況?
python已正確安裝numpy但無法調用?
我數學很差,最近報了培訓班在學Python編程,上課都能聽懂,自己寫程序的時候感覺特別難是怎麼回事?

TAG:Python | 編程 | 計算機 | Python框架 | Python入門 |