當前位置:
首頁 > 知識 > Python :淺析 return 和 finally 共同挖的坑

Python :淺析 return 和 finally 共同挖的坑

(點擊

上方藍字

,快速關注我們)




來源:Lin_R


segmentfault.com/a/1190000010701665


如有好文章投稿,請點擊 → 這裡了解詳情




初識 return



相信每一個用過Python函數的童鞋, 肯定會用過return語句, return顧名思義, 就是用來返回值給調用者, 例如:





def

test

()

:

    

a

=

2


    

return

a


 


s

=

test

()

print

s


 


# 輸出結果


2




對於上面的結果, 相信大家都不會感到意外, 那麼加大點難度, 如果在return語句還有代碼呢? 那句代碼會怎樣呢?





def

test

()

:


    

a

=

2


    

return

a


    

s

=

3

    

print

s


 


s

=

test

()


print

s


 


# 結果是什麼?




老司機肯定一眼就能看出結果, 但是對於尚在入門或者對return不很了解的童鞋, 可能就會懵逼了~ 後面的兩句代碼是否會被執行?




答案是:

不會執行




return正如它的名字那樣, 當執行這句代碼, 整個函數都會返回, 整個調用就算結束了~ 所以在return後面的代碼, 都是不會被執行的!




也正因為這個特性, 所以有種編碼規範叫early return的編碼規範就被倡導。




它的意思大概就是: 當條件已經滿足返回時, 就馬上返回




舉個例子來說明:





def

test

()

:


    

a

=

2


    

if

a

>

2

:


        

result

=

"more than"


    

else

:


        

result

=

"less than"


    

return

result


 


s

=

test

()


print

s




上面的代碼應該比較容易理解, 就是根據a的值, 來決定返回的result是什麼. 這樣的編碼相信也是大部分童鞋喜歡用的, 因為這樣比較符合我們直覺, 然而, 這樣寫似乎有點浪費, 因為當第一個判斷結束了, 如果結果為真, 就應該返回more than, 然後結束函數, 否則肯定就是返回less than, 所以我們可以把代碼調整成這樣:





def

test

()

:


    

a

=

2


    

if

a

>

2

:


        

return

"more than"


    

else

:


        

return

"less than"


 


s

=

test

()


print

s




甚至是:





def

test

()

:


    

a

=

2


    

if

a

>

2

:


        

return

"more than"


    

return

"less than"


 


s

=

test

()


print

s




結果都是和第一個寫法是一樣的! 第一次看到這樣寫法的童鞋, 可能會覺得比較難以接受, 甚至覺得可讀性很差, 但是其實這樣的寫法, 我覺得反而會稍微好點. 因為:






  1. 運行的代碼數少了, 調用方能更快得到結果



  2. 有利於減少嵌套的層數, 便於理解.




對於第2點在這需要解釋下, 很多時候我們寫得代碼, 嵌套很深, 都是因為if/else的鍋, 因為嵌套的if/else 比較多, 所以導致一堆代碼都嵌套得比較深, 這樣對於其他小夥伴, 簡直就是災難, 因為他們很可能在閱讀這部分代碼時, 就忘了前面的邏輯….




為了更加容易理解, 舉個代碼例子:





def

test

()

:


    

a

=

2


    

if

a

>

2

:


        

result

=

"not 2"


    

else

:


        

a

+=

2


        

if

a

<

2

:


            

result

=

"not 2"


        

else

:


            

for

i

in

range

(

2

)

:


                

print

"test ~"


            

result

=

"Target !"


    

return

result


 


s

=

test

()


print

s


 


# 輸出結果


test

~


test

~


Target

!




代碼簡化優化版:





def

test

()

:


    

a

=

2


    

if

a

>

2

:


        

return

"not 2"


    


    

a

+=

2


    

if

a

<

2

:


        

return

"not 2"


    


    

for

i

in

range

(

2

)

:


        

print

"test ~"


 


    

return

"Target !"


 


s

=

test

()


print

s


 


# 輸出結果


test

~


test

~


Target

!




這樣對比這來看, 應該能更好地理解為什麼說early return能夠減少嵌套的層數吧~ 有疑問歡迎留言討論~




談談深坑




剛才花了比較長的篇幅去介紹return, 相信看到這裡, 對於return應該有比較基本的理解了! 所以來聊聊更加迷惑的話題:





當 return 遇上 try..finally, 會怎樣呢?




如果剛才有認真看的話, 會注意到一句話, 就是:





return 代表整個函數返回, 函數調用算結束




但事實真的這樣嗎? 通常這樣問, 答案一般都不是 ~~




先來看看例子:





def

test

()

:


    

try

:


        

a

=

2


        

return

a


    

except

:


        

pass


 


    

finally

:


        

print

"finally"


 


s

