【博客大賽】論python中器的組合

python中有幾種特殊的對象,如可迭代對象、生成器、迭代器、裝飾器等等,特別是生成器這些能夠說是python中的門面擔當,應用好這些特性的話,能夠給咱們的項目帶來本質上的提高,裝逼不說,這構築的是代碼護城河,祖傳代碼別人不再敢動。熟悉特性的概念在和麪試官交流的過程當中也是挺吃香的不是嗎?如今這麼捲了,面試官也不多會問到迭代啊、遞歸啊什麼的,反過來講,在社招面試被問到了這種看起來挺淺薄的問題,可能就是掛的節奏了:)嘿嘿,真的,畢竟面試是要有相對應的面試時間的,總要有水題來刷時間啊┑( ̄Д  ̄)┍node

三者關係

可迭代對象、迭代器和生成器這三個概念很容易混淆,前二者一般不會區分的很明顯,只是用法上有區別。生成器在某種概念下能夠看作是特殊的迭代器,它比迭代實現上更加簡潔。三者關係如圖:python

【博客大賽】論python中器的組合

可迭代對象

可迭代對象Iterable Object,簡單的來理解就是可使用for或者while來循環遍歷的對象。好比常見的 listsetdict等,能夠用如下方法來測試對象是不是可迭代面試

>>> from collections import Iterable
>>> isinstance('yerik', Iterable)     # str是否可迭代
True
>>> isinstance([5, 2, 0], Iterable)   # list是否可迭代
True
>>> isinstance(520, Iterable)       # 整數是否可迭代
False

本質

可迭代對象的本質就是能夠向咱們提供一個迭代器幫助咱們對其進行迭代遍歷使用。 可迭代對象經過 __iteration__提供一個迭代器,在迭代一個可迭代對象的時候,實際上就是先獲取該對象提供的迭代器,而後經過這個迭代器來以此獲取對象中的每個數據,這也是一個具有__iter__方法的對象,就是一個可迭代對象的緣由。算法

from collections import Iterable

class ListIter(object):
    def __init__(self):
        self.container = list()

    def add(self, item):
        self.container.append(item)

    # 能夠經過註釋如下兩行代碼來感覺可迭代對象檢測的原理
    def __iter__(self):
        pass

if __name__ == '__main__':
    listiter = ListIter()
    print(isinstance(listiter, Iterable))

經過對可迭代對象使用iter() 函數獲取此可迭代對象的迭代器,而後對取到的迭代器不斷使用next() 函數來獲取下一條數據。iter() 函數實際上就是調用了可迭代對象的__iter__方法。編程

迭代器

迭代器是用來記錄每次迭代訪問到的位置,當對迭代器使用next() 函數的時候,迭代器會返回他所記錄位置的下一個位置的數據。實際上,在使用next() 函數的時候,調用的就是迭代器對象的__next__方法。python3 要求迭代器自己也是可迭代對象,因此還要爲迭代器對象實現__iter__方法,而__iter__方法要返回一個迭代器,迭代器自己正是一個迭代器,因此迭代器的__iter__方法返回自身便可.閉包

對全部的可迭代對象調用dir() 方法時,會發現他們都默認實現了__iter__ 方法。咱們能夠經過iter(object) 來建立一個迭代器。app

>>> x = [8 ,8 ,8]
>>> dir(x)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>> y = iter(x)
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>

調用iter() 以後,建立一個list_iterator對象,會發現增長了__next__ 方法。咱們不妨斷言全部實現了__iter____next__ 兩個方法的對象,都是迭代器。ssh

>>> dir(y)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']

迭代器是帶狀態的對象,它會記錄當前迭代所在的位置,以方便下次迭代的時候獲取正確的元素。__iter__返回迭代器自身,__next__返回容器中的下一個值,若是容器中沒有更多元素了,則拋出StopIteration異常。socket

