当调用len时,Python如何确保__len__的返回值是一个整数?

Nat*_*men 2 python class operator-overloading

class foo:
    def __init__(self, data):
        self.data = data
    def __len__(self):
        return self.data
Run Code Online (Sandbox Code Playgroud)

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

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

Set*_*ton 6

TL; DR

在C级,Python插入__len__一个特殊的插槽,捕获调用的输出__len__并对其进行一些验证以确保它是正确的.


为了回答这个问题,我们不得不稍微考虑len在Python中调用引擎盖下发生的事情.

首先,让我们建立一些行为.

>>> 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
Run Code Online (Sandbox Code Playgroud)

当你调用时len,builtin_len会调用C函数.我们来看看这个.

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);
}
Run Code Online (Sandbox Code Playgroud)

您将注意到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);
}
Run Code Online (Sandbox Code Playgroud)

它检查类型是否定义了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);  // <== THIS IS A MACRO for (PyVarObject*) a->ob_size;
}

static PySequenceMethods list_as_sequence = {
    ...
    (lenfunc)list_length,                       /* sq_length */
    ...
};
Run Code Online (Sandbox Code Playgroud)

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)."),
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

这里有两点需要注意:

  1. 如果返回负数,ValueError则会显示带有消息的a "__len__() should return >= 0".这正是我试图打电话时收到的错误len(foo(-1))!
  2. Python试图在返回之前强制转换__len__为a 的返回值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;    
    /* OMITTED FOR BREVITY */
Run Code Online (Sandbox Code Playgroud)

这里的相关位是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;
}
Run Code Online (Sandbox Code Playgroud)

根据您收到的错误,我们可以看到'5'没有定义__index__.我们可以为自己验证:

>>> '5'.__index__()
Traceback:
...
AttributeError: 'str' object has no attribute '__index__'
Run Code Online (Sandbox Code Playgroud)