Python如何确保在调用len时__len__的返回值为整数?

5
class foo:
    def __init__(self, data):
        self.data = data
    def __len__(self):
        return self.data

如果我通过为data传入一个字符串来运行它,当调用此类的实例时调用len会出错。具体来说,我会得到'str' object cannot be interpreted as an integer的错误信息。

那么,在__len__中的return语句必须是整数吗?我认为如果我要覆盖它,应该能够输出任何我想要的东西,那么为什么不可能呢?


4
你可以在 __len__ 方法中返回任何你想要的内容,但这并不意味着 len 函数会接受它。len 函数不仅仅是简单地定义为 def len(x): return x.__len__() - user2357112
3
根据文档,该函数应该返回一个大于等于0的整数。这意味着,如果您没有这样做,就不能保证能正常工作。 - Archimaredes
1个回答

18

简短回答

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)
/*[clinic end generated code: output=fa7a270d314dfb6c input=bc55598da9e9c9b5]*/
{
    Py_ssize_t res;

    res = PyObject_Size(obj);  // <=== THIS IS WHAT IS IMPORTANT!!!
    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);  // <==== THIS IS WHAT IS IMPORTANT!!!

    return PyMapping_Size(o);
}

它检查类型是否定义了sq_length函数(序列长度),如果是,则调用它以获取长度。似乎在C级别上,Python将所有定义 __len__ 的对象分类为序列或映射(即使我们在Python级别上并不这样认为)。在我们的情况下,Python认为这个类是一个序列,因此它调用sq_length


让我们简单地说明一下:对于内置类型(例如listset等),Python实际上不会调用函数来计算长度,而是访问存储在C结构中的值,因此非常快。每种内置类型都定义了如何通过将访问器方法分配给sq_length来访问它。让我们快速查看一下如何为列表实现这个过程

static Py_ssize_t
list_length(PyListObject *a)
{
    return Py_SIZE(a);  // <== THIS IS A MACRO for (PyVarObject*) a->ob_size;
}

static PySequenceMethods list_as_sequence = {
    ...
    (lenfunc)list_length,                       /* sq_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);  // <=== HERE!!!
    Py_DECREF(res);
    if (len < 0) {  // <== AND HERE!!!
        if (!PyErr_Occurred())
            PyErr_SetString(PyExc_ValueError,
                            "__len__() should return >= 0");
        return -1;
    }
    return len;
}

这里有两件事情需要注意:

  1. 如果返回一个负数,将会抛出一个ValueError错误,并显示信息"__len__() should return >= 0"。当我尝试调用len(foo(-1))时,就是收到完全一样的错误!
  2. 在返回之前,Python会试图将__len__的返回值强制转换为Py_ssize_tPy_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;    
    /* OMITTED FOR BREVITY */

这里涉及到的内容在PyNumber_Index中,Python使用它将任意对象转换为适用于索引的整数。 这是你问题的实际答案所在。 我做了一些注释。

PyObject *
PyNumber_Index(PyObject *item)
{
    PyObject *result = NULL;
    if (item == NULL) {
        return null_error();
    }

    if (PyLong_Check(item)) {  // IS THE OBJECT ALREADY AN int? IF SO, RETURN IT NOW.
        Py_INCREF(item);
        return item;
    }
    if (!PyIndex_Check(item)) {  // DOES THE OBJECT DEFINE __index__? IF NOT, FAIL.
        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)) {  // IF __index__ DOES NOT RETURN AN int, FAIL.
        PyErr_Format(PyExc_TypeError,
                     "__index__ returned non-int (type %.200s)",
                     result->ob_type->tp_name);
        Py_DECREF(result);
        return NULL;
    }
    /* Issue #17576: warn if 'result' not of exact type int. */
    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__'

感谢您的详细回复。很酷看到所有在幕后进行的工作,使Python正常运行。 - Nate Stemen
非常详细的解释。但是有没有人能用一个段落或更少的内容来解释一下呢? - Anshuman Jayaprakash
1
@AnshumanJayaprakash 这就是答案顶部的“TL;DR”(“太长了;没读过”)段落的用途。 - SethMMorton
1
@AnshumanJayaprakash 我更改了顶部的措辞,以明确第一段是摘要。 - SethMMorton
1
非常有启发性的深入探讨。教会了我一些我不知道的东西。 - Mark Ransom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接