03月04, 2022

番外-为什么 Tuple 是只读的

一位读者朋友私信一个问题,非常有意思: "为什么 Tuple 是只读的?"。今天结合代码,一探究竟。

image.png

Python 中所有类型都是 type 对象的实例,甚至 type 自身也是 type 的实例,在 python 交互模式下,很容易通过下面的方式确定:

>>> str.__class__
<class 'type'>
>>> type(type)
<class 'type'>
>>> isinstance(str,  type)
True
>>> isinstance(type,  type)
True

可以很容易猜到,其他所有类型都是 type 的实例。Python 的 class 语法,实际上也是语法糖,比如:

class MyClass:
    def say(self):
        print("hello!")

实际上是创建 type 的实例:

def say(self):
    print("hello!")

MyClass = type('MyClass', (object,), dict(say = say))

这两种方式创建的 MyClass 类型,都可以得到下面的结果:

>>> myInstance = MyClass()
>>> myInstance.say()
hello!

第二种方式可以很清楚的看到创建 type 实例的调用 type(name, inherent, memberdict)

所以,Python 的类型本身也是实例,类型也和普通实例类似,内部拥有一系列属性,当然也包含该类型支持的方法。

Tuple 类型是内置类型,在 C 语言层面有明确的编码实现

PyTypeObject PyTuple_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "tuple",
    /*省略了一些无关的内容*/
    tuple_methods,                              /* tp_methods */
    /* ... */
};

C 语言层面通过 PyTypeObject 表示一种类型 (较早版本 Python 基本类型不是 Object),这是一个很大的结构体,我们注意到其中有个成员 tp_methods,显然它描述了这个类型支持哪些方法,Tuple 类型的 tuple_methods定义

static PyMethodDef tuple_methods[] = {
    TUPLE___GETNEWARGS___METHODDEF
    TUPLE_INDEX_METHODDEF
    TUPLE_COUNT_METHODDEF
    {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
    {NULL,              NULL}           /* sentinel */
};

可以看到 Tuple 类型支持 4 种方法,TUPLE_INDEX_METHODDEF 和 TUPLE_COUNT_METHODDEF 分别是 tuple 的 indexcount 方法的实现,只需要简单的检索代码就可以确定。

下面是 List 类型的方法定义:

static PyMethodDef list_methods[] = {
    {"__getitem__", (PyCFunction)list_subscript, METH_O|METH_COEXIST, "x.__getitem__(y) <==> x[y]"},
    LIST___REVERSED___METHODDEF
    LIST___SIZEOF___METHODDEF
    LIST_CLEAR_METHODDEF
    LIST_COPY_METHODDEF
    LIST_APPEND_METHODDEF
    LIST_INSERT_METHODDEF
    LIST_EXTEND_METHODDEF
    LIST_POP_METHODDEF
    LIST_REMOVE_METHODDEF
    LIST_INDEX_METHODDEF
    LIST_COUNT_METHODDEF
    LIST_REVERSE_METHODDEF
    LIST_SORT_METHODDEF
    {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
    {NULL,              NULL}           /* sentinel */
};

仅仅通过宏的字面,也很容易猜到 List 类型实现了什么方法。

image.png

对比 List 和 Tuple 类型的方法数组,你大概就能明白,为什么 Tuple 是只读的,因为该类型对象中的 tp_methods 数组没有可写接口的实现!这也造成你无法在 Python 中更新 Tuple 的成员。

C 世界中 Python 类型的方法用 PyMethodDef 结构表示,它的结构很简单,忽略掉一些细节,每一个方法被表示为:

image.png

这样,试图对某个类型的变量调用方法时,VM 会在类型的方法数组中,按字符串匹配方法名称(实际上会优化,但本质上就是字符串匹配),并调用对应的函数。如果在类型方法数组中的找不到被调用的方法名,就抛出 object has no attribute 异常。

这里也可以窥探到 Python 对 C-string 的使用频度比较高,好在 Python 内部对字符串的访问做了优化,之前的文章有提到这一点,我们就不用为它担心了。

本文链接:http://www.thinkinpython.com/post/ex_why_tuple_ro.html

-- EOF --