[Python] 魔法方法__len__的底层实现
1 魔法方法__len__的介绍
1.1 魔法方法介绍
Python中的魔法方法是指以双下划线__开头和结尾的特殊方法,也称为"dunder methods"(double underscore methods)。这些方法允许你自定义类的行为,使其具有类似于内置类型的行为,例如加法、比较等。它们通常被称为Python的特殊方法,因为它们有着特殊的功能和语法。
以下是一些常见的Python魔法方法及其用途的例子:
__init__(self, ...)
: 初始化对象,在创建对象时调用该方法。
__str__(self)
: 返回对象的字符串表示,使用print()函数时会调用该方法。
__repr__(self)
: 返回对象的"official"字符串表示,通常用于调试和开发目的。
__len__(self)
: 返回对象的长度,通常用于支持内置len()函数。
__getitem__(self, key)
: 定义对象的索引操作,使其支持类似列表和字典的索引访问。
__setitem__(self, key, value)
: 定义对象的索引赋值操作。
__delitem__(self, key)
: 定义对象的删除操作。
__add__(self, other)
: 定义对象的加法操作,使其支持+运算符。
__sub__(self, other)
: 定义对象的减法操作,使其支持-运算符。
__eq__(self, other)
: 定义对象的相等比较操作,使其支持==运算符。
__lt__(self, other)
: 定义对象的小于比较操作,使其支持<运算符。
__gt__(self, other)
: 定义对象的大于比较操作,使其支持>运算符。
__call__(self, ...)
: 让对象可以像函数一样被调用。
1.2 例子
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
else:
raise TypeError("Unsupported operand type for +")
def __eq__(self, other):
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return False
# 创建两个Vector对象
v1 = Vector(1, 2)
v2 = Vector(3, 4)
# 使用自定义的魔法方法进行加法操作
v3 = v1 + v2
# 使用自定义的魔法方法进行相等比较
if v1 == v2:
print("v1 and v2 are equal")
else:
print("v1 and v2 are not equal")
print(v3) # 输出:Vector(4, 6)
1.2 __len__()
方法的介绍
该方法的时间复杂度为O(1)
,也就是说,它的性能是非常高的。当你调用内置的len()
函数并传递一个对象作为参数时,Python会尝试调用该对象的__len__()
方法来获取其长度。这使得自定义的对象可以像内置的容器对象(例如列表、元组、字符串等)一样支持len()函数。
__len__()
方法在你创建的类中可以根据需要进行定义,它应该返回一个表示对象长度的整数值。如果未定义__len__()
方法,当尝试使用len()函数获取对象的长度时,会抛出TypeError异常。
2 __len__()
的实现源码解读
2.1 Objects的实现
设计到底层的方法,由于对象和方法都是内建的,并不是由Python实现的,我们就不得不看一下它的C语言实现,所以我们先打开Objects的实现,如下,该定义可以在cpython/Include /object.h
中找到。
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
可以看到,在Objects中,存在一个ob_size
,用于对象的大小,我们在后面会发现__len__()
和它的关系。
2.2 __len__()
方法实现
该方法的实现可以在cpython/Python /bltinmodule.c
中找到,如下
/*[clinic input]
len as builtin_len
obj: object
/
Return the number of items in a container.
[clinic start generated code]*/
static PyObject *
builtin_len(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=fa7a270d314dfb6c input=bc55598da9e9c9b5]*/
{
Py_ssize_t res;
res = PyObject_Size(obj);
if (res < 0) {
assert(PyErr_Occurred());
return NULL;
}
return PyLong_FromSsize_t(res);
}
可以看到,这个函数主要做了以下事情:
- 调用了
PyObject_Size(obj)
函数,该函数的实现如下:
该函数主要是获取了指向对象头的指针,如果它的长度是合理的,就返回,如果对象不是序列对象(即没有定义Py_ssize_t // 返回类型是大小 PyObject_Size(PyObject *o) { if (o == NULL) { // 如果为空,则报错 null_error(); return -1; } /** 这是通过序列方法表调用对象的sq_length方法, 以获取序列对象的长度。sq_length是一个函数指针, 指向用于获取序列对象长度的函数 **/ PySequenceMethods *m = Py_TYPE(o)->tp_as_sequence; // 指针不为空 if (m && m->sq_length) { // 获取长度 Py_ssize_t len = m->sq_length(o); // 确保长度是非负的 assert(_Py_CheckSlotResult(o, "__len__", len >= 0)); return len; } return PyMapping_Size(o); }
sq_length
方法),则会调用PyMapping_Size(o)
来获取映射对象的大小。这个函数用于获取映射对象(例如字典)的键值对数量。 - 判断返回的结果,如果长度为非负,则转成
long
类型返回,否则报错并返回空。 - 以
list
为例,我们可以看到static PySequenceMethods list_as_sequence = { (lenfunc)list_length, /* sq_length */ (binaryfunc)list_concat, /* sq_concat */ (ssizeargfunc)list_repeat, /* sq_repeat */ (ssizeargfunc)list_item, /* sq_item */ 0, /* sq_slice */ (ssizeobjargproc)list_ass_item, /* sq_ass_item */ 0, /* sq_ass_slice */ (objobjproc)list_contains, /* sq_contains */ (binaryfunc)list_inplace_concat, /* sq_inplace_concat */ (ssizeargfunc)list_inplace_repeat, /* sq_inplace_repeat */ };
list
的长度和list_length
函数进行了关联,其定义如下
而static Py_ssize_t list_length(PyListObject *a) { return Py_SIZE(a); }
Py_SIZE
则返回了PyObject
当中的ob_size
。
3 总结
从该__len__()
方法,理解了为什么内建的方法可以做到高效,不仅是逻辑简单,更因为直接是C语言,效率更高。此外,也学习了如何查询Python内建函数对应的C语言实现,帮助我们理解和感叹Python语言设计的细节,对于其他函数可使用同样的方式学习了解。