Python :淺析 return 和 finally 共同挖的坑
(點擊
上方藍字
,快速關注我們)
來源:Lin_R
segmentfault.com/a/1190000010701665
如有好文章投稿,請點擊 → 這裡了解詳情
初識 return
相信每一個用過Python函數的童鞋, 肯定會用過return語句, return顧名思義, 就是用來返回值給調用者, 例如:
def
test
()
:
a
=
2
return
a
s
=
test
()
s
# 輸出結果
2
對於上面的結果, 相信大家都不會感到意外, 那麼加大點難度, 如果在return語句還有代碼呢? 那句代碼會怎樣呢?
def
test
()
:
a
=
2
return
a
s
=
3
s
s
=
test
()
s
# 結果是什麼?
老司機肯定一眼就能看出結果, 但是對於尚在入門或者對return不很了解的童鞋, 可能就會懵逼了~ 後面的兩句代碼是否會被執行?
答案是:
不會執行
return正如它的名字那樣, 當執行這句代碼, 整個函數都會返回, 整個調用就算結束了~ 所以在return後面的代碼, 都是不會被執行的!
也正因為這個特性, 所以有種編碼規範叫early return的編碼規範就被倡導。
它的意思大概就是: 當條件已經滿足返回時, 就馬上返回
舉個例子來說明:
def
test
()
:
a
=
2
if
a
>
2
:
result
=
"more than"
else
:
result
=
"less than"
return
result
s
=
test
()
s
上面的代碼應該比較容易理解, 就是根據a的值, 來決定返回的result是什麼. 這樣的編碼相信也是大部分童鞋喜歡用的, 因為這樣比較符合我們直覺, 然而, 這樣寫似乎有點浪費, 因為當第一個判斷結束了, 如果結果為真, 就應該返回more than, 然後結束函數, 否則肯定就是返回less than, 所以我們可以把代碼調整成這樣:
def
test
()
:
a
=
2
if
a
>
2
:
return
"more than"
else
:
return
"less than"
s
=
test
()
s
甚至是:
def
test
()
:
a
=
2
if
a
>
2
:
return
"more than"
return
"less than"
s
=
test
()
s
結果都是和第一個寫法是一樣的! 第一次看到這樣寫法的童鞋, 可能會覺得比較難以接受, 甚至覺得可讀性很差, 但是其實這樣的寫法, 我覺得反而會稍微好點. 因為:
運行的代碼數少了, 調用方能更快得到結果
有利於減少嵌套的層數, 便於理解.
對於第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
)
:
"test ~"
result
=
"Target !"
return
result
s
=
test
()
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
)
:
"test ~"
return
"Target !"
s
=
test
()
s
# 輸出結果
test
~
test
~
Target
!
這樣對比這來看, 應該能更好地理解為什麼說early return能夠減少嵌套的層數吧~ 有疑問歡迎留言討論~
談談深坑
剛才花了比較長的篇幅去介紹return, 相信看到這裡, 對於return應該有比較基本的理解了! 所以來聊聊更加迷惑的話題:
當 return 遇上 try..finally, 會怎樣呢?
如果剛才有認真看的話, 會注意到一句話, 就是:
return 代表整個函數返回, 函數調用算結束
但事實真的這樣嗎? 通常這樣問, 答案一般都不是 ~~
先來看看例子:
def
test
()
:
try
:
a
=
2
return
a
except
:
pass
finally
:
"finally"
s
=
test
()
s
可以猜猜這句print a會不會列印? 相信很多童鞋都想了一會, 然後說不會~ 然而這個答案是錯的, 真正的輸出是:
finally
2
有木有覺得彷彿看見了新大陸, 在一開始的例子中, return後面的語句沒有被執行, 但是在這裡, 相隔那麼遠, 卻依舊沒有忘記, 這或許就是」真愛」吧!
然而就是因為這種」真愛」, 總是會讓一堆新老司機掉坑裡..然後還不知道為毛..
為了避免它們再繼續借用打著」真愛」的幌子, 欺負我們, 讓我們一起來揭開這」真愛」的真面目!
於是, 我們得藉助偷窺神器: dis, 想想都有點小興奮!
import
dis
def
test
()
:
try
:
a
=
2
return
a
except
:
pass
finally
:
"finally"
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
_
ITEM
35
_
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設置:
b_type (opcode 也就是SETUP_FINALLY)
b_level
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 標準庫筆記:string模塊
※那些有趣/用的 Python 庫,15篇 Python 技術熱文
※Python 判斷文件是否存在的三種方法
TAG:Python開發者 |