=

test

()


print

s




可以猜猜這句print a會不會列印? 相信很多童鞋都想了一會, 然後說不會~ 然而這個答案是錯的, 真正的輸出是:





finally


2




有木有覺得彷彿看見了新大陸, 在一開始的例子中, return後面的語句沒有被執行, 但是在這裡, 相隔那麼遠, 卻依舊沒有忘記, 這或許就是」真愛」吧!




然而就是因為這種」真愛」, 總是會讓一堆新老司機掉坑裡..然後還不知道為毛..




為了避免它們再繼續借用打著」真愛」的幌子, 欺負我們, 讓我們一起來揭開這」真愛」的真面目!




於是, 我們得藉助偷窺神器: dis, 想想都有點小興奮!





import

dis


def

test

()

:


    

try

:


        

a

=

2


        

return

a


    

except

:


        

pass


 


    

finally

:


        

print

"finally"


 


print

dis

.

dis

(

test

)




輸出比較長, 單獨寫:





# 輸出結果


  

6

          

0

SETUP

_

FINALLY

          

28

(

to

31

)


              

3

SETUP

_

EXCEPT

            

14

(

to

20

)


 


  

7

          

6

LOAD

_

CONST

              

1

(

2

)


              

9

STORE

_

FAST

              

0

(

a

)


 


  

8

          

12

LOAD

_

FAST

                

0

(

a

)


            

15

RETURN

_

VALUE

        


            

16

POP

_

BLOCK

          


            

17

JUMP

_

FORWARD

            

7

(

to

27

)


 


  

9

     >>  

20

POP

_

TOP

            


            

21

POP

_

TOP

            


            

22

POP

_

TOP

            


 


10

          

23

JUMP

_

FORWARD

            

1

(

to

27

)


            

26

END_FINALLY

        


        >>  

27

POP

_

BLOCK

          


            

28

LOAD

_

CONST

              

0

(

None

)


 


13

     >>  

31

LOAD

_

CONST

              

2

(

"finally"

)


            

34

PRINT

_

ITEM

          


            

35

PRINT

_

NEWLINE

      


            

36

END

_

FINALLY

        


            

37

LOAD

_

CONST

              

0

(

None

)


            

40

RETURN_VALUE




這邊簡單說著這些列所代表的意思:





第一列是代碼在文件的行號


第二列位元組碼的偏移量


位元組碼的名字


參數


位元組碼處理參數最終的結果




在位元組碼中可以看到, 依次是SETUP_FINALLY 和 SETUP_EXCEPT, 這個對應的就是finally和try,雖然finally在try後面, 雖然我們通常幫他們看成一個整體, 但是他們在實際上卻是分開的… 因為我們重點是finally, 所以就單單看SETUP_FINALLY





//

ceval

.

c


TARGET

(

SETUP_FINALLY

)


        

_setup_finally

:


        

{


            /*

NOTE

:

If

you add any

new

block

-

setup opcodes that


               are

not

try

/

except

/

finally

handlers

,

you may need


               to update the PyGen_NeedsFinalizing

()

function

.


               */


 


            

PyFrame_BlockSetup

(

f

,

opcode

,

INSTR_OFFSET

()

+

oparg

,


                              

STACK_LEVEL

());


            

DISPATCH

();


        

}


 


 


//

fameobject

.

c


void


PyFrame_BlockSetup

(

PyFrameObject

*

f

,

int

type

,

int

handler

,

int

level

)


{


    

PyTryBlock

*

b

;


    

if

(

f

->

f_iblock

>=

CO_MAXBLOCKS

)


        

Py_FatalError

(

"XXX block stack overflow"

);


    

b

= &

f

->

f_blockstack

[

f

->

f_iblock

++

];


    

b

->

b_type

=

type

;


    

b

->

b_level

=

level

;


    

b

->

b_handler

=

handler

;


}




從上面的代碼, 很明顯就能看出來, SETUP_FINALLY 就是調用下PyFrame_BlockSetup去創建一個Block, 然後為這個Block設置:






  1. b_type (opcode 也就是SETUP_FINALLY)



  2. b_level



  3. b_handler (INSTR_OFFSET() + oparg)




handler 可能比較難理解, 其實看剛才的 dis 輸出就能看到是哪個, 就是 13 >> 31 LOAD_CONST 2 (『finally』), 這個箭頭就是告訴我們跳轉的位置的, 為什麼會跳轉到這句呢? 因為6 0 SETUP_FINALLY 28 (to 31)已經告訴我們將要跳轉到31這個位置~~~




如果這個搞清楚了, 那就再來繼續看 return, return對應的位元組碼是: RETURN_VALUE, 所以它對應的源碼是:





//

ceval

.

c


TARGET_NOARG

(

RETURN_VALUE

)


        

