Python 的 Sequence 切片下標問題
(點擊
上方藍字
,快速關注我們)
來源:Lin_R
segmentfault.com/a/1190000009008550
如有好文章投稿,請點擊 → 這裡了解詳情
前言
在python中, 切片是一個經常會使用到的語法, 不管是元組, 列表還是字元串, 一般語法就是:
sequence[ilow:ihigh:step] # ihigh,step 可為空; 為了簡短易懂, 暫時排除step的用法考慮
先來簡單示範下用法
sequence
=
[
1
,
2
,
3
,
4
,
5
]
sequence
[
ilow
:
ihigh
]
# 從ilow開始到ihigh-1結束
sequence
[
ilow
:
]
# 從ilow開始直到末尾
sequence
[
:
ihigh
]
# 從頭部開始直到ihigh結束
sequence
[
:
]
# 複製整個列表
語法很簡潔, 也很容易理解, 這種語法在我們日常使用中 是簡單又好用, 但我相信在我們使用這種切片語法時, 都會習慣性謹遵一些規則:
ilow, ihigh均小於 sequece的長度
ilow < ihigh
因為在大部分情況下, 只有遵循上面的規則, 才能得到我們預期的結果! 可是如果我不遵循呢? 切片會怎樣?
不管我們在使用元組, 列表還是字元串, 當我們想取中一個元素時, 我們會用到如下語法:
sequence
=
[
1
,
2
,
3
,
4
,
5
]
sequence
[
1
]
# 輸出2
sequence
[
2
]
# 輸出3
上面出現的 1,2 我們姑且稱之為下標, 不管是元組, 列表還是字元串, 我們都能通過下標來取出對應的值, 但是如果下標超過對象的長度, 那麼將觸發索引異常(IndexError)
sequence
=
[
1
,
2
,
3
,
4
,
5
]
sequence
[
15
]
### 輸出 ###
Traceback
(
most recent call
last
)
:
File
"test.py"
,
line
2
,
in
<
module
>
a
[
20
]
IndexError
:
list index out of
range
那麼對於切片呢? 兩種語法很相似, 假設我 ilow 和 ihigh分別是10和20, 那麼結果是怎樣呢
情景重現
# version: python2.7
a
=
[
1
,
2
,
3
,
5
]
a
[
10
:
20
]
# 結果會報異常嗎?
看到10和20, 完全超出了序列a的長度, 由於前面的代碼, 或者以前的經驗, 我們總會覺得這樣肯定也會導致一個IndexError,那我們開終端來試驗下:
>>>
a
=
[
1
,
2
,
3
,
5
]
>>>
a
[
10
:
20
]
[]
結果居然是: [], 這感覺有點意思.是只有列表才會這麼, 字元串呢, 元組呢
>>>
s
=
"23123123123"
>>>
s
[
400
:
2000
]
""
>>>
t
=
(
1
,
2
,
3
,
4
)
>>>
t
[
200
:
1000
]
()
結果都和列表的類似, 返回屬於各自的空結果.
看到結果的我們眼淚掉下來, 不是返回一個IndexError, 而是直接返回空, 這讓我們不禁想到, 其實語法相似, 背後的東西肯定還是不同的, 那我們下面一起來嘗試去解釋下這結果吧
原理分析
在揭開之前, 咱們要先搞清楚, python是怎樣處理這個切片的, 可以通過dis模塊來協助:
############# 切片 ################
[
root
@
iZ23pynfq19Z
~
]
# cat test.py
a
=
[
11
,
2
,
3
,
4
]
a
[
20
:
30
]
#結果:
[
root
@
iZ23pynfq19Z
~
]
# python -m dis test.py
1
0
LOAD
_
CONST
0
(
11
)
3
LOAD
_
CONST
1
(
2
)
6
LOAD
_
CONST
2
(
3
)
9
LOAD
_
CONST
3
(
4
)
12
BUILD
_
LIST
4
15
STORE
_
NAME
0
(
a
)
2
18
LOAD
_
NAME
0
(
a
)
21
LOAD
_
CONST
4
(
20
)
24
LOAD
_
CONST
5
(
30
)
27
SLICE
+
3
28
_
ITEM
29
_
NEWLINE
30
LOAD
_
CONST
6
(
None
)
33
RETURN_VALUE
############# 單下標取值 ################
[
root
@
gitlab
~
]
# cat test2.py
a
=
[
11
,
2
,
3
,
4
]
a
[
20
]
#結果:
[
root
@
gitlab
~
]
# python -m dis test2.py
1
0
LOAD
_
CONST
0
(
11
)
3
LOAD
_
CONST
1
(
2
)
6
LOAD
_
CONST
2
(
3
)
9
LOAD
_
CONST
3
(
4
)
12
BUILD
_
LIST
4
15
STORE
_
NAME
0
(
a
)
2
18
LOAD
_
NAME
0
(
a
)
21
LOAD
_
CONST
4
(
20
)
24
BINARY
_
SUBSCR
25
_
ITEM
26
_
NEWLINE
27
LOAD
_
CONST
5
(
None
)
30
RETURN
_
VALUE
在這簡單介紹下dis模塊, 有經驗的老司機都知道, python在解釋腳本時, 也是存在一個編譯的過程, 編譯的結果就是我們經常看到的pyc文件, 這裡面codeobject對象組成的位元組碼, 而dis就是將這些位元組碼用比較可觀的方式展示出來, 讓我們看到執行的過程, 下面是dis的輸出列解釋:
第一列是數字是原始源代碼的行號。
第二列是位元組碼的偏移量:LOAD_CONST在第0行.以此類推。
第三列是位元組碼人類可讀的名字。它們是為程序員所準備的
第四列表示指令的參數
第五列是計算後的實際參數
前面就不贅述了, 就是讀常量存變數的過程, 最主要的區別就是: test.py 切片是使用了位元組碼 SLICE+3實現的, 而test2.py 單下標取值主要通過位元組碼BINARY_SUBSCR實現的,如同我們猜測的一樣, 相似的語法卻是截然不同的代碼.因為我們要展開討論的是切片(SLICE+3), 所以就不再展開BINARY_SUBSCR, 感興趣的童鞋可以查看相關源碼了解具體實現, 位置: python/object/ceval.c
那我們下面來展開討論下 SLICE+3
/*取自: python2.7 python/ceval.c */
// 第一步:
PyEval_EvalFrameEx
(
PyFrameObject *
f
,
int
throwflag
)
{
....
// 省略n行代碼
TARGET_WITH_IMPL_NOARG
(
SLICE
,
_slice
)
TARGET_WITH_IMPL_NOARG
(
SLICE_1
,
_slice
)
TARGET_WITH_IMPL_NOARG
(
SLICE_2
,
_slice
)
TARGET_WITH_IMPL_NOARG
(
SLICE_3
,
_slice
)
_slice
:
{
if
((
opcode
-
SLICE
)
&
2
)
w
=
POP
();
else
w
=
NULL
;
if
((
opcode
-
SLICE
)
&
1
)
v
=
POP
();
else
v
=
NULL
;
u
=
TOP
();
x
=
apply_slice
(
u
,
v
,
w
);
// 取出v: ilow, w: ihigh, 然後調用apply_slice
Py_DECREF
(
u
);
Py_XDECREF
(
v
);
Py_XDECREF
(
w
);
SET_TOP
(
x
);
if
(
x
!=
NULL
)
DISPATCH
();
break
;
}
....
// 省略n行代碼
}
// 第二步:
apply_slice
(
PyObject *
u
,
PyObject *
v
,
PyObject *
w
)
/* return u[v:w] */
{
PyTypeObject *
tp
=
u
->
ob_type
;
PySequenceMethods *
sq
=
tp
->
tp_as_sequence
;
if
(
sq
&&
sq
->
sq_slice
&&
ISINDEX
(
v
)
&&
ISINDEX
(
w
))
{
// v,w的類型檢查,要整型/長整型對象
Py_ssize_t
ilow
=
0
,
ihigh
=
PY_SSIZE_T_MAX
;
if
(
!
_PyEval_SliceIndex
(
v
,
&
ilow
))
// 將v對象再做檢查, 並將其值轉換出來,存給ilow
return
NULL
;
if
(
!
_PyEval_SliceIndex
(
w
,
&
ihigh
))
// 同上
return
NULL
;
return
PySequence_GetSlice
(
u
,
ilow
,
ihigh
);
// 獲取u對象對應的切片函數
}
else
{
PyObject *
slice
=
PySlice_New
(
v
,
w
,
NULL
);
if
(
slice
!=
NULL
)
{
PyObject *
res
=
PyObject_GetItem
(
u
,
slice
);
Py_DECREF
(
slice
);
return
res
;
}
else
return
NULL
;
}
// 第三步:
PySequence_GetSlice
(
PyObject *
s
,
Py_ssize_t
i1
,
Py_ssize_t
i2
)
{
PySequenceMethods *
m
;
PyMappingMethods *
mp
;
if
(
!
s
)
return
null_error
();
m
=
s
->
ob_type
->
tp_as_sequence
;
if
(
m
&&
m
->
sq_slice
)
{
if
(
i1
sq_length
)
{
// 先做個簡單的初始化, 如果左右下表小於, 將其加上sequence長度使其歸為0
Py_ssize
_
t
l
=
(
*
m
->
sq_length
)(
s
);
if
(
l
sq_slice
(
s
,
i1
,
i2
);
}
else
if
((
mp
=
s
->
ob_type
->
tp_as_mapping
)
&&
mp
->
mp_subscript
)
{
PyObject *
res
;
PyObject *
slice
=
_PySlice_FromIndices
(
i1
,
i2
);
if
(
!
slice
)
return
NULL
;
res
=
mp
->
mp_subscript
(
s
,
slice
);
Py_DECREF
(
slice
);
return
res
;
}
return
type_error
(
""%.200s" object is unsliceable"
,
s
);
雖然上面的代碼有點長, 不過關鍵地方都已經注釋出來, 而我們也只需要關注那些地方就足夠了. 如上, 我們知道最終是要執行 m->sq_slice(s, i1, i2), 但是這個sq_slice有點特別, 因為不同的對象, 它所對應的函數不同, 下面是各自的對應函數:
// 字元串對象
StringObject
.
c
:
(
ssizessizeargfunc
)
string_slice
,
/*sq_slice*/
// 列表對象
ListObject
.
c
:
(
ssizessizeargfunc
)
list_slice
,
/* sq_slice */
// 元組
TupleObject
.
c
:
(
ssizessizeargfunc
)
tupleslice
,
/* sq_slice */
因為他們三個的函數實現大致相同, 所以我們只分析其中一個就可以了, 下面是對列表的切片函數分析:
/*
取自ListObject
.
c
*/
static
PyObject
*
list_slice
(
PyListObject
*
a
,
Py_ssize_t
ilow
,
Py_ssize_t
ihigh
)
{
PyListObject
*
np
;
PyObject
**
src
,
**
dest
;
Py_ssize
_
t
i
,
len
;
if
(
ilow
<
0
)
ilow
=
0
;
else
if
(
ilow
>
Py_SIZE
(
a
))
//
如果ilow
大於a
長度,
那麼重新賦值為
a
的長度
ilow
=
Py_SIZE
(
a
);
if
(
ihigh
<
ilow
)
ihigh
=
ilow
;
else
if
(
ihigh
>
Py_SIZE
(
a
))
//
如果ihigh
大於a
長度,
那麼重新賦值為
a
的長度
ihigh
=
Py_SIZE
(
a
);
len
=
ihigh
-
ilow
;
np
=
(
PyListObject
*
)
PyList_New
(
len
);
//
創建一個ihigh
-
ilow
的新列表對象
if
(
np
==
NULL
)
return
NULL
;
src
=
a
->
ob_item
+
ilow
;
dest
=
np
->
ob_item
;
for
(
i
=
0
;
i
<
len
;
i
++
)
{
//
將a
處於該範圍內的成員,
添加到新列表對象
PyObject
*
v
=
src
[
i
];
Py_INCREF
(
v
);
dest
[
i
]
=
v
;
}
return
(
PyObject
*
)
np
;
}
結論
從上面的sq_slice函數對應的切片函數可以看到, 如果在使用切片時, 左右下標都大於sequence的長度時, 都將會被重新賦值成sequence的長度, 所以咱們一開始的切片: print a[10:20], 實際上運行的是: print a4:4. 通過這次的分析, 以後在遇到下標大於對象長度的切片, 應該不會再懵逼了~
看完本文有收穫?請轉
發分享給更多人
關注「Python開發者」,提升Python技能


※用 Python 寫一個簡單的Web框架
※如何用Python對數據進行差分
※英特爾Python發行版助力數據科學
※Python 的正則表達式彩蛋
※tomd: 用Python將HTML轉換回Markdown
TAG:Python |
※FAQ詳解「Meltdown和Spectre」問題,接踵而來的「Skyfall和Solace」是否僅是騙局?
※Gradle Kotlin DSL的accessors 生成問題
※常規外企面試問題1 Tell Me About Yourself
※iOS 惹禍:iPhone更新後AirPods問題不斷
※iOS 惹禍 iPhone更新後AirPods問題不斷
※Belle&Sebastian:如何解決人類的問題?
※Andrew Neeme現身Reddit AMA回答粉絲問題
※英特爾:Spectre和Meltdown漏洞的修補程序存在問題
※Galaxy S9/S9 Plus又被泄露了 硬剛iPhone X應該沒問題
※英特爾:修復Spectre和Meltdown漏洞的補丁存在問題
※解決畫面撕裂問題:Microsoft 微軟 Xbox One S/X 即將支持 AMD FreeSync「防撕裂」技術
※你遇到的問題,它是problem,還是question?
※Samsung承認Android Oreo更新後可能存在隨機重新啟動問題
※性能測試loadrunner場景問題之HTTP
※Oculus修復Rift當機問題,微軟改進SteamVR遊戲性能
※8大關鍵問題 讓你全面了解Social Lending 蜂巢星球
※國人iPhone4s太卡上述蘋果,iPhoneX再爆新通話問題
※Python 之父Guido van Rossum在2013年對幾個問題的回應
※FB將召開緊急會議 讓員工提出有關Cambridge Analytica的問題
※霍金先生思考過的最大的問題:More is different?