11 - 函數的執行流程-函數遞歸-匿名函數-生成器

1 函數的執行流程

函數的執行須要對函數進行壓棧的,什麼是壓棧呢,簡而言之就是在函數執行時在棧中建立棧幀存放須要變量以及指針的意思。具體涉及的知識很是多,這裏就已一個Python腳本簡單進行分析。閉包

def foo1(b, b1=3):
    print('call foo1', b, b1)

def foo2(c):
    foo3(c)
    print('call foo2', c)

def foo3(d):
    print('call foo3', d)

def main():
    print('call main')
    foo1(100, 101)
    foo2(20)
    print('main ending')

main()

當咱們運行上面代碼時,它的執行流程以下:async

  1. 全局棧幀中生成foo一、foo二、foo三、main函數對象
  2. main函數調用
  3. main中查找內建函數print壓棧,將常量字符串壓棧,調用函數,彈出棧頂
  4. main中全局查找函數foo1壓棧,將常量100、101壓棧,調用函數foo1,建立棧幀。print函數壓棧,字符串和變量b、b1壓棧,調用函數,彈出棧頂,返回值。
  5. main中全局查找foo2函數壓棧,將常量200壓棧,調用foo2,建立棧幀。foo3函數壓棧,變量c引用壓棧,調用foo3,建立棧幀。foo3完成print函數調用返回。foo2恢復調用,執行print語句後,返回值。main中foo2調用結束後彈出棧頂,main繼續執行print函數調用,彈出棧頂,main函數返回

1.1 字節碼瞭解壓棧過程

        Python 代碼先被編譯爲字節碼後,再由Python虛擬機來執行字節碼, Python的字節碼是一種相似彙編指令的中間語言, 一個Python語句會對應若干字節碼指令,虛擬機一條一條執行字節碼指令, 從而完成程序執行。Python dis 模塊支持對Python代碼進行反彙編, 生成字節碼指令。下面針對上面的例子經過字節碼理解函數調用時的過程。函數

import dis
print(dis.dis(main))


# ======> result
 53           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('call main')
              4 CALL_FUNCTION            1
              6 POP_TOP

 54           8 LOAD_GLOBAL              1 (foo1)
             10 LOAD_CONST               2 (100)
             12 LOAD_CONST               3 (101)
             14 CALL_FUNCTION            2
             16 POP_TOP

 55          18 LOAD_GLOBAL              2 (foo2)
             20 LOAD_CONST               4 (20)
             22 CALL_FUNCTION            1
             24 POP_TOP

 56          26 LOAD_GLOBAL              0 (print)
             28 LOAD_CONST               5 ('main ending')
             30 CALL_FUNCTION            1
             32 POP_TOP
             34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

字節碼含義:性能

  1. LOAD_GLOBAL:加載全局函數(print)
  2. LOAD_CONST: 加載常量
  3. CALL_FUNCTION: 函數調用
  4. POP_TOP:彈出棧頂
  5. RETURN_VALUE: 返回值

1.2 嵌套函數的壓棧

def outer():
    c = 100
    def inner():
        nonlocal c
        c += 200
        return c
    return inner

a = outer()
a()
  1. 函數只有在執行的時候纔會壓棧,因此在outer執行時,會開闢棧空間壓棧(c,inner)
  2. 執行完後,刪除棧空間,可是因爲outer返回了內部函數inner,但並無執行,因此不會繼續壓棧,當執行a的時候,會從新壓棧,而此時內部函數已經記住了外部自由變量c,而且每次調用outer都會從新生成一個inner。測試

    注意:這種狀況叫作閉包,自由變量c會被當成內部函數inner的一個屬性,被調用。優化

PS:內存兩大區域(棧,堆)。垃圾回收,清理的是堆中的空間。函數的調用就是壓棧的過程,而變量的建立都是在堆中完成的。 棧中存儲的都是堆中的內存地址的指向,棧清空,並不會使堆中的對象被清除,只是指向已經被刪除。函數,變量都是在堆內建立的,函數調用須要壓棧線程

2 遞歸

        函數直接或者間接的調用自身就叫遞歸,遞歸須要有邊界條件、遞歸前進段、遞歸返回段,當邊界條件不知足的時候,遞歸前進,當邊界條件知足時,遞歸返回。注意:遞歸必定要有邊界條件,不然可能會形成內存溢出。指針

2.1 遞歸函數

        前面咱們學過斐波那契序列,利用遞歸函數,咱們能夠更簡潔的編寫一個計算斐波那契序列第N項,或者前N項的代碼:代碼規範

在數學上,斐波納契數列以以下被以遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)

# 公式版本  
def fib(n):
    if n < 3:
        return 1
    return fib(n-1) + fib(n-2)


# 公式版本之簡潔版
def fib(n):
    return 1 if n < 3 else fib(n-1) + fib(n-2)

