简短回答
Python在C级别中将__len__
插入到一个特殊槽位中,该槽位捕获对__len__
的调用输出,并对其进行一些验证以确保它是正确的。
详细回答
为了回答这个问题,我们需要深入探讨在Python中调用 len
时发生了什么。
首先,让我们建立一些行为。
>>> class foo:
... def __init__(self, data):
... self.data = data
... def __len__(self):
... return self.data
...
>>> len(foo(-1))
Traceback:
...
ValueError: __len__() should return >= 0
>>> len(foo('5'))
Traceback:
...
TypeError: 'str' object cannot be interpreted as an integer
>>> len(foo(5))
5
当你调用
len
时,会触发 C 函数
builtin_len
。现在我们来看看这个函数。
static PyObject *
builtin_len(PyObject *module, PyObject *obj)
{
Py_ssize_t res;
res = PyObject_Size(obj);
if (res < 0 && PyErr_Occurred())
return NULL;
return PyLong_FromSsize_t(res);
}
你会注意到被调用的
PyObject_Size
函数 - 这个函数将返回任意 Python 对象的大小。让我们继续深入探究。
Py_ssize_t
PyObject_Size(PyObject *o)
{
PySequenceMethods *m;
if (o == NULL) {
null_error();
return -1;
}
m = o->ob_type->tp_as_sequence;
if (m && m->sq_length)
return m->sq_length(o);
return PyMapping_Size(o);
}
它检查类型是否定义了sq_length
函数(序列长度),如果是,则调用它以获取长度。似乎在C级别上,Python将所有定义 __len__
的对象分类为序列或映射(即使我们在Python级别上并不这样认为)。在我们的情况下,Python认为这个类是一个序列,因此它调用sq_length
。
让我们简单地说明一下:对于内置类型(例如list
、set
等),Python实际上不会调用函数来计算长度,而是访问存储在C结构中的值,因此非常快。每种内置类型都定义了如何通过将访问器方法分配给sq_length
来访问它。让我们快速查看一下如何为列表实现这个过程:
static Py_ssize_t
list_length(PyListObject *a)
{
return Py_SIZE(a);
}
static PySequenceMethods list_as_sequence = {
...
(lenfunc)list_length,
...
};
ob_size
存储对象的大小(即列表中的元素数)。因此,当调用sq_length
时,它被发送到list_length
函数以获取ob_size
的值。
好的,那么这就是内置类型的实现方式...那么对于像我们的foo
这样的自定义类,它是如何工作的呢?由于“dunder方法”(例如__len__
)是特殊的,Python会在我们的类中检测到它们并对它们进行特殊处理(具体来说,将它们插入到特殊槽中)。
大部分情况下都在typeobject.c中处理。__len__
函数被拦截并分配给sq_length
槽(就像内置函数一样!)文件底部附近。
SQSLOT("__len__", sq_length, slot_sq_length, wrap_lenfunc,
"__len__($self, /)\n--\n\nReturn len(self)."),
slot_sq_length
函数是我们最终可以回答您问题的地方。
static Py_ssize_t
slot_sq_length(PyObject *self)
{
PyObject *res = call_method(self, &PyId___len__, NULL);
Py_ssize_t len;
if (res == NULL)
return -1;
len = PyNumber_AsSsize_t(res, PyExc_OverflowError);
Py_DECREF(res);
if (len < 0) {
if (!PyErr_Occurred())
PyErr_SetString(PyExc_ValueError,
"__len__() should return >= 0");
return -1;
}
return len;
}
这里有两件事情需要注意:
- 如果返回一个负数,将会抛出一个
ValueError
错误,并显示信息"__len__() should return >= 0"
。当我尝试调用len(foo(-1))
时,就是收到完全一样的错误! - 在返回之前,Python会试图将
__len__
的返回值强制转换为Py_ssize_t
(Py_ssize_t
是有符号的size_t
版本,类似于一种特殊类型的整数,保证能够对容器中的元素进行索引)。
好的,让我们看看PyNumber_AsSsize_t
的实现。它有点长,所以我会省略掉不相关的部分。
Py_ssize_t
PyNumber_AsSsize_t(PyObject *item, PyObject *err)
{
Py_ssize_t result;
PyObject *runerr;
PyObject *value = PyNumber_Index(item);
if (value == NULL)
return -1;
这里涉及到的内容在PyNumber_Index
中,Python使用它将任意对象转换为适用于索引的整数。 这是你问题的实际答案所在。 我做了一些注释。
PyObject *
PyNumber_Index(PyObject *item)
{
PyObject *result = NULL;
if (item == NULL) {
return null_error();
}
if (PyLong_Check(item)) {
Py_INCREF(item);
return item;
}
if (!PyIndex_Check(item)) {
PyErr_Format(PyExc_TypeError,
"'%.200s' object cannot be interpreted "
"as an integer", item->ob_type->tp_name);
return NULL;
}
result = item->ob_type->tp_as_number->nb_index(item);
if (!result || PyLong_CheckExact(result))
return result;
if (!PyLong_Check(result)) {
PyErr_Format(PyExc_TypeError,
"__index__ returned non-int (type %.200s)",
result->ob_type->tp_name);
Py_DECREF(result);
return NULL;
}
if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
"__index__ returned non-int (type %.200s). "
"The ability to return an instance of a strict subclass of int "
"is deprecated, and may be removed in a future version of Python.",
result->ob_type->tp_name)) {
Py_DECREF(result);
return NULL;
}
return result;
}
根据你收到的错误,我们可以看到'5'
没有定义__index__
。我们可以自行验证:
>>> '5'.__index__()
Traceback:
...
AttributeError: 'str' object has no attribute '__index__'
__len__
方法中返回任何你想要的内容,但这并不意味着len
函数会接受它。len
函数不仅仅是简单地定义为def len(x): return x.__len__()
。 - user2357112