{


            

retval

=

POP

();


            

why

=

WHY_RETURN

;


            

goto

fast_block_end

;


        

}




原來我們以前理解的return是假return! 這個return並沒有直接返回嘛, 而是將堆棧的值彈出來, 賦值個retval, 然後將why設置成WHY_RETURN, 接著就跑路了! 跑到一個叫fast_block_end;的地方~, 沒辦法, 為了揭穿真面目, 只好掘地三尺了:





while

(

why

!=

WHY_NOT

&&

f

->

f_iblock

>

0

)

{


            

fast_block_end

:


        

while

(

why

!=

WHY_NOT

&&

f

->

f_iblock

>

0

)

{


            /*

Peek at the current

block

.

*/


            

PyTryBlock

*

b

= &

f

->

f_blockstack

[

f

->

f_iblock

-

1

];


 


            

assert

(

why

!=

WHY_YIELD

);


            

if

(

b

->

b_type

==

SETUP_LOOP

&&

why

==

WHY_CONTINUE

)

{


                

why

=

WHY_NOT

;


                

JUMPTO

(

PyInt_AS_LONG

(

retval

));


                

Py_DECREF

(

retval

);


                

break

;


            

}


 


            /*

Now we have to pop the

block

.

*/


            

f

->

f_iblock

--

;


 


            

while

(

STACK_LEVEL

()

>

b

->

b_level

)

{


                

v

=

POP

();


                

Py_XDECREF

(

v

);


            

}


            

if

(

b

->

b_type

==

SETUP_LOOP

&&

why

==

WHY_BREAK

)

{


                

why

=

WHY_NOT

;


                

JUMPTO

(

b

->

b_handler

);


                

break

;


            

}


            

if

(

b

->

b_type

==

SETUP_FINALLY

||


                

(

b

->

b_type

==

SETUP_EXCEPT

&&


                

why

==

WHY_EXCEPTION

)

||


                

b

->

b_type

==

SETUP_WITH

)

{


                

if

(

why

==

WHY_EXCEPTION

)

{


                    

PyObject

*

exc

,

*

val

,

*

tb

;


                    

PyErr_Fetch

(

&

exc

,

&

val

,

&

tb

);


                    

if

(

val

==

NULL

)

{


                        

val

=

Py_None

;


                        

Py_INCREF

(

val

);


                    

}


                    /*

Make the raw exception

data


                       available to the

handler

,


                      

so

a

program can emulate the


                       Python main

loop

.

  

Don

"t do


                       this for "

finally

"

.

*/


                    

if

(

b

->

b_type

==

SETUP_EXCEPT

||


                        

b

->

b_type

==

SETUP_WITH

)

{


                        

PyErr_NormalizeException

(


                            &

exc

,

&

val

,

&

tb

);


                        

set_exc_info

(

tstate

,


                                    

exc

,

val

,

tb

);


                    

}


                    

if

(

tb

==

NULL

)

{


                        

Py_INCREF

(

Py_None

);


                        

PUSH

(

Py_None

);


                    

}

else


                        

PUSH

(

tb

);


                    

PUSH

(

val

);


                    

PUSH

(

exc

);


                

}


                

else

{


                    

if

(

why

&

(

WHY_RETURN

|

WHY_CONTINUE

))


                        

PUSH

(

retval

);


                    

v

=

PyInt_FromLong

((

long

)

why

);


                    

PUSH

(

v

);


                

}


                

why

=

WHY_NOT

;


                

JUMPTO

(

b

->

b_handler

);


                

break

;


            

}


        

}

/*

unwind

stack

*/




在這需要回顧下剛才的一些知識, 剛才我們看了return的代碼, 看到它將why設置成了 WHY_RETURN, 所以在這麼一大串判斷中, 它只是走了最後面的else, 動作也很簡單, 就是將剛才return儲存的值retval再push壓回棧, 同時將why轉換成long再壓回棧, 然後有設置了下why,接著就是屁顛屁顛去執行剛才SETUP_FINALLY設置的b_handler代碼了~ 當這這段bhandler代碼執行完, 就再通過END_FINALLY去做回該做的事, 而這裡就是, return retval




結論




所以, 我們應該能知道為什麼當我們執行了return代碼, 為什麼finally的代碼還會先執行了吧, 因為return的本質, 就是設置why和retval, 然後goto到一個大判斷, 最後根據why的值去執行對應的操作! 所以可以說並不是真的實質性的返回. 希望我們往後再用到它們的時候, 別再掉坑裡!




看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

頂會審稿人領你打開深度學習的大門
Python 線性分類模型簡介
Python 標準庫筆記:string模塊
那些有趣/用的 Python 庫,15篇 Python 技術熱文
Python 判斷文件是否存在的三種方法

TAG:Python開發者 |