目錄python
函數的執行須要對函數進行壓棧的,什麼是壓棧呢,簡而言之就是在函數執行時在棧中建立棧幀存放須要變量以及指針的意思。具體涉及的知識很是多,這裏就已一個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
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
字節碼含義:性能
LOAD_GLOBAL
:加載全局函數(print)LOAD_CONST
: 加載常量CALL_FUNCTION
: 函數調用POP_TOP
:彈出棧頂RETURN_VALUE
: 返回值def outer(): c = 100 def inner(): nonlocal c c += 200 return c return inner a = outer() a()
執行完後,刪除棧空間,可是因爲outer返回了內部函數inner,但並無執行,因此不會繼續壓棧,當執行a的時候,會從新壓棧,而此時內部函數已經記住了外部自由變量c,而且每次調用outer都會從新生成一個inner。測試
注意:這種狀況叫作閉包,自由變量c會被當成內部函數inner的一個屬性,被調用。優化
PS:內存兩大區域(棧,堆)。垃圾回收,清理的是堆中的空間。函數的調用就是壓棧的過程,而變量的建立都是在堆中完成的。 棧中存儲的都是堆中的內存地址的指向,棧清空,並不會使堆中的對象被清除,只是指向已經被刪除。函數,變量都是在堆內建立的,函數調用須要壓棧
線程
函數直接或者間接的調用自身就叫遞歸,遞歸須要有邊界條件、遞歸前進段、遞歸返回段,當邊界條件不知足的時候,遞歸前進,當邊界條件知足時,遞歸返回。注意:遞歸必定要有邊界條件,不然可能會形成內存溢出。指針
前面咱們學過斐波那契序列,利用遞歸函數,咱們能夠更簡潔的編寫一個計算斐波那契序列第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)爲例子:
遞歸的要求:
遞歸調用的深度不宜過深,Python對遞歸的深度作了限制,以保護解釋器,超過遞歸深度限制,則拋出RecursionError
異常。
使用
sys.getrecursionlimit()
獲取當前解釋器限制的最大遞歸深度
因爲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
通過對比,咱們發現使用遞歸雖然代碼更優美了,可是運行時間還不如咱們的普通循環的版本,這是由於遞歸重複計算了不少次,當規模到達必定程度時,那麼這個時間是成指數遞增的的。
總結一下如今的問題:
遞歸複雜
,函數反覆壓棧
,棧內存就很快溢出了。如何優化呢?前面的版本使用遞歸函數時會重複計算一些相同的數據,那麼咱們來改進一下,在代碼層面對遞歸的特性進行優化。
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)
並無進行重複計算,因此要使用遞歸,必需要考慮重複計算以及函數遞歸調用時產生的內存浪費等。
間接遞歸,就是經過別的函數,來調用函數自己,下面來看一個例子,來理解間接遞歸的概念:
def foo1(): foo2() def foo2(): foo1() foo1()
咱們能夠看到,這種遞歸調用是很是危險的,可是每每這種狀況在代碼複雜的狀況下,仍是可能發生這種調用。要用代碼規範來避免這種遞歸調用的發生。
遞歸是一種很天然的表達,符合邏輯思惟:
能不用遞歸則不用遞歸
。 沒有名字的函數,在Python中被稱爲匿名函數,考慮一下,咱們以前都是經過def語句定義函數的名字開始定義一個函數的,那麼沒有名字改如何定義?沒有名字該如何調用呢?
Python中藉助lambda表達式構建匿名函數。它的格式爲:
lambda '參數列表':'返回值' # 等於: def xxx(參數列表): 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]
還記得,咱們以前的默認值字典嗎,這裏的:
d = defaultdict(lambda :0)
,其實就等於(lambda :0)()
,即當咱們傳入任意值時都返回0
生成器指的生成器對象,能夠由生成器表達式獲得,也可使用yield關鍵字獲得一個生成器函數,調用這個函數返回一個生成器對象。
生成器函數,函數體中包含yield關鍵字的函數,就是生成器函數,調用後返回生成器對象。關於生成器對象,咱們能夠理解它就是一個可迭代對象
,是一個迭代器
,只不過它是延遲計算
的,惰性求值
的。
咱們說在函數中使用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:
這個報錯看起來是否是很熟悉?沒錯,和生成器表達式的結果是相同的,只不過生成器函數能夠寫的更加的複雜,如今咱們來看下生成器函數的執行過程。
再次執行時會執行到下一個yield語句
yield關鍵字``,和return關鍵字
在生成器場景下,不能一塊兒使用
。由於return語句會致使當前函數當即返回,沒法繼續執行,也沒法繼續獲取下一個值,而且return語句的返回值也不能被獲取到,還會產生StopIteration的異常.
再來總結一下生成器的特色:
咱們想要生成一個無限天然數的序列時,生成器就是一個很好的方式
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))
協程是生成器的一種高級方法,比進程、線程更輕量級,是在用戶空間調度函數的一種實現,Python 3 的asyncio就是協程實現,已經加入到了標準庫中,Python 3.5 使用async、await關鍵字直接原生支持寫成。協程在現階段來講比較複雜,後面會詳細進行說明,這裏提一下實現思路:
還能夠引入調度的策略來實現切換的方式
協程是一種非搶佔式調度
在Python 3.3之後,出現了yield from語法糖。它的用法是
def counter(): yield from range(10)
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))
生成器包生成器,真的是有夠懶的了!