一位读者朋友私信一个问题,非常有意思: "为什么 Tuple 是只读的?"。今天结合代码,一探究竟。
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 的 index 和 count 方法的实现,只需要简单的检索代码就可以确定。
下面是 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 类型实现了什么方法。
对比 List 和 Tuple 类型的方法数组,你大概就能明白,为什么 Tuple 是只读的,因为该类型对象中的 tp_methods 数组没有可写接口的实现!这也造成你无法在 Python 中更新 Tuple 的成员。
C 世界中 Python 类型的方法用 PyMethodDef 结构表示,它的结构很简单,忽略掉一些细节,每一个方法被表示为:
这样,试图对某个类型的变量调用方法时,VM 会在类型的方法数组中,按字符串匹配方法名称(实际上会优化,但本质上就是字符串匹配),并调用对应的函数。如果在类型方法数组中的找不到被调用的方法名,就抛出 object has no attribute 异常。
这里也可以窥探到 Python 对 C-string 的使用频度比较高,好在 Python 内部对字符串的访问做了优化,之前的文章有提到这一点,我们就不用为它担心了。