元組
Python中的元組容器序列(tuple)與列表容器序列(list)具有極大的相似之處,因此也常被稱為不可變的列表。
但是兩者之間也有很多的差距,元組側重於資料的展示,而列表側重於資料的儲存與操作。
它們非常相似,雖然都可以儲存任意型別的資料,但是一個元組定義好之後就不能夠再進行修改。
元組特性
元組的特點:
- 元組屬於容器序列
- 元組屬於不可變型別
- 元組底層由順序儲存組成,而順序儲存是線性結構的一種
基本宣告
以下是使用類例項化的形式進行物件宣告:
tup = tuple((1, 2, 3, 4, 5))
print("值:%r,型別:%r" % (tup, type(tup)))
# 值:(1, 2, 3, 4, 5),型別:<class 'tuple'>
也可以選擇使用更方便的字面量形式進行物件宣告,使用逗號對資料項之間進行分割:
tup = 1, 2, 3, 4, 5
print("值:%r,型別:%r" % (tup, type(tup)))
# 值:(1, 2, 3, 4, 5),型別:<class 'tuple'>
為了美觀,我們一般會在兩側加上(),但是要確定一點,元組定義是逗號分隔的資料項,而並非是()包裹的資料項:
tup = (1, 2, 3, 4, 5)
print("值:%r,型別:%r" % (tup, type(tup)))
# 值:(1, 2, 3, 4, 5),型別:<class 'tuple'>
多維元組
當一個元組中巢狀另一個元組,該元組就可以稱為多維元組。
如下,定義一個2維元組:
tup = (1, 2, ("三", "四"))
print("值:%r,型別:%r" % (tup, type(tup)))
# 值:(1, 2, ('三', '四')),型別:<class 'tuple'>
續行操作
在Python中,元組中的資料項如果過多,可能會導致整個元組太長,太長的元組是不符合PEP8規範的。
- 每行最大的字元數不可超過79,文件字元或者註釋每行不可超過72
Python雖然提供了續行符\,但是在元組中可以忽略續行符,如下所示:
tup = (
1,
2,
3,
4,
5
)
print("值:%r,型別:%r" % (tup, type(tup)))
# 值:(1, 2, 3, 4, 5),型別:<class 'tuple'>
型別轉換
元組支援與布林型、字串、列表、以及集合型別進行型別轉換:
tup = (1, 2, 3)
bTup = bool(tup) # 布林型別
strTup = str(tup) # 字串型別
liTup = list(tup) # 列表型別
setTup = set(tup) # 集合型別
print("值:%r,型別:%r" % (bTup, type(bTup)))
print("值:%r,型別:%r" % (strTup, type(strTup)))
print("值:%r,型別:%r" % (liTup, type(liTup)))
print("值:%r,型別:%r" % (setTup, type(setTup)))
# 值:True,型別:<class 'bool'>
# 值:'(1, 2, 3)',型別:<class 'str'>
# 值:[1, 2, 3],型別:<class 'list'>
# 值:{1, 2, 3},型別:<class 'set'>
如果一個2維元組遵循一定的規律,那麼也可以將其轉換為字典型別:
tup = (("k1", "v1"), ("k2", "v2"), ("k3", "v3"))
dictTuple = dict(tup)
print("值:%r,型別:%r" % (dictTuple, type(dictTuple)))
# 值:{'k1': 'v1', 'k2': 'v2', 'k3': 'v3'},型別:<class 'dict'>
索引操作
元組的索引操作僅支援獲取資料項。
其他的任意索引操作均不被支援。
使用方法參照列表的索引切片一節。
絕對引用
元組擁有絕對引用的特性,無論是深拷貝還是淺拷貝,都不會獲得其副本,而是直接對源物件進行引用。
但是列表沒有絕對引用的特性,程式碼驗證如下:
>>> import copy
>>> # 列表的深淺拷貝均建立新列表...
>>> oldLi = [1, 2, 3]
>>> id(oldLi)
4542649096
>>> li1 = copy.copy(oldLi)
>>> id(li1)
4542648840
>>> li2 = copy.deepcopy(oldLi)
>>> id(li2)
4542651208
>>> # 元組的深淺拷貝始終引用老元組
>>> oldTup = (1, 2, 3)
>>> id(oldTup)
4542652920
>>> tup1 = copy.copy(oldTup)
>>> id(tup1)
4542652920
>>> tup2 = copy.deepcopy(oldTup)
>>> id(tup2)
4542652920
Python為何要這樣設計?其實仔細想想不難發現,元組不能對其進行操作,僅能獲取資料項。
那麼也就沒有生成多個副本提供給開發人員操作的必要了,因為你修改不了元組,索性直接使用絕對引用策略。
值得注意的一點:[:]也是淺拷貝,故對元組來說屬於絕對引用範疇。
元組的陷阱
Leonardo Rochael在2013年的Python巴西會議提出了一個非常具有思考意義的問題。
我們先來看一下:
>>> t = (1, 2, [30, 40])
>>> t[-1] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
現在,t到底會發生下面4種情況中的哪一種?
- t 變成 (1, 2, [30, 40, 50, 60])。
- 因為 tuple 不支援對它的資料項賦值,所以會丟擲 TypeError 異常。
- 以上兩個都不是。
- a 和 b 都是對的。
正確答案是4,t確實會變成 (1, 2, [30, 40, 50, 60]),但同時元組是不可變型別故會引發TypeError異常的出現。
>>> t
(1, 2, [30, 40, 50, 60])
如果是使用extend()對t[-1]的列表進行資料項的增加,則答案會變成1。
我當初在看了這個問題後,暗自告訴自己了2件事情:
-
list的資料項增加儘量不要使用+=,而應該使用append()或者extend()
Ps:我也不知道自己為什麼會產生這樣的想法,但這個想法確實伴隨我很長時間,直至現在
-
tuple中不要存放可變型別的資料,如list、set、dict等..
元組更多的作用是展示資料,而不是運算元據。
舉個例子,當使用者根據某個操作獲取到了眾多資料項之後,你可以將這些資料項做出元組並返回。
使用者對被返回的原物件只能看,不能修改,若想修改則必須建立新其他型別物件。
解構方法
元組的解構方法與列表使用相同。
使用方法參照列表的解構方法一節。
常用方法
方法一覽
常用的list方法一覽表:
方法名 | 返回值 | 描述 |
---|---|---|
count() | integer | 返回資料項在T中出現的次數 |
index() | integer | 返回第一個資料項在T中出現位置的索引,若值不存在,則丟擲ValueError |
基礎公用函式:
函式名 | 返回值 | 描述 |
---|---|---|
len() | integer | 返回容器中的專案數 |
enumerate() | iterator for index, value of iterable | 返回一個可迭代物件,其中以小元組的形式包裹資料項與正向索引的對應關係 |
reversed() | ... | 詳情參見函式章節 |
sorted() | ... | 詳情參見函式章節 |
獲取長度
使用len()方法來獲取元組的長度。
返回int型別的值。
tup = ("A", "B", "C", "D", "E", "F", "G")
print(len(tup))
# 7
Python在對內建的資料型別使用len()方法時,實際上是會直接的從PyVarObject結構體中獲取ob_size屬性,這是一種非常高效的策略。
PyVarObject是表示記憶體中長度可變的內建物件的C語言結構體。
直接讀取這個值比呼叫一個方法要快很多。
統計次數
使用count()方法統計資料項在該元組中出現的次數。
返回int:
tup = ("A", "B", "C", "D", "E", "F", "G", "A")
aInTupCount = tup.count("A")
print(aInTupCount)
# 2
查詢位置
使用index()方法找到資料項在當前元組中首次出現的位置索引值,如資料項不存在則丟擲異常。
返回int。
tup = ("A", "B", "C", "D", "E", "F", "G", "A")
aInTupIndex = tup.index("A")
print(aInTupIndex)
# 0
底層探究
記憶體開闢
Python內部實現中,列表和元組還是有一定的差別的。
元組在建立物件申請記憶體的時候,記憶體空間大小便進行了固定,後續不可更改(如果是傳入了一個可迭代物件,例如tupe(range(100)),這種情況會進行擴容與縮容,下面的章節將進行探討研究)。
而列表在建立物件申請記憶體的時候,記憶體空間大小不是固定的,如果後續對其新增或刪除資料項,列表會進行擴容或者縮容機制。
元組建立
空元組
若建立一個空元組,會直接進行建立,然後將這個空元組丟到快取free_list中。
元組的free_list最多能快取 20 * 2000 個元組,這個在下面會進行講解。
如圖所示:
元組轉元組
這樣的程式碼會進行元組轉元組:
tup = tuple((1, 2, 3))
首先內部本身就是一個元組(1, 2, 3),所以會直接將內部的這個元組拿出來並返回引用,並不會再次建立。
程式碼驗證:
>>> oldTup = (1, 2, 3)
>>> id(oldTup)
4384908128
>>> newTup = tuple(oldTup)
>>> id(newTup)
4384908128
>>>
列表轉元組
列表轉元組會將列表中的每一個資料項都拿出來,然後放入至元組中:
tup = tuple([1, 2, 3])
所以你會發現,列表和元組中的資料項引用都是相同的:
>>> li1 = ["A", "B", "C"]
>>> tup = tuple(li1)
>>> print(id(li1[0]))
4383760656
>>> print(id(tup[0]))
4383760656
>>>
可迭代物件轉元組
可迭代物件是沒有長度這一概念的,如果是可迭代物件轉換為元組,會先對可迭代物件的長度做一個猜想。
並且根據這個猜想,為元組開闢一片記憶體空間,用於存放可迭代物件的資料項。
然後內部會獲取可迭代物件的迭代器,對其進行遍歷操作,拿出資料項後放至元組中。
如果猜想的長度太小,會導致元組內部的記憶體不夠存放下所有的迭代器資料項,此時該元組會進行內部的擴容機制,直至可迭代物件中的資料項全部被新增至元組中。
rangeObject = range(1, 101)
tup = tuple(rangeObject)
// 假如猜想的是9
// 第一步:+ 10
// 第二步:+ (原長度+10) * 0.25
// 其實,就是增加【原長度*0.25 + 2.5】
如果猜想的長度太大,而實際上迭代器中的資料量偏少,則需要對該元組進行縮容。
切片取值
對元組進行切片取值的時候,會開闢一個新元組用於存放切片後得到的資料項。
tup = (1, 2, 3)
newSliceTup = tup[0:2]
當然,如果是[:]的操作,則參照絕對引用,直接返回被切片的元組引用。
程式碼驗證:
>>> id(tup)
4384908416
>>> newSliceTup = tup[0:2]
>>> id(newSliceTup)
4384904392
快取機制
free_list快取
元組的快取機制和列表的快取機制不同。
元組的free_list會快取0 - 19長度的共20種元組,其中每一種長度的元組通過單向連結串列橫向擴充套件快取至2000個,如下圖所示:
當每一次的del操作有資料項的元組時,都會將該元組資料項清空並掛載至free_list單向連結串列的頭部的位置。
del 元組1
del 元組2
del 元組3
如下圖所示:
當要建立一個元組時,會通過建立元組的長度,從free_list單向連結串列的頭部取出一個元組,然後將資料項存放進去。
前提是free_list單向連結串列中快取的有該長度的元組。
tup = (1, 2, 3)
空元組與非空元組的快取
空元組的快取是一經建立就快取到free_list單向連結串列中。
而非空元組的快取必須是del操作後才快取到free_list單向連結串列中。
空元組的建立
第一次建立空元組後,空元組會快取至free_list單向連結串列中。
以後的每一次空元組建立,返回的其實都是同一個引用,也就是說空元組在free_list單向連結串列中即使被引用了也不會被銷燬。
>>> t1 = ()
>>> id(t1)
4511088712
>>> t2 = ()
>>> id(t2)
4511088712
非空元組的建立
當free_list單向連結串列中有相同長度的元組時,會進行引用並刪除。
這個在上圖中已經示例過了,就是這個:
程式碼示例:
$ python3
Python 3.6.8 (v3.6.8:3c6b436a57, Dec 24 2018, 02:04:31)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> v1 = (None, None, None)
>>> id(v1)
4384907696
>>> v2 = (None, None, None)
>>> id(v2)
4384908056
>>> del v1
>>> del v2 # ①
>>> v3 = (None, None, None)
>>> id(v3) # ②
4384908056
>>> v4 = (None, None, None)
>>> id(v4) # ③
4384907696
>>>
①:free_list num_free=3 單向連結串列結構:v2 —> v1
②:建立了v3,拿出v2的空元組,填入v3資料項,故v2和v3的id值相等,證明引用同一個元組,此時free_list num_free=3 單向連結串列結構為:—> v1
③:建立了v4,拿出v1的空元組,填入v4資料項,故v1和v4的id值相等,證明引用同一個元組
tupleobject.c原始碼
官網參考:點我跳轉
原始碼一覽:點我跳轉
以下是擷取了一些關鍵性原始碼,並且做上了中文註釋,方便查閱。
每一個元組都有幾個關鍵性的屬性:
Py_ssize_t ob_refcnt; // 引用計數器
Py_ssize_t ob_size; // 資料項個數,即元組大小
PyObject *ob_item[1]; // 儲存元組中的資料項 [指標, ]
關於快取free_list的屬性:
PyTuple_MAXSAVESIZE // 相當於圖中的 free_num ,最大20,即縱向擴充套件的快取元組長度
PyTuple_MAXFREELIST // 圖中 free_list 的橫向擴充套件快取列表個數,最大2000
建立元組
空元組
PyObject *
PyTuple_New(Py_ssize_t size)
{
PyTupleObject *op;
// 快取相關
Py_ssize_t i;
// 元組的大小不能小於0
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
#if PyTuple_MAXSAVESIZE > 0
// 建立空元組,優先從快取中獲取
// size = 0 表示這是一個空元組,從free_list[0]中獲取空元組
if (size == 0 && free_list[0]) {
// op就是空元組
op = free_list[0];
// 新增空元組引用計數器 + 1
Py_INCREF(op);
#ifdef COUNT_ALLOCS
tuple_zero_allocs++;
#endif
// 返回空元組的指標
return (PyObject *) op;
}
// 如果建立的不是空元組,且這個建立的元組資料項個數小於20,並且free_list[size]不等於空,表示有快取
// 則從快取中去獲取,不再重新開闢記憶體
if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
// 拿出元組
free_list[size] = (PyTupleObject *) op->ob_item[0];
// num_free減1
numfree[size]--;
#ifdef COUNT_ALLOCS
fast_tuple_allocs++;
#endif
/* Inline PyObject_InitVar */
// 初始化,定義這個元組的長度為資料項個數
#ifdef Py_TRACE_REFS
Py_SIZE(op) = size;
// 定義型別為 tuple
Py_TYPE(op) = &PyTuple_Type;
#endif
// 增加一次新的引用
_Py_NewReference((PyObject *)op);
}
// 如果是空元組
else
#endif
{
// 檢查記憶體情況,是否充足
/* Check for overflow */
if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - sizeof(PyTupleObject) -
sizeof(PyObject *)) / sizeof(PyObject *)) {
return PyErr_NoMemory();
}
// 開闢記憶體,並獲得一個元組:op
op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
if (op == NULL)
return NULL;
}
// 空元組的每一個槽位都是NULL
for (i=0; i < size; i++)
op->ob_item[i] = NULL;
#if PyTuple_MAXSAVESIZE > 0
// 快取空元組
if (size == 0) {
free_list[0] = op;
++numfree[0];
Py_INCREF(op); /* extra INCREF so that this is never freed */
}
#endif
#ifdef SHOW_TRACK_COUNT
count_tracked++;
#endif
// 將元組加入到GC機制中,用於記憶體管理
_PyObject_GC_TRACK(op);
return (PyObject *) op;
}
可迭代物件轉元組
這個不在tupleobject.c原始碼中,而是在abstract.c原始碼中。
官網參考:點我跳轉
原始碼一覽:點我跳轉
PyObject *
PySequence_Tuple(PyObject *v)
{
PyObject *it; /* iter(v) */
Py_ssize_t n; /* guess for result tuple size */
PyObject *result = NULL;
Py_ssize_t j;
if (v == NULL) {
return null_error();
}
/* Special-case the common tuple and list cases, for efficiency. */
// 如果是元組轉換元組,如 tup = (1, 2, 3) 或者 tup = ((1, 2, 3))直接返回記憶體地址
if (PyTuple_CheckExact(v)) {
Py_INCREF(v);
return v;
}
// 如果是列表轉換元組,則執行PyList_AsTuple(),將列表轉換為元組
// 如 tup = ([1, 2, 3])
if (PyList_CheckExact(v))
return PyList_AsTuple(v);
/* Get iterator. */
// 獲取迭代器, tup = (range(1, 4).__iter__())
it = PyObject_GetIter(v);
if (it == NULL)
return NULL;
/* Guess result size and allocate space. */
// 猜想迭代器長度,也就是猜一下有多少個資料項
n = PyObject_LengthHint(v, 10);
if (n == -1)
goto Fail;
// 根據猜想的迭代器長度,進行元組的記憶體開闢
result = PyTuple_New(n);
if (result == NULL)
goto Fail;
/* Fill the tuple. */
// 將迭代器中每個資料項新增至元組中
for (j = 0; ; ++j) {
PyObject *item = PyIter_Next(it);
if (item == NULL) {
if (PyErr_Occurred())
goto Fail;
break;
}
//如果迭代器中資料項比猜想的多,則證明開闢記憶體不足需要需要進行擴容
if (j >= n) {
size_t newn = (size_t)n;
/* The over-allocation strategy can grow a bit faster
than for lists because unlike lists the
over-allocation isn't permanent -- we reclaim
the excess before the end of this routine.
So, grow by ten and then add 25%.
*/
// 假如猜想的是9
// 第一步:+ 10
// 第二步:+ (原長度+10) * 0.25
// 其實,就是增加【原長度*0.25 + 2.5】
newn += 10u;
newn += newn >> 2;
// 判斷是否超過了元組的資料項個數限制(sys.maxsize)
if (newn > PY_SSIZE_T_MAX) {
/* Check for overflow */
PyErr_NoMemory();
Py_DECREF(item);
goto Fail;
}
n = (Py_ssize_t)newn;
// 擴容機制
if (_PyTuple_Resize(&result, n) != 0) {
Py_DECREF(item);
goto Fail;
}
}
// 將資料項放入元組之中
PyTuple_SET_ITEM(result, j, item);
}
/* Cut tuple back if guess was too large. */
// 如果猜想的資料項太多,而實際上迭代器中的資料量偏少
// 則需要對該元組進行縮容
if (j < n &&
_PyTuple_Resize(&result, j) != 0)
goto Fail;
Py_DECREF(it);
return result;
Fail:
Py_XDECREF(result);
Py_DECREF(it);
return NULL;
}
列表轉元組
這個不在tupleobject.c原始碼中,而是在listobject.c原始碼中。
官網參考:點我跳轉
原始碼一覽:點我跳轉
PyObject *
PyList_AsTuple(PyObject *v)
{
PyObject *w;
PyObject **p, **q;
Py_ssize_t n;
// 例如:tup = ([1, 2, 3])
// 進行列表的驗證
if (v == NULL || !PyList_Check(v)) {
PyErr_BadInternalCall();
return NULL;
}
// 獲取大小,即資料項個數
n = Py_SIZE(v);
// 開闢記憶體
w = PyTuple_New(n);
// 如果是空元組
if (w == NULL)
return NULL;
// 執行遷徙操作
p = ((PyTupleObject *)w)->ob_item;
q = ((PyListObject *)v)->ob_item;
// 將列表中資料項的引用,也給元組進行引用
// 這樣列表中資料項和元組中的資料項都引用同1個物件
while (--n >= 0) {
// 資料項引用計數 + 1
Py_INCREF(*q);
*p = *q;
p++;
q++;
}
// 返回元組
return w;
}
切片取值
PyObject *
PyTuple_GetSlice(PyObject *op, Py_ssize_t i, Py_ssize_t j)
// 切片會觸發該方法
{
// 如果對空元組進行切片,則會丟擲異常
if (op == NULL || !PyTuple_Check(op)) {
PyErr_BadInternalCall();
return NULL;
}
// 內部的具體實現方法
return tupleslice((PyTupleObject *)op, i, j);
}
static PyObject *
tupleslice(PyTupleObject *a, Py_ssize_t ilow,
Py_ssize_t ihigh)
{
PyTupleObject *np;
PyObject **src, **dest;
Py_ssize_t i;
Py_ssize_t len;
// 計算索引位置
if (ilow < 0)
ilow = 0;
if (ihigh > Py_SIZE(a))
ihigh = Py_SIZE(a);
if (ihigh < ilow)
ihigh = ilow;
// 如果是[:]的操作,則直接返回源元組物件a的指標,即絕對引用
if (ilow == 0 && ihigh == Py_SIZE(a) && PyTuple_CheckExact(a)) {
Py_INCREF(a);
return (PyObject *)a;
}
// 初始化新的切片物件元組長度
len = ihigh - ilow;
// 開始切片,建立了一個新元組np
np = (PyTupleObject *)PyTuple_New(len);
if (np == NULL)
return NULL;
src = a->ob_item + ilow;
dest = np->ob_item;
// 對源元組中的資料項的引用計數+1
for (i = 0; i < len; i++) {
PyObject *v = src[i];
Py_INCREF(v);
dest[i] = v;
}
// 返回切片物件新元組np的引用
return (PyObject *)np;
}
快取相關
static void
tupledealloc(PyTupleObject *op)
{
Py_ssize_t i;
Py_ssize_t len = Py_SIZE(op);
PyObject_GC_UnTrack(op);
Py_TRASHCAN_SAFE_BEGIN(op)
// 如果元組的長度大於0,則不是一個非空元組
if (len > 0) {
i = len;
// 將內部的資料項引用計數都 - 1
while (--i >= 0)
Py_XDECREF(op->ob_item[i]);
#if PyTuple_MAXSAVESIZE > 0
// 準備快取,判斷num_free是否小於20,並且單向連結串列中的已快取元組個數小於2000
if (len < PyTuple_MAXSAVESIZE &&
numfree[len] < PyTuple_MAXFREELIST &&
Py_TYPE(op) == &PyTuple_Type)
{
// 新增至連結串列頭部
op->ob_item[0] = (PyObject *) free_list[len];
// 將num_free + 1
numfree[len]++;
free_list[len] = op;
goto done; /* return */
}
#endif
}
// 記憶體中進行銷燬
Py_TYPE(op)->tp_free((PyObject *)op);
done:
Py_TRASHCAN_SAFE_END(op)
}