在第一次接觸 Python 的時候,你可能寫過類似 for i in [2, 3, 5, 7, 11, 13]: print(i) 這樣的語句。for in 語句理解起來很直觀形象,比起 C++ 和 java 早期的 for (int i = 0; i < n; i ++) printf("%d\n", a[i]) 這樣的語句,不知道簡潔清晰到哪里去了。 但是,你想過 Python 在處理 for in 語句的時候,具體發(fā)生了什么嗎?什么樣的對象可以被 for in 來枚舉呢? 我們深入到 Python 的容器類型實現(xiàn)底層去走走,了解一種叫做迭代器和生成器的東西。 你肯定用過的容器、可迭代對象和迭代器 容器這個概念非常好理解。我們說過,在Python 中一切皆對象,對象的抽象就是類,而對象的集合就是容器。 列表(list: [0, 1, 2]),元組(tuple: (0, 1, 2)),字典(dict: {0:0, 1:1, 2:2}),集合(set: set([0, 1, 2]))都是容器。對于容器,你可以很直觀地想象成多個元素在一起的單元;而不同容器的區(qū)別,正是在于內部數(shù)據(jù)結構的實現(xiàn)方法。然后,你就可以針對不同場景,選擇不同時間和空間復雜度的容器。 所有的容器都是可迭代的(iterable)。這里的迭代,和枚舉不完全一樣。迭代可以想象成是你去買蘋果,賣家并不告訴你他有多少庫存。這樣,每次你都需要告訴賣家,你要一個蘋果,然后賣家采取行為:要么給你拿一個蘋果;要么告訴你,蘋果已經(jīng)賣完了。你并不需要知道,賣家在倉庫是怎么擺放蘋果的。 嚴謹?shù)卣f,迭代器(iterator)提供了一個 next 的方法。調用這個方法后,你要么得到這個容器的下一個對象,要么得到一個 StopIteration 的錯誤(蘋果賣完了)。你不需要像列表一樣指定元素的索引,因為字典和集合這樣的容器并沒有索引一說。比如,字典采用哈希表實現(xiàn),那么你就只需要知道,next 函數(shù)可以不重復不遺漏地一個一個拿到所有元素即可。 而可迭代對象,通過 iter() 函數(shù)返回一個迭代器,再通過 next() 函數(shù)就可以實現(xiàn)遍歷。for in 語句將這個過程隱式化,所以,你只需要知道它大概做了什么就行了。 來看下面這段代碼,主要向你展示怎么判斷一個對象是否可迭代。當然,這還有另一種做法,是 isinstance(obj, Iterable)。 def is_iterable(param): 通過這段代碼,你就可以知道,給出的類型中,除了數(shù)字 1234 之外,其它的數(shù)據(jù)類型都是可迭代的。 生成器,又是什么? 據(jù)我所知,很多人對生成器這個概念會比較陌生,因為生成器在很多常用語言中,并沒有相對應的模型。 這里,你只需要記著一點:生成器是懶人版本的迭代器。 我們知道,在迭代器中,如果我們想要枚舉它的元素,這些元素需要事先生成。這里,我們先來看下面這個簡單的樣例。 import os 聲明一個迭代器很簡單,[i for i in range(100000000)]就可以生成一個包含一億元素的列表。每個元素在生成后都會保存到內存中,你通過代碼可以看到,它們占用了巨量的內存,內存不夠的話就會出現(xiàn) OOM 錯誤。 不過,我們并不需要在內存中同時保存這么多東西,比如對元素求和,我們只需要知道每個元素在相加的那一刻是多少就行了,用完就可以扔掉了。 于是,生成器的概念應運而生,在你調用 next() 函數(shù)的時候,才會生成下一個變量。生成器在 Python 的寫法是用小括號括起來,(i for i in range(100000000)),即初始化了一個生成器。 這樣一來,你可以清晰地看到,生成器并不會像迭代器一樣占用大量內存,只有在被使用的時候才會調用。而且生成器在初始化的時候,并不需要運行一次生成操作,相比于 test_iterator() ,test_generator() 函數(shù)節(jié)省了一次生成一億個元素的過程,因此耗時明顯比迭代器短。 到這里,你可能說,生成器不過如此嘛,我有的是錢,不就是多占一些內存和計算資源嘛,我多出點錢就是了唄。 哪怕你是土豪,請坐下先喝點茶,再聽我繼續(xù)講完,這次,我們來實現(xiàn)一個自定義的生成器。 生成器,還能玩什么花樣? 數(shù)學中有一個恒等式,(1 + 2 + 3 + ... + n)^2 = 1^3 + 2^3 + 3^3 + ... + n^3,想必你高中就應該學過它?,F(xiàn)在,我們來驗證一下這個公式的正確性。老規(guī)矩,先放代碼,你先自己閱讀一下,看不懂的也不要緊,接下來我再來詳細講解。 def generator(k): 這段代碼中,你首先注意一下 generator() 這個函數(shù),它返回了一個生成器。 接下來的yield 是魔術的關鍵。對于初學者來說,你可以理解為,函數(shù)運行到這一行的時候,程序會從這里暫停,然后跳出,不過跳到哪里呢?答案是 next() 函數(shù)。那么 i ** k 是干什么的呢?它其實成了 next() 函數(shù)的返回值。 這樣,每次 next(gen) 函數(shù)被調用的時候,暫停的程序就又復活了,從 yield 這里向下繼續(xù)執(zhí)行;同時注意,局部變量 i 并沒有被清除掉,而是會繼續(xù)累加。我們可以看到 next_1 從 1 變到 8,next_3 從 1 變到 512。 聰明的你應該注意到了,這個生成器居然可以一直進行下去!沒錯,事實上,迭代器是一個有限集合,生成器則可以成為一個無限集。我只管調用 next(),生成器根據(jù)運算會自動生成新的元素,然后返回給你,非常便捷。 到這里,土豪同志應該也坐不住了吧,那么,還能再給力一點嗎? 別急,我們再來看一個問題:給定一個 list 和一個指定數(shù)字,求這個數(shù)字在 list 中的位置。 下面這段代碼你應該不陌生,也就是常規(guī)做法,枚舉每個元素和它的 index,判斷后加入 result,最后返回。 def index_normal(L, target): 那么使用迭代器可以怎么做呢?二話不說,先看代碼。 def index_generator(L, target): 聰明的你應該看到了明顯的區(qū)別,我就不做過多解釋了。唯一需要強調的是, index_generator 會返回一個 Generator 對象,需要使用 list 轉換為列表后,才能用 print 輸出。 這里我再多說兩句。在Python 語言規(guī)范中,用更少、更清晰的代碼實現(xiàn)相同功能,一直是被推崇的做法,因為這樣能夠很有效提高代碼的可讀性,減少出錯概率,也方便別人快速準確理解你的意圖。當然,要注意,這里“更少”的前提是清晰,而不是使用更多的魔術操作,雖說減少了代碼卻反而增加了閱讀的難度。 回歸正題。接下來我們再來看一個問題:給定兩個序列,判定第一個是不是第二個的子序列。(LeetCode 鏈接如下:https:///problems/is-subsequence/ ) 先來解讀一下這個問題本身。序列就是列表,子序列則指的是,一個列表的元素在第二個列表中都按順序出現(xiàn),但是并不必挨在一起。舉個例子,[1, 3, 5] 是 [1, 2, 3, 4, 5] 的子序列,[1, 4, 3] 則不是。 要解決這個問題,常規(guī)算法是貪心算法。我們維護兩個指針指向兩個列表的最開始,然后對第二個序列一路掃過去,如果某個數(shù)字和第一個指針指的一樣,那么就把第一個指針前進一步。第一個指針移出第一個序列最后一個元素的時候,返回 True,否則返回 False。 不過,這個算法正常寫的話,寫下來怎么也得十行左右。 那么如果我們用迭代器和生成器呢? def is_subsequence(a, b): 這簡短的幾行代碼,你是不是看得一頭霧水,不知道發(fā)生了什么? 來,我們先把這段代碼復雜化,然后一步步看。 def is_subsequence(a, b): 首先,第二行的b = iter(b),把列表 b 轉化成了一個迭代器,這里我先不解釋為什么要這么做。 接下來的gen = (i for i in a)語句很好理解,產(chǎn)生一個生成器,這個生成器可以遍歷對象 a,因此能夠輸出 1, 3, 5。而 (i in b)需要好好揣摩,這里你是不是能聯(lián)想到 for in 語句? 沒錯,這里的(i in b),大致等價于下面這段代碼: while True: 這里非常巧妙地利用生成器的特性,next() 函數(shù)運行的時候,保存了當前的指針。比如再看下面這個示例: b = (i for i in range(5)) 至于最后的 all() 函數(shù),就很簡單了。它用來判斷一個迭代器的元素是否全部為 True,如果是則返回 True,否則就返回 False. 于是到此,我們就很優(yōu)雅地解決了這道面試題。在這個技術知識點上,在實際工作的應用上,你已經(jīng)比很多人更加熟練了。繼續(xù)加油! 總結一下,今天我們講了四種不同的對象,分別是容器、可迭代對象、迭代器和生成器。
|
|
來自: hanxinhanxin > 《PYTHON》