>>> next(y)
8
>>> next(y)
8
>>> next(y)
8
>>> next(y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

for 循環的本質

咱們常常會寫出如下代碼:ide

for item in obj:

實際上這行代碼執行了如下4步:

  1. 判斷obj 是否爲可迭代對象,便是否有__iter__方法
  2. 在第一步成立前提下, 系統調用iter()函數. 獲得obj對象__iter__方法的返回值,這個其實能夠本身顯式調用
  3. __iter__方法的返回值是一個迭代器,有__iter____next__方法
  4. for 不斷的調用迭代器中__next__方法並將值賦給item, 當遇到Stopiteration 的異常後循環結束.

生成器

利用迭代器,能夠在每次迭代獲取數據,經過next() 方法時按照特定的規律進行生成,可是在實現一個迭代器時,關於當前迭代到的狀態須要本身記錄,進而才能根據但前狀態生成下一個數據。爲了達到記錄當前狀態,並配合next() 函數進行迭代使用,能夠採用更簡便的語法,即生成器,其本質上是一類特殊的迭代器。

生成器和裝飾器都是python中最吸引人的兩個黑科技,生成器雖沒有裝飾器那麼經常使用,但在某些針對的情境下十分有效。

好比咱們在建立列表的時候,可能會受到內存限制(特別是在刷題的時候),容量確定是有限的,並且不可能所有給他一次枚舉出來。這裏可使用列表生成式,可是它有一個致命的缺點就是定義即生成,很是的浪費空間和效率。若是列表元素能夠按照某種算法推算出來,那咱們能夠在循環的過程當中不斷推算出後續的元素,這樣就沒必要建立完整的list,從而節省大量的空間。這種一邊循環一邊計算的機制,稱爲生成器:generator

要建立一個generator ,最簡單的方法是改造列表生成式

>>> [x*x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> (x*x for x in range(10))
<generator object <genexpr> at 0x7f8fcc3b5e60>

還有一個方法是生成器函數,一樣是經過def 定義,以後經過yield來支持迭代器協議,因此比迭代器寫起來更簡單,咱們甚至能夠下斷言道,只要在一個函數中有yield 關鍵字那麼這個函數就不是一個函數,而是生成器

>>> def spam():
...  yield "first"
...  yield "second"
...  yield "3"
...  yield 123
... 
>>> spam
<function spam at 0x7f8fd0391c80>
>>> gen = spam()
>>> gen
<generator object spam at 0x7f8fcc3b5f68>
>>> gen.__next__()
'first'
>>> gen.__next__()
'second'
>>> gen.__next__()
'3'
>>> gen.__next__()
123
>>> gen.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

固然通常都是經過for來使用的,這樣不用關心StopIteration的異常

>>> for it in spam():
...  print(it)
... 
first
second
3
123

更進一步的是將生成器和迭代器進行組合,這裏是經過iter()來實現

>>> for it in iter(spam()):
...  print(it)
... 
first
second
3
123

本質上就是在進行函數調用的時候,返回一個生成器對象。使用next() 調用的時候,遇到yield就返回,記錄此時的函數調用位置,下次調用next() 時,從斷點處開始。

說實話有的時候,迭代器和生成器很難區分,畢竟generator 是比Iterator更加簡單的實現方式。官方文檔寫到

Python’s generators provide a convenient way to implement the iterator protocol.

所以徹底能夠像使用iterator 同樣使用generator ,固然除了定義。畢竟定義一個iterator,須要分別實現__iter__() 方法和__next__() 方法,但generator 只須要一個小小的yield

此外generator 還有send()close() 方法,都是隻能在next()調用以後,生成器出去掛起狀態時才能使用的。

總的來講生成器在Python中是一個很是強大的編程結構,能夠用更少地中間變量寫流式代碼,相比其它容器對象它更能節省內存和CPU,固然它能夠用更少的代碼來實現類似的功能。如今就能夠動手重構你的代碼了,但凡看到相似:

def something():
    res = list()
    for ... in iter(...):
        res.append(x)
    return res

均可以用生成器函數來替換:

def iter_something():
    for ... in iter(...):
        yield x

python 是支持協程的,也就是微線程,就是經過generator 來實現的。配合generator 咱們能夠自定義函數的調用層次關係從而本身來調度線程。

實戰

經過兩個經典例子來真實感覺一下迭代器與生成器的妙用

斐波那契數列

用 普通函數,迭代器和生成器來實現斐波那契數列,區分三種

輸出數列的前N個數

普通函數

這個其實就是內循環,沒啥好說的,通過max次循環完成輸出

def fab(max):
    n,a,b = 0,0,1
    L = []
    while n < max:
        L.append(b)
        a,b = b,a+b
        n += 1
    return L

Iterator方法

爲了節省內存,和處於未知輸出的考慮,使用迭代器來改善代碼。

class fab(object):
    '''
    Iterator to produce Fibonacci
    '''
    def __init__(self,max):
        self.max = max
        self.n = 0
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.n < self.max:
            r = self.b
            self.a,self.b = self.b,self.a + self.b
            self.n += 1
            return r
        raise StopIteration('Done')

迭代器什麼都好,就是寫起來不簡潔。因此用 yield 來改寫第三版。

Generator

def fab(max):
    n,a,b = 0,0,1
    while n < max:
        yield b
        a,b = b,a+b
        n += 1

使用下面來輸出

for a in fab(8):
    print(a)

看起來很簡潔,並且有了迭代器的特性。

更進一步

這個是將迭代器和生成器結合起來使用,這個只須要進行一個小小的改動就有的成效

for a in iter(fab(8)):
    print(a)

樹上的應用

斐波那契數列能夠說是全部程序猿入門必經的練習,那麼對於樹的遍歷也是很是實用的小竅門,不妨看下leetcode的897. 遞增順序搜索樹這道題,按中序遍歷將其從新排列爲一棵遞增順序搜索樹,使樹中最左邊的節點成爲樹的根節點,而且每一個節點沒有左子節點,只有一個右子節點。

【博客大賽】論python中器的組合

咱們用上迭代器與生成器的組合以後獲得題解

def increasingBST(self, root: TreeNode) -> TreeNode:
        def dfs(node: TreeNode):
            if node:
                yield from dfs(node.left)
                yield node.val
                yield from dfs(node.right)

        ans = cur = TreeNode()
        for v in iter(dfs(root)):
            cur.right = TreeNode(v)
            cur = cur.right
        return ans.right

裝飾器

裝飾器Decorator是python中最吸引人的特性,其本質上仍是一個函數,它可讓已有的函數不作任何改動的狀況下增長功能。

很是適合有切面需求的場景,好比權限校驗,日誌記錄和性能測試等等。好比想要執行某個函數前記錄日誌或者記錄時間來統計性能,又不想改動這個函數,就能夠經過裝飾器來實現。

不用裝飾器,咱們會這樣來實如今函數執行前插入日誌

def test():
    print('i am tester')

def test():
    print('tester is running')
    print('i am test')

雖然這樣寫是知足了需求,可是改動了原有的代碼,若是有其餘的函數也須要插入日誌的話,就須要改寫全部的函數,不能複用代碼,爲了實現代碼複用的需求,能夠這麼改進

def use_logg(func):
    logging.warn("%s is running" % func.__name__)
    func()

def test():
    print('i am tester')

use_log(teat)    #將函數做爲參數傳入

這樣寫的確能夠複用插入的日誌,缺點就是顯示的封裝原來的函數,其實咱們更加但願透明的作這件事。用裝飾器來寫

bar = use_log(bar)def use_log(func):
    def wrapper(*args,**kwargs):
        logging.warn('%s is running' % func.__name___)
        return func(*args,**kwargs)
    return wrapper

def test():
    print('I am tester')

tester = use_log(test)
tester()

use_log() 就是裝飾器,它把真正咱們想要執行的函數test()封裝在裏面,返回一個封裝了加入代碼的新函數,看起來就像是test() 被裝飾了同樣。這個例子中的切面就是函數進入的時候,在這個時候,咱們插入了一句記錄日誌的代碼。這樣寫仍是不夠透明,經過@語法糖來起到tester = use_log(test)的做用。

bar = use_log(bar)def use_log(func):
    def wrapper(*args,**kwargs):
        logging.warn('%s is running' % func.__name___)
        return func(*args,**kwargs)
    return wrapper

@use_log
def test():
    print('I am tester')

@use_log
def haha():
    print('I am haha')

test()
haha()

這樣看起來就很簡潔,並且代碼很容易複用。能夠當作是一種智能的高級封裝。

裝飾器也是能夠帶參數的,這位裝飾器提供了更大的靈活性。

def use_log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator

@use_log(level="warn")
def test(name='tester'):
    print("i am %s" % name)

test()

其實是對裝飾器的一個函數封裝,並返回一個裝飾器。這裏涉及到做用域的概念,能夠把它當作一個帶參數的閉包。當使用@use_log(level='warn')時,會將level 的值傳給裝飾器的環境中。它的效果至關於use_log(level='warn')(test),也就是一個三層的調用。

這裏有一個美中不足,decorator 不會改變裝飾的函數的功能,但會悄悄的改變一個__name__ 的屬性(還有其餘一些元信息),由於__name__ 是跟着函數命名走的。能夠用@functools.wraps(func) 來讓裝飾器仍然使用func 的名字。好比

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

functools.wraps 也是一個裝飾器,它將原函數的元信息拷貝到裝飾器環境中,從而不會被所替換的新函數覆蓋掉。

有了裝飾器,咱們就能夠剝離出大量與函數功能自己無關的代碼,增長了代碼的複用性。

總結

  1. 容器是一系列元素的集合,如str、list、set、dict、file、sockets對象均可以看做是容器,容器均可以被迭代(用在for,while等語句中),所以他們被稱爲可迭代對象。
  2. 可迭代對象實現了__iter__方法,該方法返回一個迭代器對象。
  3. 迭代器持有一個內部狀態的字段,用於記錄下次迭代返回值,它實現了__next____iter__方法,迭代器不會一次性把全部元素加載到內存,而是須要的時候才生成返回結果。
  4. 生成器是一種特殊的迭代器,它的返回值不是經過return而是用yield
  5. 裝飾器是一種特殊的閉包,本質上是一個函數

參考資料

  1. https://wiki.python.org/moin/Generators
  2. https://www.jianshu.com/p/efaa19594cf4
  3. http://www.javashuo.com/article/p-vegjchto-do.html
相關文章
相關標籤/搜索