背景介紹在學(xué)習(xí)Python的初期,對容器,迭代器,生產(chǎn)器等概念一直沒有去梳理清楚,所以也就是一直糊里糊涂的用著Python。最近,用的多了,總是需要看一些源碼文件,想通過源碼文件來了解自己寫的代碼性能瓶頸會在哪里?此時,發(fā)現(xiàn)這些概念,我似乎需要仔細的梳理一下。梳理的過程中,會參閱一些參考文獻,畢竟站在前人的肩膀上會看的更遠嘛,在文末,會一并附上參看文獻。 contrainer 容器出自官方文檔的一句定義:Some objects contain references to other objects; these are called containers. 容器是一種把多個元素組織在一起的數(shù)據(jù)結(jié)構(gòu),容器中的元素可以逐個地迭代獲取,可以用in, not in關(guān)鍵字判斷元素是否包含在容器中。通常這類數(shù)據(jù)結(jié)構(gòu)把所有的元素存儲在內(nèi)存中(也有一些特例,并不是所有的元素都放在內(nèi)存,比如迭代器和生成器對象)那么在Python中,常見的容器有哪些?
以上常用的容器,各自都有自己的數(shù)據(jù)特點。這里本不應(yīng)該一一闡述,但多數(shù)使用者都應(yīng)該有所了解。 可修改與否?我們常用的容器,如:列表,字典是可以直接修改的,即從地址上修改存儲的內(nèi)容; 有可以修改,就有不可以修改,不然就沒有必要區(qū)別分了。不可修改的有元組tuple,類似的不可變數(shù)據(jù)類型包括整型int、浮點型float、字符串型string。 當(dāng)然不可變的容器和數(shù)據(jù)類型在我理解并不是真的不可變,如果你要修改,就是改變指針指向位置,將指針指向新的內(nèi)容位置,那么原始內(nèi)容是不變的,隨著指針的移動,就便成了廢棄的了,被程序清楚。 傳址與傳址的概念傳值是指傳入一個參數(shù)的值,傳址是指傳入一個參數(shù)的地址,也就是內(nèi)存的地址(指針)。二者的區(qū)別是如果函數(shù)里面對傳入的參數(shù)重新賦值,函數(shù)外的全局變量是否相應(yīng)改變,用傳值傳入的參數(shù)不會改變的,用傳址傳入就會改變。 Python不允許程序員顯性的傳值還是傳址操作。Python參數(shù)傳遞采用的是“傳對象引用”的方式。這種方式也相當(dāng)于傳值和傳址的綜合形式。當(dāng)函數(shù)收到的是一個可變對象(比如字典或者列表)的引用(指針),就能修改對象的原始值——相當(dāng)于傳址。如果函數(shù)收到的是一個不可變對象(比如數(shù)字、字符或者元組)的引用,就不能直接修改原始對象——相當(dāng)于傳值。 所以python的傳值和傳址是根據(jù)傳入?yún)?shù)的類型來選擇的 傳值的參數(shù)類型:數(shù)字,字符串,元組 傳址的參數(shù)類型:列表,字典 所以在寫程序時候一定要注意這一點,如果不小心就容易出現(xiàn)bug 自定義容器Python 提供了collections容器類,(待續(xù)) Iterables 可迭代對象前面說的很多容器其實都是可迭代對象,此外還有更多的對象同樣也是可迭代對象,比如處于打開狀態(tài)的files等等。凡是可以返回一個迭代器的對象都可以稱之為可迭代對象,舉一個簡單的例子:
這里x是一個可迭代對象,可迭代對象和容器一樣是一種通俗的叫法,它們具有包含關(guān)系,屬于概括的概念,并不是指某種具體的數(shù)據(jù)類型,list是可迭代對象,dict是可迭代對象,set也是可迭代對象。y和z是兩個獨立的迭代器,迭代器內(nèi)部持有一個狀態(tài),該狀態(tài)用于記錄當(dāng)前迭代所在的位置,以方便下次迭代的時候獲取正確的元素。迭代器有一種具體的迭代器類型,比如list_iterator,set_iterator??傻鷮ο髮崿F(xiàn)了__iter__方法,該方法返回一個迭代器對象。 當(dāng)運行以下代碼:
背后真實的調(diào)用過程如下圖所示: Iterators 迭代器迭代的意思就是重復(fù)的做一些事情,例如循環(huán)的形式。任何具有__next__()方法的對象都是迭代器,對迭代器調(diào)用next()方法可以獲取下一個值。next()方法不需要任何參數(shù),如果next()方法被調(diào)用時,迭代器沒有值可以返回,就會引發(fā)一個StopIteration的異常。 另外迭代器一般是__iter__() 方法返回。 所以迭代器本質(zhì)上是一個產(chǎn)生值的工廠,每次向迭代器請求下一個值,迭代器都會進行計算出相應(yīng)的值并返回。 迭代器的例子很多,例如,所有itertools模塊中的函數(shù)都會返回一個迭代器,有的還可以產(chǎn)生無窮的序列。
從一個有限序列中生成無限序列:
為了更直觀地感受迭代器內(nèi)部的執(zhí)行過程,我們定義一個迭代器,如斐波那契數(shù)列:
Fib既是一個可迭代對象(包含__iter__方法),又是一個迭代器(因為實現(xiàn)了__next__方法)。實例變量prev和curr用戶維護迭代器內(nèi)部的狀態(tài)。每次調(diào)用next()方法的時候做兩件事: 為下一次調(diào)用next()方法修改狀態(tài),為當(dāng)前這次調(diào)用生成返回結(jié)果。迭代器就像一個懶加載的工廠,等到有人需要的時候才給它生成值返回,沒調(diào)用的時候就處于休眠狀態(tài)等待下一次調(diào)用。 Generators 生成器生成器算得上是Python語言中最吸引人的特性之一,生成器其實是一種特殊的迭代器,不過這種迭代器更加優(yōu)雅。
fib就是一個普通的python函數(shù),它特殊的地方在于函數(shù)體中沒有return關(guān)鍵字,函數(shù)的返回值是一個生成器對象。當(dāng)執(zhí)行f=fib()返回的是一個生成器對象,此時函數(shù)體中的代碼并不會執(zhí)行,只有顯示或隱示地調(diào)用next的時候才會真正執(zhí)行里面的代碼。 來剖析代碼:首先,fib是一個很普通的函數(shù),但是函數(shù)中沒有return語句,函數(shù)的返回值是一個生成器。 當(dāng)調(diào)用f = fib()時,生成器被實例化并返回,這時并不會執(zhí)行任何代碼,生成器處于空閑狀態(tài),注意這里prev, curr = 0, 1并未執(zhí)行。 然后這個生成器被包含在isslice()中,而這又是一個迭代器,所以還是沒有執(zhí)行上面的代碼。 然后這個迭代器又被包含在list()中,它會根據(jù)傳進來的參數(shù)生成一個列表。所以它首先對isslice()對象調(diào)用next()方法,isslice()對象又會對實例f調(diào)用next()。 然后執(zhí)行到底11步的時候,isslice()對象就會拋出StopIteration異常,意味著已經(jīng)到達末尾了。注意生成器不會接收到第11次next()請求,后面會被垃圾回收掉。 生成器在Python中是一個非常強大的編程結(jié)構(gòu),可以用更少地中間變量寫流式代碼,此外,相比其它容器對象它更能節(jié)省內(nèi)存和CPU,當(dāng)然它可以用更少的代碼來實現(xiàn)相似的功能。現(xiàn)在就可以動手重構(gòu)你的代碼了,但凡看到類似:
都可以用生成器函數(shù)來替換實現(xiàn),效果會更好:
生成器的類型在Python中生成器有兩種類型:生成器函數(shù)以及生成器表達式。生成器函數(shù)就是包含yield參數(shù)的函數(shù)。生成器表達式與列表解析式類似。
使用set解析式也可以達到同樣的目的:
或者dict解析式:
還可以使用生成器表達式:
注意我們第一次調(diào)用next()之后,lazy_squares對象的狀態(tài)已經(jīng)發(fā)生改變,所以后面后面地調(diào)用list()方法只會返回部分元素組成的列表。 總結(jié)生成器是Python中一種非常強大的特性,它讓我們能夠編寫更加簡潔的代碼,同時也更加節(jié)省內(nèi)存,使用CPU也更加高效。 容器,迭代對象,迭代器,生成器具有著一種必然的聯(lián)系,是一種進步和推廣。 容器是一系列元素的集合,str、list、set、dict、file、sockets對象都可以看作是容器,容器都可以被迭代(用在for,while等語句中),因此他們被稱為可迭代對象。 可迭代對象實現(xiàn)了__iter__方法,該方法返回一個迭代器對象。 迭代器持有一個內(nèi)部狀態(tài)的字段,用于記錄下次迭代返回值,它實現(xiàn)了__next__和__iter__方法,迭代器不會一次性把所有元素加載到內(nèi)存,而是需要的時候才生成返回結(jié)果。 生成器是一種特殊的迭代器,它的返回值不是通過return而是用yield。 學(xué)習(xí)一個東西,梳理好內(nèi)在聯(lián)系,深層次理解一個概念很重要。 參考文獻:1.https:///posts/iterators-vs-generators/ |
|