不少人可能不明白其中原理,這裏簡要說明一下,以fib(6)爲例子:

  1. fib(6) 返回 fib(5) + fib(4)
  2. fib(5) 返回 fib(4) + fib(3)
  3. fib(4) 返回 fib(3) + fib(2)
  4. fib(3) 返回 fib(2) + fib(1)
  5. fib(2),fib(1) 是邊界,return 1,而後逐級調用返回

re_fib

遞歸的要求:

  • 遞歸必定要有退出條件,遞歸調用必定要執行到這個退出條件。沒有退出條件的遞歸調用,就是無限調用
  • 遞歸調用的深度不宜過深,Python對遞歸的深度作了限制,以保護解釋器,超過遞歸深度限制,則拋出RecursionError異常。

    使用sys.getrecursionlimit()獲取當前解釋器限制的最大遞歸深度

2.2 遞歸的性能

        因爲Python是預先計算等式右邊的,因此咱們發現,上圖中,重複計算了fib(4)fib(3)那麼效率呢?因爲只是計算了fib(6),若是fib(35)呢?能夠預想,它要重複計算多少次啊。這裏咱們來測試一下它執行的時間。

# 遞歸版本
import datetime

def fib(n):
    return 1 if n < 3 else fib(n - 2) + fib(n - 1)

start = datetime.datetime.now()
fib(35)
total_seconds = (datetime.datetime.now() - start).total_seconds()
print(total_seconds)   # 1.628643



# 循環版本
def fib(n):
    a = 1
    b = 1
    count = 2
    while count < n:
        a, b = b, a + b
        count += 1
    return b

start = datetime.datetime.now()
print(fib(35))
total_seconds = (datetime.datetime.now() - start).total_seconds()
print(total_seconds)  # 0.0

        通過對比,咱們發現使用遞歸雖然代碼更優美了,可是運行時間還不如咱們的普通循環的版本,這是由於遞歸重複計算了不少次,當規模到達必定程度時,那麼這個時間是成指數遞增的的。

總結一下如今的問題:

  1. 循環稍微複雜一點,可是隻要不是死循環,能夠屢次迭代直至算出結果
  2. fib函數代碼極簡易懂,可是隻要獲取到最外層的函數調用,內部跌過結果都是中間結果。並且給定一個n都要進行近2n次遞歸,深度越深,效率越低。爲了獲取斐波那契數量須要在外面套一個n次的循環,效率就更低了。
  3. 遞歸還有深度限制,若是遞歸複雜函數反覆壓棧,棧內存就很快溢出了。

2.3 遞歸的優化

        如何優化呢?前面的版本使用遞歸函數時會重複計算一些相同的數據,那麼咱們來改進一下,在代碼層面對遞歸的特性進行優化。

def fib(n, a=1, b=1):
    a, b = b, a + b
    if n < 3:
        return b
    return fib(n - 1, a, b)

        代碼優化後,發現運行時間很快,由於計算的是fib(n),fib(n-1)..fib(1)並無進行重複計算,因此要使用遞歸,必需要考慮重複計算以及函數遞歸調用時產生的內存浪費等。

2.4 間接遞歸

間接遞歸,就是經過別的函數,來調用函數自己,下面來看一個例子,來理解間接遞歸的概念:

def foo1():
    foo2()

def foo2():
    foo1()

foo1()

        咱們能夠看到,這種遞歸調用是很是危險的,可是每每這種狀況在代碼複雜的狀況下,仍是可能發生這種調用。要用代碼規範來避免這種遞歸調用的發生。

2.5 遞歸總結

遞歸是一種很天然的表達,符合邏輯思惟:

  • 運行效率低,每一次調用函數都要開闢棧幀。
  • 有深度限制,若是遞歸層次太深,函數反覆壓棧,棧內存很快就溢出了。
  • 若是是有限次數的遞歸,可使用遞歸調用,或者使用循環代替,雖然代碼稍微複雜一點,可是隻要不是死循環,能夠屢次疊代直至算出結果。
  • 絕大多數遞歸,均可以使用循環實現,能不用遞歸則不用遞歸

3 匿名函數

        沒有名字的函數,在Python中被稱爲匿名函數,考慮一下,咱們以前都是經過def語句定義函數的名字開始定義一個函數的,那麼沒有名字改如何定義?沒有名字該如何調用呢?
        Python中藉助lambda表達式構建匿名函數。它的格式爲:

lambda '參數列表':'返回值'


# 等於:
def xxx(參數列表):
    return 返回值

須要注意的是:

  1. 冒號左邊是參數裂變,但不要括號。
  2. 冒號右邊是函數體,但不能出現等號。
  3. 函數體只能寫一行,不能使用分號分隔多個語句。(也被稱爲單行函數)
  4. return語句,不寫return關鍵字

下面來看一下各類匿名函數的寫法

