Python数据结构(二)tuple使用和原理(附面试细节)

Python的元组(tuple)

Python 的元组与列表类似A,不同之处在于元组的元素不能修改。

元组使用小括号,列表使用方括号。元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可。如:t=(1, 2, 3)。但是元组不能二次赋值,一旦创建成功就不能修改,所以一般称为只读列表。

tuple的特点

上面说了tuple是不可更改的,只读型类型。这有什么好处和不足呢?
从表面上来看,tuple和list两种类型只是不可改和可改的区别,但是数据的不可修改性在程序设计中也是非常重要的。例如,当需要将数据作为参数传递给API,但不希望API修改参数时,就可以传递一个元组类型;再如,当需要定义一组key时,也可以采用tuple类型,但不可使用list类型。因此可以说元组和列表是互为补充的数据类型。

初学会感觉元组的不可修改特性可能会让元组变得非常不灵活,因为元组作为容器对象,很多时候需要对容器的元素进行修改,这在元组中是不允许的。所以对于初学者除了少数场景下需要tuple,常用的都使用list就行了。

创建tuple

创建元组和list区别不大,但是细节需要注意,只有一个元素时,需要在后面加上,。

t1 = () # 创建空元组
print(t1)
# ()
print(type(t1))
#

t2 = (1996, 2018, 'python', 'top') # 常见创建元组方式
print(t2)
# (1996, 2018, 'python', 'top')

t3 = (1, 2) * 2 # 复制一次
print(t3)
# (1, 2, 1, 2)

t4 = (1,) # 单个元素后面要加上, 这是必须的
print(t4)
# (1,)

修改元组???

上面不是说元组不可修改?元组是不可修改,我们看一下修改会出现什么情况。

t1=(1,2,3)
t1[1]=2
print(t1)
# # 下面报错了,看到没不可修改
# Traceback (most recent call last):
# File "tuple20190909.py", line 22, in
# t1[1]=2
# TypeError: 'tuple' object does not support item assignment

刚刚我们是修改一个元组对象是不可行的,但是,注意下面例子修改是可行的。

t1=(1,2,3)
t2=(1,2)
t1=t1+t2# 元组不可修改,但是这句话是通过元组运算得到新元组赋值给t1
print(t1)
# (1, 2, 3, 1, 2)

元组运算符

与字符串一样,元组之间可以使用 + 号和 * 号进行运算。这就意味着他们可以组合和复制,运算后会生成一个新的元组。

Python数据结构(二)tuple使用和原理(附面试细节)

元组索引

因为元组也是一个序列,所以我们可以访问元组中的指定位置的元素,也可以截取索引中的一段元素,如下所示:

L = ('spam', 'Spam', 'SPAM!')

Python数据结构(二)tuple使用和原理(附面试细节)


元组遍历和列表遍历类似,比如:

L = ('spam', 'Spam', 'SPAM!')
for x in L:
print(x)
# spam
# Spam
# SPAM!

元组的内置函数

元组内置函数也类似于列表,有一下几个常用函数:

  • 1.len(tuple):计算元组元素个数。
  • 2.max(tuple):输出元组中的最大值。
  • 3.min(tuple):输出元组中的最小值。
  • 4.tuple(seq):将列表转化为元组。

元组妙用

巧用zip函数。通过元组和zip函数,可以按次序取多个对象的值组成新的元组,这对数据处理非常有用。

t1=(1,2,3)
t2=(4,5,6)
t3=(t1,t2)

t4=list(zip(*t3))
print(t4)
# [(1, 4), (2, 5), (3, 6)]

使用tuple做dict字典的key。tuple可以作为key,而list不行,因为tuple是不可变对象。这在以后哈希处理也非常有用。

t1 = (1, 2, 3)
t2 = [4, 5, 6]
d = {} # 这是字典

d[t1] = 1
print(d)
# {(1, 2, 3): 1}

d[t2] = 2 # 报错了
# Traceback (most recent call last):
# File "tuple20190909.py", line 58, in
# d[t2]=2
# TypeError: unhashable type: 'list'

【补充内容】

tuple底层实现原理

tuple底层定义:

typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
  • PyObject_VAR_HEAD
    PyTupleObject在底层是个变长对象(需要存储列表元素个数).
    虽然, 在python中, tuple是不可变对象
  • PyObject *ob_item[1];
    指向存储元素的数组
Python数据结构(二)tuple使用和原理(附面试细节)

