• 《python解释器源码剖析》第5章--python中的tuple对象


    5.0 序

    我们知道对于tuple,就相当于不支持元素添加、修改、删除等操作的list

    5.1 PyTupleObject对象

    tuple的实现机制非常简单,可以看做是在list的基础上删除了增删改等操作。既然如此,那要元组有什么用呢?毕竟元组的功能只是list的子集。元组存在的最大一个特点就是,它可以作为字典的key、以及可以作为集合的元素。因为字典和集合存储数据的原理是哈希表,字典和集合我们后续章节会说。对于list这样的可变对象来说是可以动态改变的,而哈希值是一开始就计算好的,显然如果支持动态修改的话,那么哈希值肯定会变,这是不允许的。所以我们希望key是一个序列,显然元组再适合不过了。

    从tuple的特点也能看出:tuple的底层是一个变长对象,但同时也是一个不可变对象。

    typedef struct {
        PyObject_VAR_HEAD
        PyObject *ob_item[1];
    } PyTupleObject;
    

    可以看到,对于不可变对象来说,它底层结构体定义也非常简单。一个引用计数、一个类型、一个指针数组索引为1的元素的地址,至于这里为什么是1,而且我们在PyLongObject中好像看到索引也是1,这一点不必纠结,就把它当成第一个元素的内存地址即可。

    并且我们发现不想列表,元组没有allocated,这是因为它是不可变的,不支持resize操作。至于维护的值,同样是指针组成的数组,数组里面的每一个指针都指向了具体的值。

    5.2 PyTupleObject对象的创建

    正如list一样,python创建PyTupleObject也提供了类似的初始化方法

    PyObject *
    PyTuple_New(Py_ssize_t size)
    {
        PyTupleObject *op;
        Py_ssize_t i;
        if (size < 0) {
            PyErr_BadInternalCall();
            return NULL;
        }
    #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;
        }
        if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
            free_list[size] = (PyTupleObject *) op->ob_item[0];
            numfree[size]--;
    #ifdef COUNT_ALLOCS
            fast_tuple_allocs++;
    #endif
            /* Inline PyObject_InitVar */
    #ifdef Py_TRACE_REFS
            Py_SIZE(op) = size;
            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 = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
            if (op == NULL)
                return 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
        _PyObject_GC_TRACK(op);
        return (PyObject *) op;
    }
    
    

    和PyListObject初始化类似,同样需要做一些类型检测,内存是否溢出等等。

    5.2.1 查看元素

    tuple和list一样,支持通过索引来获取元素,并且两者的性能是类似的。

    PyObject *
    PyTuple_GetItem(PyObject *op, Py_ssize_t i)
    {
        //类型检测
        if (!PyTuple_Check(op)) {
            PyErr_BadInternalCall();
            return NULL;
        }
        
        //索引是否越界
        if (i < 0 || i >= Py_SIZE(op)) {
            PyErr_SetString(PyExc_IndexError, "tuple index out of range");
            return NULL;
        }
        //直接返回ob_item[i]
        //转化成了PyTupleObject *,证明tuple里面存储的也是指针
        //但是在python层面上,打印会自动打印指针所指向的值
        return ((PyTupleObject *)op) -> ob_item[i];
    }
    

    5.2.2 设置元素

    我们知道tuple是不可变的,但是如果我们非要设置会怎么样呢?

    我们看一下源码

    int
    PyTuple_SetItem(PyObject *op, Py_ssize_t i, PyObject *newitem)
    {
        PyObject **p;
        if (!PyTuple_Check(op) || op->ob_refcnt != 1) {
            Py_XDECREF(newitem);
            PyErr_BadInternalCall();
            return -1;
        }
        if (i < 0 || i >= Py_SIZE(op)) {
            Py_XDECREF(newitem);
            PyErr_SetString(PyExc_IndexError,
                            "tuple assignment index out of range");
            return -1;
        }
        p = ((PyTupleObject *)op) -> ob_item + i;
        Py_XSETREF(*p, newitem);
        return 0;
    }
    

    从源码上我们看到这个list是一样的啊,那python是怎么判断这是元组并且报错的呢?还记得我们之前说的tp_as_number,tp_as_sequence,tp_as_mapping吗?我们继续看,Object/abstract.c

    int
    PySequence_SetItem(PyObject *s, Py_ssize_t i, PyObject *o)
    {
        PySequenceMethods *m;
    	
        //s为NULL,也就是PYTHON中的None,直接报错返回-1
        if (s == NULL) {
            null_error();
            return -1;
        }
    	
        //获取对应类型的tp_as_sequence属性
        m = s->ob_type->tp_as_sequence;
        //如果m存在,像int就没有这个属性。
        //但是字符串、和元组都有,因此还需要一层判断
        //如果m->sq_ass_item还不为NULL的话,直接设置元素
        //否则的话
        if (m && m->sq_ass_item) {
            if (i < 0) {
                if (m->sq_length) {
                    Py_ssize_t l = (*m->sq_length)(s);
                    if (l < 0) {
                        assert(PyErr_Occurred());
                        return -1;
                    }
                    i += l;
                }
            }
            return m->sq_ass_item(s, i, o);
        }
    	//直接提示:xx不支持设置值
        type_error("'%.200s' object does not support item assignment", s);
        return -1;
    }
    

    5.3 静态资源缓存

    列表和元组两者在通过索引查找元素的时候是一致的,但是元组除了能作为字典的key之外,还有一个特点,就是分配的速度比较快。一方面是因为由于其不可变性,使得在编译的时候就确定了,使得分配的速度快之外、另一方面就是它还具有静态资源缓存的作用。

    对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。这样,下次我们再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。

    from timeit import timeit
    
    
    t1 = timeit(stmt="x1 = [1, 2, 3, 4, 5]", number=1000000)
    t2 = timeit(stmt="x2 = (1, 2, 3, 4, 5)", number=1000000)
    
    print(round(t1, 2))  # 0.05
    print(round(t2, 2))  # 0.01
    

    可以看到用时,元组只是列表的五分之一。这便是元组的另一个优势,可以将资源缓存起来。

  • 相关阅读:
    ADO 缓存更新
    DBNavigator中把insert变为append
    JQuery 选择器
    VS2013默认打开HTML文件没有设计视图
    windows 关机 重启 命令
    java如何计算两个日期之间相差多少天?
    java的list集合操作List<T>转化List<Long>
    ExcelUtil工具类
    oracle的START WITH CONNECT BY PRIOR用法
    oracle的with as用法
  • 原文地址:https://www.cnblogs.com/traditional/p/11764673.html
Copyright © 2020-2023  润新知