(lambda x,y: x + y)(4,5)    # 9
(lambda x,y=10: x+y)(10)    # 20
(lambda x,y=10: x+y)(x=10)  # 20
(lambda x,y=10: x+y)(10,y=10)  # 20
(lambda x,y=10,*args: x+y)(10,y=10)  # 20    
(lambda x,y=10,*args,m,n,**kwargs: x+y)(10,y=10)   # 20  
(lambda *args:(i for i in args)(1,2,3,4,5)    # generate<1,2,3,4,5>
(lambda *args:(i for i i in args))(*range(5))  # generate<1,2,3,4,5>
[ x for x in (lambda *args: (i for i in args))(*range(5)) ] # [1,2,3,4,5]
[ x for x in (lambda *args: map(lambda x:x+1,(i for i in args)))(*range(5))] # [2,3,4,5,6]
  • lambda是一匿名函數,咱們在它後面加括號就表示函數調用
  • 在高階函數傳參時,使用lambda表達式,每每能簡化代碼

還記得,咱們以前的默認值字典嗎,這裏的:d = defaultdict(lambda :0),其實就等於(lambda :0)(),即當咱們傳入任意值時都返回0

4 Python生成器

        生成器指的生成器對象,能夠由生成器表達式獲得,也可使用yield關鍵字獲得一個生成器函數,調用這個函數返回一個生成器對象。
        生成器函數,函數體中包含yield關鍵字的函數,就是生成器函數,調用後返回生成器對象。關於生成器對象,咱們能夠理解它就是一個可迭代對象,是一個迭代器,只不過它是延遲計算的,惰性求值的。

4.1 基本結構

        咱們說在函數中使用yield關鍵字來返回數據的函數,叫作生成器函數,那麼咱們來寫一個生成器函數,看看和return函數有什麼區別

In [87]: def func():
    ...:     for i in range(2):
    ...:         yield i
    ...:
In [90]: g = func()

In [91]: next(g)
Out[91]: 0

In [92]: next(g)
Out[92]: 1

In [93]: next(g)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-93-e734f8aca5ac> in <module>
----> 1 next(g)

StopIteration:

        這個報錯看起來是否是很熟悉?沒錯,和生成器表達式的結果是相同的,只不過生成器函數能夠寫的更加的複雜,如今咱們來看下生成器函數的執行過程。

  1. 當函數執行過程當中遇到yield函數時,會暫停,並把yield表達式的值返回。
  2. 再次執行時會執行到下一個yield語句

    yield關鍵字``,和return關鍵字在生成器場景下,不能一塊兒使用。由於return語句會致使當前函數當即返回,沒法繼續執行,也沒法繼續獲取下一個值,而且return語句的返回值也不能被獲取到,還會產生StopIteration的異常.

再來總結一下生成器的特色:

  1. 包含yield語句的生成器函數調用生成生成器對象的時候,生成器函數的函數體不會當即執行。
  2. next(genreator) 會從函數的當前位置向後執行到以後碰到的一個yield語句,會彈出值,並暫停函數執行。
  3. 再次調用next函數,和上一條同樣的處理結果
  4. 繼續調用哪一個next函數,生成器函數若是結束執行了(顯示或隱式調用了return語句),會拋出StopIteration異常

4.2 使用場景

咱們想要生成一個無限天然數的序列時,生成器就是一個很好的方式

def counter():
    c = 0
    while True:
        c += 1
        yield c 
c = counter()

In [95]: next(c)
Out[95]: 1

In [96]: next(c)
Out[96]: 2

In [97]: next(c)
Out[97]: 3

又或者前面的斐波那契序列,咱們也能夠利用生成器的特色,惰性計算。

def fib(n, a=0, b=1):
    for i in range(n):
        yield b
        a, b = b, a + b

print(list(fib(5)))

或者包含全部斐波那契序列的生成器

def fib():
    a = 0
    b = 1
    while True:
        yield b
        a, b = b, a + b

g = fib()
for i in range(101):
    print(next(g))

4.3 協程coriutine

        協程是生成器的一種高級方法,比進程、線程更輕量級,是在用戶空間調度函數的一種實現,Python 3 的asyncio就是協程實現,已經加入到了標準庫中,Python 3.5 使用async、await關鍵字直接原生支持寫成。協程在現階段來講比較複雜,後面會詳細進行說明,這裏提一下實現思路:

  • 有兩個生成器A、B
  • next(A)後,A執行到了yield語句後暫停,而後去執行next(B),B執行到yield語句也暫停,而後再次調用next(A),再次調用next(B)
  • 周而復始,就實現了調度的效果
  • 還能夠引入調度的策略來實現切換的方式

    協程是一種非搶佔式調度

4.4 yield from

在Python 3.3之後,出現了yield from語法糖。它的用法是

def counter():
    yield from range(10)
  • yield from 後面須要一個可迭代對象
  • yield from iterable 實際上等同於 for item in iterable: yield item

固然yield from也能夠結合生成器來使用,由於生成器也是一個可迭代對象啊。

def fib(n):
    a = 0
    b = 1
    for i in range(n):
        yield b
        a,b = b,a+b

def counter():
    yield from fib(10)

g = counter()
print(list(g))

生成器包生成器,真的是有夠懶的了!

相關文章
相關標籤/搜索