楔子
在程序开发中,map、filter、zip 可以说是非常常见了,下面来从源码的角度分析一下它们的实现原理。首先需要说明的是,这几个不是函数,而是类。
map
map 是将一个序列中的每个元素都作用于同一个函数(类、方法也可以),当然,我们知道调用 map 的时候并没有马上执行,而是返回一个 map 对象。既然是对象,那么底层必有相关的定义。
// Python/bltinmodule.c
typedef struct {
PyObject_HEAD
PyObject *iters;
PyObject *func;
} mapobject;
解释一下里面的字段含义:
- PyObject_HEAD:见过很多次了,它是任何对象都会有的头部信息。包含一个引用计数 ob_refcnt,和一个指向类型对象的指针 ob_type;
- iters:一个指向 PyTupleObject 的指针。以 map(lambda x: x + 1, [1, 2, 3]) 为例,那么这里的 iters 就相当于是 ([1, 2, 3].__iter__(),)。至于为什么,分析源码的时候就知道了;
- func:显然就是函数指针了,PyFunctionObject *;
通过底层的结构体定义,我们也可以得知在调用 map 时并没有真正的执行,对于函数和可迭代对象,只是维护了两个指针去指向它。而一个 PyObject 占用 16 字节,再加上两个 8 字节的指针总共 32 字节。因此在 64 位机器上,任何一个 map 对象所占的大小都是 32 字节。
numbers = list(range(100000))
strings = ["abc", "def"]
# 都占 32 字节
print(map(lambda x: x * 3, numbers).__sizeof__()) # 32
print(map(lambda x: x * 3, strings).__sizeof__()) # 32
再来看看 map 的用法,Python 中的 map 不仅可以作用于一个序列,还可以作用于任意多个序列。
# x 就是列表里面的每一个元组
# x => (1, 2, 3)
# x => (4, 5, 6)
# x => (7, 8, 9)
m1 = map(
lambda x: x[0] + x[1] + x[2],
[(1, 2, 3), (4, 5, 6), (7, 8, 9)]
)
print(list(m1)) # [6, 15, 24]
# map 还可以接收任意多个可迭代对象
# (x, y, z) => (1, 4, 7)
# (x, y, z) => (2, 5, 8)
# (x, y, z) => (3, 6, 9)
m2 = map(
lambda x, y, z: x + y + z,
[1, 2, 3], [4, 5, 6], [7, 8, 9]
)
print(list(m2)) # [12, 15, 18]
# 所以底层结构体中的 iters 在这里就相当于
# ([1, 2, 3].__iter__(), [4, 5, 6].__iter__(), [7, 8, 9].__iter__())
# 我们说 map 的第一个参数是函数,后面可以接收任意多个可迭代对象
# 但是注意:可迭代对象的数量 和 函数的参数个数 一定要匹配
m3 = map(
lambda x, y, z: str(x) + y + z,
[1, 2, 3], ["a", "b", "c"], "abc"
)
print(list(m3)) # ['1aa', '2bb', '3cc']
# 但是可迭代对象之间的元素个数不要求相等,会以最短的为准
m3 = map(
lambda x, y, z: str(x) + y + z,
[1, 2, 3], ["a", "b", "c"], "ab"
)
print(list(m3)) # ['1aa', '2bb']
# 当然也支持更加复杂的形式
# (x, y) => ((1, 2), 3)
# (x, y) => ((2, 3), 4)
m5 = map(
lambda x, y: x[0] + x[1] + y,
[(1, 2), (2, 3)], [3, 4]
)
print(list(m5)) # [6, 9]
所以 map 会将后面所有可迭代对象中的每一个元素按照顺序依次取出,然后传递到函数中,因此函数的参数个数和可迭代对象的个数一定要相等。
那么 map 对象在底层是如何创建的呢?很简单,因为 map 是一个类,那么调用的时候一定会执行里面的 __new__ 方法。
// Python/bltinmodule.c
static PyObject *
map_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *it, *iters, *func;
mapobject *lz;
Py_ssize_t numargs, i;
// map 对象在底层对应的是 mapobject
// map 类本身在底层对应的则是 PyMap_Type
// _PyArg_NoKeywords 表示检验是否没有传递关键字参数
// 如果没传递,那么结果为真,传递了,结果为假
if (type == &PyMap_Type && !_PyArg_NoKeywords("map", kwds))
// 可以看到 map 不接收关键字参数,如果传递了,那么会报如下错误
// TypeError: map() takes no keyword arguments
return NULL;
// args 指向一个 PyTupleObject,位置参数都在这里面
// 包含了 1 个函数,以及 numargs - 1 个可迭代对象,显然 args 的长度至少为 2
numargs = PyTuple_Size(args);
// 如果 args 的长度小于 2,抛出 TypeError
if (numargs < 2) {
PyErr_SetString(PyExc_TypeError,
"map() must have at least two arguments.");
return NULL;
}
// 申请一个元组,容量为 numargs - 1
// 用于存放传递的所有可迭代对象对应的迭代器
iters = PyTuple_New(numargs-1);
if (iters == NULL)
return NULL;
// 依次循环
for (i=1 ; i<numargs ; i++) {
// 获取索引为 i 的可迭代对象,调用 __iter__,然后拿到对应的迭代器
it = PyObject_GetIter(PyTuple_GET_ITEM(args, i));
// 为 NULL 表示获取失败,说明遍历出的不是可迭代对象
// 但是 iters 这个元组已经申请了,所以减少其引用计数,将其销毁
if (it == NULL) {
Py_DECREF(iters);
return NULL;
}
// 将对应的迭代器设置在元组 iters 中
PyTuple_SET_ITEM(iters, i-1, it);
}
// 调用 PyMap_Type 的 tp_alloc,为其实例对象申请空间
lz = (mapobject *)type->tp_alloc(type, 0);
// 为 NULL 表示申请失败,减少 iters 的引用计数
if (lz == NULL) {
Py_DECREF(iters);
return NULL;
}
// 将 iters 赋值给 lz->iters
lz->iters = iters;
// 获取第一个参数,也就是函数
func = PyTuple_GET_ITEM(args, 0);
// 增加引用计数,因为该函数作为参数传递给 map 了
Py_INCREF(func);
// 将 func 赋值给 lz->func
lz->func = func;
// 转成泛型指针 PyObject *,然后返回
return (PyObject *)lz;
}
所以我们看到 map_new 做的工作很简单,就是实例化一个 map 对象,然后对内部的字段进行赋值。我们用 Python 来模拟一下上述过程:
class MyMap:
def __new__(cls, *args, **kwargs):
if kwargs:
raise TypeError("MyMap 不接收关键字参数")
numargs = len(args)
if numargs < 2:
raise TypeError("MyMap 至少接收两个参数")
# 元组内部的元素不可以改变,所以这里使用列表来模拟
# 创建一个长度为 numargs - 1 的列表,元素都是 None,模拟 C 中的 NULL
iters = [None] * (numargs - 1)
i = 1
while i < numargs: # 逐步循环
it = iter(args[i]) # 获取可迭代对象,得到其迭代器
iters[i - 1] = it # 设置在 iters 中
i += 1
# 为实例对象申请空间
instance = object.__new__(cls)
# 设置字段
instance.iters = iters
instance.func = args[0]
# 返回实例对象
return instance
m = MyMap(lambda x, y: x + y, [1, 2, 3], [11, 22, 33])
print(m) # <__main__.MyMap object at 0x7fa5dadcfc10>
print(m.func) # <function <lambda> at 0x7fa5dae36550>
print(m.func(2, 3)) # 5
print(
m.iters
) # [<list_iterator object at 0x7fa5dadcfc40>, <list_iterator object at 0x7fa5dadcf7c0>]
print([list(it) for it in m.iters]) # [[1, 2, 3], [11, 22, 33]]
我们看到非常简单,另外这里没有设置构造函数 __init__,这是因为 map 内部没有 __init__,它的成员字段都是在 __new__ 里面设置的。
# map 的 __init__ 实际上就是 object 的 __init__
print(map.__init__ is object.__init__) # True
调用 map 只是得到一个 map 对象,整个过程并没有进行任何的计算。如果要计算的话,我们可以调用 __next__、或者使用 for 循环等等。
m = map(lambda x: x + 1, [1, 2, 3, 4, 5])
print([i for i in m]) # [2, 3, 4, 5, 6]
# for 循环的背后本质上会调用迭代器的 __next__
# map 对象也是一个迭代器
m = map(lambda x: int(x) + 1, "12345")
while True:
try:
print(m.__next__())
except StopIteration:
break
"""
2
3
4
5
6
"""
当然上面都不是最好的方式,如果只是单纯地将元素迭代出来,而不做任何处理的话,那么交给 tuple、list、set 等类型对象才是最佳的方式,像 tuple(m)、list(m)、set(m) 等等。
所以如果你是 [x for x in it] 这种做法的话,那么更建议你使用 list(it),效率会更高,因为它用的是 C 中的 for 循环。当然不管是哪种做法,底层都是一个不断调用 __next__、逐步迭代的过程。
下面我们来看看 map 底层是怎么做的?
static PyObject *
map_next(mapobject *lz)
{
// small_stack 是一个 C 的栈数组,里面存放 PyObject *
// 显然它用来存放 map 中所有可迭代对象迭代出来的元素
// 而这个 _PY_FASTCALL_SMALL_STACK 是一个宏
// 定义在 Include/cpython/abstract.h 中,值为 5
// 如果函数参数的个数小于等于 5 的话,便可申请在栈中
// 之所以将其设置成 5,是为了不滥用 C 的栈,从而减少栈溢出的风险
PyObject *small_stack[_PY_FASTCALL_SMALL_STACK];
// 二级指针,指向 small_stack 数组的首元素,所以是 PyObject **
PyObject **stack;
Py_ssize_t niters, nargs, i;
// 函数调用的返回值
PyObject *result = NULL;
// 获取 iters 的长度,也就是迭代器的数量,当然同时也是调用函数时的参数数量
niters = PyTuple_GET_SIZE(lz->iters);
// 如果小于等于 5,那么获取这些迭代器中的元素之后
// 直接使用在 C 栈里面申请的数组进行存储
if (niters <= (Py_ssize_t)Py_ARRAY_LENGTH(small_stack)) {
stack = small_stack;
}
else {
// 如果超过了 5,那么不好意思,只能在堆区重新申请了
stack = PyMem_Malloc(niters * sizeof(stack[0]));
// 返回 NULL 表示申请失败,说明没有内存了,于是报错
if (stack == NULL) {
PyErr_NoMemory();
return NULL;
}
}
// 走到这里说明一切顺利,那么下面就开始迭代了
nargs = 0;
// 依次遍历,得到每一个迭代器
for (i=0; i < niters; i++) {
// 获取索引为 i 对应的迭代器
PyObject *it = PyTuple_GET_ITEM(lz->iters, i);
// 拿到 __next__,进行调用
PyObject *val = Py_TYPE(it)->tp_iternext(it);
// 如果 val 为 NULL,说明有一个迭代器迭代结束了,或者出错了
// 那么直接跳转到 exit 标签
if (val == NULL) {
goto exit;
}
// 将 val 设置在数组索引为 i 的位置,然后进行下一轮循环
// 也就是获取下一个迭代器中的元素
stack[i] = val;
// 如果可迭代对象的个数小于 5,比如 3,那么 stack 会申请在栈区
// 但是在栈区申请的话,stack 的长度固定为 5,此时后两个元素是无效的
// 所以要通过变量 nargs 记录有效参数的个数
nargs++;
}
/*
以 map(func, [1, 2, 3], ["xx", "yy", "zz"], [11, 22, 33]) 为例
那么 lz -> iters 就是 ([1, 2, 3].__iter__(),
["xx", "yy", "zz"].__iter__(),
[11, 22, 33].__iter__())
第一次迭代,for 循环结束时,stack 指向数组 [1, "xx", 11]
第二次迭代,for 循环结束时,stack 指向数组 [2, "yy", 22]
第三次迭代,for 循环结束时,stack 指向数组 [3, "zz", 33]
*/
// 进行调用,lz->func 就是函数,stack 指向函数的首个参数,nargs 就是参数个数
result = _PyObject_FastCall(lz->func, stack, nargs);
exit:
// 调用完毕之后,将 stack 里面指针指向的对象的引用计数减 1
for (i=0; i < nargs; i++) {
Py_DECREF(stack[i]);
}
// 如果 stack != small_stack,说明该 stack 是在堆区申请的,要释放
if (stack != small_stack) {
PyMem_Free(stack);
}
// 返回 result
return result;
}
我们用 Python 举例说明:
m = map(
lambda x, y, z: str(x) + y + str(z),
[1, 2, 3], ["xx", "yy", "zz"], [11, 22, 33]
)
# 第一次迭代,stack 指向 [1, "xx", 11]
print(m.__next__()) # 1xx11
# 第二次迭代,stack 指向 [2, "yy", 22]
print(m.__next__()) # 2yy22
# 第三次迭代,stack 指向 [3, "zz", 33]
print(m.__next__()) # 3zz33
以上就是 map 的用法。
filter
然后是 filter 的实现原理,看完了 map 之后,再看 filter 就简单多了。
lst = [1, 2, 3, 4, 5]
print(
list(filter(lambda x: x % 2 != 0, lst))
) # [1, 3, 5]
filter 接收两个参数,第一个是函数(类、方法),第二个是可迭代对象。然后当我们迭代的时候,会将可迭代对象中的每一个元素都传入到函数中,如果返回的结果为真,则该元素保留,为假则丢弃。
另外第一个参数除了可以是可调用对象之外,它还可以是 None。
lst = ["古明地觉", "", [], 123, 0, {}, [1]]
# 会自动选择结果为真的元素
print(list(filter(None, lst))) # ['古明地觉', 123, [1]]
至于为什么,一会看 filter 的实现就清楚了。首先来看看 filter 对象的底层结构:
typedef struct {
PyObject_HEAD
PyObject *func;
PyObject *it;
} filterobject;
我们看到和 map 对象是一致的,没有什么区别。因为 map、filter 都不会立刻调用,而是返回一个相应的对象。
static PyObject *
filter_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
// 函数和可迭代对象
PyObject *func, *seq;
// 可迭代对象的迭代器
PyObject *it;
// 返回值,filter 对象(指针)
filterobject *lz;
// filter 也不接收关键字参数
if (type == &PyFilter_Type && !_PyArg_NoKeywords("filter", kwds))
return NULL;
// 只接收两个参数
if (!PyArg_UnpackTuple(args, "filter", 2, 2, &func, &seq))
return NULL;
// 获取 seq 对应的迭代器
it = PyObject_GetIter(seq);
if (it == NULL)
return NULL;
// 为 filter 对象申请空间
lz = (filterobject *)type->tp_alloc(type, 0);
if (lz == NULL) {
Py_DECREF(it);
return NULL;
}
// 增加函数的引用计数
Py_INCREF(func);
// 初始化字段
lz->func = func;
lz->it = it;
// 返回泛型指针
return (PyObject *)lz;
}
和 map 是类似的,因为本质上它们做的事情都是差不多的,下面看看迭代过程。
static PyObject *
filter_next(filterobject *lz)
{
// 从迭代器中迭代出来的每一个元素
PyObject *item;
// 迭代器
PyObject *it = lz->it;
// 是否为真,1 表示真、0 表示假
long ok;
// 指针,用于保存 __next__
PyObject *(*iternext)(PyObject *);
// 如果 func == None 或者 func == bool,那么 checktrue 为真
// 后续会走单独的方法,所以给 func 传递一个 None 是完全合法的
int checktrue = lz->func == Py_None || lz->func == (PyObject *)&PyBool_Type;
// 迭代器的 __next__ 方法
iternext = *Py_TYPE(it)->tp_iternext;
// 无限循环
for (;;) {
// 迭代出的元素
item = iternext(it);
if (item == NULL)
return NULL;
// 如果 checkture,或者说如果 func == None || func == bool
if (checktrue) {
// 那么直接走 PyObject_IsTrue,判断 item 是否为真
ok = PyObject_IsTrue(item);
} else {
PyObject *good;
// 否则的话,调用我们传递的 func
good = PyObject_CallFunctionObjArgs(lz->func, item, NULL);
if (good == NULL) {
Py_DECREF(item);
return NULL;
}
// 判断返回值 good 是否为真
ok = PyObject_IsTrue(good);
// 减少其引用计数,因为它不被外界所使用
Py_DECREF(good);
}
// 如果 ok 大于 0,说明 func(item) 的结果为真,那么将 item 返回
if (ok > 0)
return item;
// 同时减少其引用计数
Py_DECREF(item);
// 小于 0 的话,表示 PyObject_IsTrue 调用失败了,返回 -1
// 但这种情况基本不会发生,除非解释器本身出 bug 了
if (ok < 0)
return NULL;
// 否则说明 ok 等于 0,item 为假
// 那么进行下一轮循环,直到找到一个为真的元素
}
}
我们用 Python 测试一下:
f = filter(None, [None, "", (), False, 123])
# 会从可迭代对象里面找到一个为真的元素并返回
# 但只有最后一个元素为真,所以返回 123
print(f.__next__()) # 123
# 这里所有的元素全部为假,于是第一次迭代就会抛异常
f = filter(None, [None, "", (), False])
try:
print(f.__next__())
except StopIteration:
print("迭代结束") # 迭代结束
所以看到这里你还觉得 Python 神秘吗,从源代码的层面我们看的非常流畅,只要你有一定的 C 语言基础即可。还是那句话,尽管我们不可能写一个解释器,因为背后涉及的东西太多了,但至少我们在看的过程中,很清楚底层到底在做什么。而且这背后的实现,如果让你设计一个方案的话,那么相信你也一样可以做到。
zip
最后看看 zip,它的中文意思是拉链,很形象,就是将多个可迭代对象的元素按照顺序依次组合起来。
print(
list(zip([1, 2, 3], [11, 22, 33], [111, 222, 333]))
) # [(1, 11, 111), (2, 22, 222), (3, 33, 333)]
# 其实 zip 完全可以用 map 实现
print(
list(map(lambda x, y, z: (x, y, z), [1, 2, 3], [11, 22, 33], [111, 222, 333]))
) # [(1, 11, 111), (2, 22, 222), (3, 33, 333)]
print(
list(map(lambda *args: args, [1, 2, 3], [11, 22, 33], [111, 222, 333]))
) # [(1, 11, 111), (2, 22, 222), (3, 33, 333)]
所以 zip 的底层实现同样很简单,我们来看一下:
typedef struct {
PyObject_HEAD
Py_ssize_t tuplesize;
PyObject *ittuple;
PyObject *result;
} zipobject;
以上便是 zip 对象的底层定义,这些字段的含义暂时先不讨论,它们会体现在 zip_new 方法中,我们到时候再说。
目前根据结构体里面的字段,可以得出一个 zipobject 实例占 40 字节,16 + 8 + 8 + 8,那么结果是不是这样呢?我们来试一下就知道了。
z1 = zip([1, 2, 3], [11, 22, 33])
z2 = zip([1, 2, 3, 4], [11, 22, 33, 44])
z3 = zip([1, 2, 3], [11, 22, 33], [111, 222, 333])
print(z1.__sizeof__()) # 40
print(z2.__sizeof__()) # 40
print(z3.__sizeof__()) # 40
分析的没有错,任何一个 zip 对象所占的大小都是 40 字节。所以在计算内存大小的时候,有人会好奇这到底是怎么计算的,其实就是根据底层的结构体进行计算的。
下面看看 zip 对象是如何被实例化的。
static PyObject *
zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
zipobject *lz; // 指向创建的 zip 对象
Py_ssize_t i; // 循环变量
PyObject *ittuple; // 所有可迭代对象的迭代器组成的元组
PyObject *result; // 代码中有体现
Py_ssize_t tuplesize; // 可迭代对象的数量
// zip 同样不需要关键字参数,但是在 3.10 的时候将会提供一个关键字参数 strict
// 如果为 True,表示可迭代对象之间的长度必须相等, 否则报错
// strict 如果为 False,则和目前是等价的,会自动以短的为准
if (type == &PyZip_Type && !_PyArg_NoKeywords("zip", kwds))
return NULL;
assert(PyTuple_Check(args));
// 获取可迭代对象的数量
tuplesize = PyTuple_GET_SIZE(args);
// 申请一个元组,长度为 tuplesize,用于存放可迭代对象对应的迭代器
ittuple = PyTuple_New(tuplesize);
if (ittuple == NULL)
return NULL;
// 然后依次遍历
for (i=0; i < tuplesize; ++i) {
// 获取传递的可迭代对象
PyObject *item = PyTuple_GET_ITEM(args, i);
// 通过 PyObject_GetIter 获取对应的迭代器
PyObject *it = PyObject_GetIter(item);
if (it == NULL) {
Py_DECREF(ittuple);
return NULL;
}
// 设置在 ittuple 中
PyTuple_SET_ITEM(ittuple, i, it);
}
// 这里又申请一个元组 result,长度也为 tuplesize
result = PyTuple_New(tuplesize);
if (result == NULL) {
Py_DECREF(ittuple);
return NULL;
}
// 然后将内部的所有元素都设置为 None
for (i=0 ; i < tuplesize ; i++) {
Py_INCREF(Py_None);
PyTuple_SET_ITEM(result, i, Py_None);
}
// 申请一个 zip 对象
lz = (zipobject *)type->tp_alloc(type, 0);
if (lz == NULL) {
Py_DECREF(ittuple);
Py_DECREF(result);
return NULL;
}
// 初始化字段
lz->ittuple = ittuple;
lz->tuplesize = tuplesize;
lz->result = result;
// 转成泛型指针 PyObject * 之后返回
return (PyObject *)lz;
}
再来看看 zip 对象的定义:
typedef struct {
PyObject_HEAD
Py_ssize_t tuplesize;
PyObject *ittuple;
PyObject *result;
} zipobject;
如果以 zip([1, 2, 3], [11, 22, 33], [111, 222, 333]) 为例的话,那么:
- tuplesize 为 3。
- ittuple 为 ([1, 2, 3].__iter__(), [11, 22, 33].__iter__(), [111, 222, 333].__iter__())。
- result 为 (None, None, None)。
所以目前来说,其它的很好理解,唯独这个 result 让人有点懵,搞不懂它是干什么的(不用想肯定是每次迭代得到的值)。不过既然有这个字段,那就说明它肯定有用武之地,而派上用场的地方显然是在迭代的时候。
static PyObject *
zip_next(zipobject *lz)
{
// 循环变量
Py_ssize_t i;
// 可迭代对象的数量,或者说迭代器的数量
Py_ssize_t tuplesize = lz->tuplesize;
// (None, None, ....)
PyObject *result = lz->result;
// 每一个迭代器
PyObject *it;
// 代码中体现
PyObject *item;
PyObject *olditem;
// 如果 tuplesize == 0, 直接返回
if (tuplesize == 0)
return NULL;
// 如果 result 的引用计数为 1,证明该元组的空间已经被申请了
if (Py_REFCNT(result) == 1) {
// 要作为返回值返回,所以引用计数加 1
Py_INCREF(result);
// 遍历
for (i=0 ; i < tuplesize ; i++) {
// 依次获取每一个迭代器
it = PyTuple_GET_ITEM(lz->ittuple, i);
// 迭代出相应的元素
item = (*Py_TYPE(it)->tp_iternext)(it);
// 如果出现了 NULL,证明迭代结束了,会直接停止
// 所以会以元素最少的可迭代对象(迭代器)为准
if (item == NULL) {
Py_DECREF(result);
return NULL;
}
// 设置在 result 里面
// 但是要先获取 result 中原来的元素,并将其引用计数减 1
// 因为元组不再持有对它的引用
olditem = PyTuple_GET_ITEM(result, i);
PyTuple_SET_ITEM(result, i, item);
Py_DECREF(olditem);
}
if (!_PyObject_GC_IS_TRACKED(result)) {
_PyObject_GC_TRACK(result);
}
} else {
// 否则重新申请一个元组,以下逻辑是类似的
result = PyTuple_New(tuplesize);
if (result == NULL)
return NULL;
for (i=0 ; i < tuplesize ; i++) {
it = PyTuple_GET_ITEM(lz->ittuple, i);
item = (*Py_TYPE(it)->tp_iternext)(it);
if (item == NULL) {
Py_DECREF(result);
return NULL;
}
PyTuple_SET_ITEM(result, i, item);
}
}
// 返回元组 result
return result;
}
因为 zip 对象每次迭代出来的是一个元组,所以 result 是一个元组。如果 zip 接收了 3 个可迭代对象,那么每次迭代出来的元组会包含 3 个元素。
z = zip([1, 2, 3], [11, 22, 33])
print(z.__next__()) # (1, 11)
# 即使只有一个可迭代对象,依旧是一个元组
# 因为底层返回的 result 就是一个元组
z = zip([1, 2, 3])
print(z.__next__()) # (1,)
# 可迭代对象的嵌套也是一样的规律
# 直接把里面的列表看成一个标量即可
z = zip([[1, 2, 3], [11, 22, 33]])
print(z.__next__()) # ([1, 2, 3],)
以上就是 zip 的用法。
map、filter 和列表解析式的区别
其实在使用 map、filter 的时候,我们完全可以使用列表解析来实现。比如:
lst = [1, 2, 3, 4]
print([str(_) for _ in lst]) # ['1', '2', '3', '4']
print(list(map(str, lst))) # ['1', '2', '3', '4']
这两者之间实际上是没有什么太大区别的,都是将 lst 中的元素一个一个迭代出来、然后调用 str 、返回结果。只不过先有的 map、filter,后有的列表解析式,但 map、filter 依旧保留了下来。
如果非要找出区别话,就是列表解析使用的是 Python 的 for 循环,而调用 map 使用的是 C 的 for 循环。从这个角度来说,使用 map 的效率会更高一些。
所以后者的效率稍微更高一些,因为列表解析用的是 Python 的 for 循环,list(map(func, iter)) 用的是 C 的 for 循环。
但是注意:如果是下面这种做法的话,会得到相反的结果。
我们看到 map 变慢了,其实原因很简单,后者多了一层匿名函数的调用,所以速度变慢了。如果列表解析也是函数调用的话:
会发现速度更慢了,当然这种做法完全是吃饱了撑的。之所以说这些,是想说明在同等条件下,list(map) 这种形式是要比列表解析快的。当然在工作中,这两者都可以使用,这点效率上的差别其实不用太在意,如果真的到了需要在乎这点差别的时候,那么你应该考虑的是换一门更有效率的静态语言。
filter 和列表解析之间的差别,也是如此。因为 map 和 filter 用的都是 C 的循环,所以都会比列表解析快一点。
对于过滤含有 1000 个 False 和 1 个 True 的元组,它们的结果都是一样的,但是谁的效率更高呢?首先第一种方式肯定比第二种方式快,因为第二种方式涉及到函数的调用。但是第三种方式,我们知道它在底层会走单独的分支,所以再加上之前的结论,我们认为第三种方式是最快的。
结果也确实是我们分析的这样,另外在底层 None 和 bool 都会走相同的分支,所以这里将 None 换成 bool 也是可以的。虽然 bool 是一个类,但是通过 filter_next 函数我们知道,底层不会进行调用,也是直接使用 PyObject_IsTrue,可以将 None 换成 bool 看看结果如何,应该是差不多的。
当然啦,还是那句话,工作中用哪种方式都可以。
小结
以上我们就从源码的角度介绍了 map、filter、zip 三个内置类,它们在工作中绝对会出现。
并且 map、filter 也可以使用列表解析替代,如果逻辑简单的话,比如获取为真的元素,那么通过 list(filter(None, data)) 实现即可,效率更高,因为它走的是 C 的循环。
但如果执行的逻辑比较复杂,那么对于 map、filter 而言就要写匿名函数。比如获取大于 3 的元素,那么就需要使用 list(filter(lambda x: x > 3, data)) 这种形式了。而我们说它的效率此时是不如列表解析 [x for x in lst if x > 3] 的,因为前者多了一层函数调用。
但是在工作中,这两种方式都是可以的,使用哪一种就看个人喜好。到此我们发现,如果排除那一点点效率上的差异,那么有列表解析式就完全足够了,因为列表解析式可以同时实现 map、filter 的功能,而且表达上也更加地直观。只不过是 map、filter 先出现,然后才有的列表解析式,但是前者依旧被保留了下来。
当然 map、filter 返回的是一个可迭代对象,它不会立即计算,可以节省资源。只不过这个功能,我们也可以通过生成器表达式来实现。
欢迎大家关注我的公众号:古明地觉的编程教室。
如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。