具体源码如下:
【1】构造方法定义

  • 如果size=0, 从free_list[0]取, 直接返回
  • 否则, 确认free_list[size], 是否可用, 可用获取
  • 否则, 从内存分配新的空间
  • 初始化, 返回
PyObject * 

PyTuple_New(register Py_ssize_t size)
{
register PyTupleObject *op;
Py_ssize_t i;

// 大小为负数, return
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}

// 如果大小=0, 空元组, 直接取free_list第一个返回
#if PyTuple_MAXSAVESIZE > 0
if (size == 0 && free_list[0]) {
op = free_list[0];
Py_INCREF(op);

#ifdef COUNT_ALLOCS
tuple_zero_allocs++;
#endif

return (PyObject *) op;
}

// 如果free_list可分配, 从free_list取一个
if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
// 上面 op = free_list[size] 取得单链表头
// free_list指向单链表下一个元素, 对应位置阈值--
free_list[size] = (PyTupleObject *) op->ob_item[0];
numfree[size]--;

#ifdef COUNT_ALLOCS
fast_tuple_allocs++;
#endif

// 初始化 ob_size和ob_type
/* Inline PyObject_InitVar */
#ifdef Py_TRACE_REFS
Py_SIZE(op) = size;
Py_TYPE(op) = &PyTuple_Type;
#endif
_Py_NewReference((PyObject *)op);
}
else
#endif // free_list不可用

{
// 计算空间
Py_ssize_t nbytes = size * sizeof(PyObject *);
/* Check for overflow */
if (nbytes / sizeof(PyObject *) != (size_t)size ||
(nbytes > PY_SSIZE_T_MAX - sizeof(PyTupleObject) - sizeof(PyObject *)))
{
return PyErr_NoMemory();
}

// 分配内存
op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
if (op == NULL)
return NULL;
}

// 初始化ob_item每个元素
for (i=0; i < size; i++)
op->ob_item[i] = NULL;

// 第一次分配空数组, 将其放入free_list第一个位置
#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

_PyObject_GC_TRACK(op);

// 返回
return (PyObject *) op;
}

【2】回收

  • 回收ob_item每个元素
  • 如果符合条件, 放入到free_list
  • 否则, 回收
static void
tupledealloc(register PyTupleObject *op)
{
register Py_ssize_t i;
// 获取元素个数
register Py_ssize_t len = Py_SIZE(op);

PyObject_GC_UnTrack(op);
Py_TRASHCAN_SAFE_BEGIN(op)


if (len > 0) {
i = len;
// 遍历, 析构每个元素
while (--i >= 0)
Py_XDECREF(op->ob_item[i]);

// 与对象缓冲池相关
#if PyTuple_MAXSAVESIZE > 0
if (len < PyTuple_MAXSAVESIZE &&
numfree[len] < PyTuple_MAXFREELIST &&
Py_TYPE(op) == &PyTuple_Type)
{
op->ob_item[0] = (PyObject *) free_list[len];
numfree[len]++;
free_list[len] = op;
goto done; /* return */
}
#endif

}
// 调用回收
Py_TYPE(op)->tp_free((PyObject *)op);

done:
Py_TRASHCAN_SAFE_END(op)
}

【3】tuple对象缓冲池

定义:

/* Speed optimization to avoid frequent malloc/free of small tuples */
#ifndef PyTuple_MAXSAVESIZE
#define PyTuple_MAXSAVESIZE 20
#endif

#ifndef PyTuple_MAXFREELIST
#define PyTuple_MAXFREELIST 2000
#endif

#if PyTuple_MAXSAVESIZE > 0

static PyTupleObject *free_list[PyTuple_MAXSAVESIZE];
static int numfree[PyTuple_MAXSAVESIZE];
#endif
  • 作用: 优化小tuple的mall/free
  • PyTuple_MAXSAVESIZE = 20
    会被缓存的tuple长度阈值, 20, 长度<20的, 才会走对象缓冲池逻辑
  • PyTuple_MAXFREELIST 2000
    每种size的tuple最多会被缓存2000个
  • PyTupleObject *free_list[PyTuple_MAXSAVESIZE]
    free_list, 指针数组, 每个位置, 存储了指向一个单链表头的地址
  • numfree[PyTuple_MAXSAVESIZE]
    numfree, 一个计数数组, 存储free_list对应位置的单链表长度
  • free_list[0], 指向空数组, 有且仅有一个
Python数据结构(二)tuple使用和原理(附面试细节)


分享到:


相關文章: