在程序开发中,map、filter、zip 可以说是非常常见了,下面来从源码的角度分析一下它们的实现原理。首先需要说明的是,这几个不是函数,而是类。 map 是将一个序列中的每个元素都作用于同一个函数(类、方法也可以),当然,我们知道调用 map 的时候并没有马上执行,而是返回一个 map 对象。既然是对象,那么底层必有相关的定义。
//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 不仅可以作用于一个序列,还可以作用于任意多个序列。 m1 = map( lambda x: x[0] + x[1] + x[2], [(1, 2, 3), (4, 5, 6), (7, 8, 9)] ) # x 就是列表里面的每一个元组 # x => (1, 2, 3) # x => (4, 5, 6) # x => (7, 8, 9) print(list(m1)) # [6, 15, 24]
# map 还可以接收任意多个可迭代对象 m2 = map( lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9] ) # (x, y, z) => (1, 4, 7) # (x, y, z) => (2, 5, 8) # (x, y, z) => (3, 6, 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__ 方法。 //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 里面,上面的 kwds 是关键字参数 //这里获取位置参数的个数,1个函数、numargs - 1个可迭代对象 //而 args 是一个 PyTupleObject * numargs = PyTuple_Size(args); // 如果参数个数小于2 if (numargs < 2) { //抛出 TypeError,表示 map 至少接收两个位置参数 //一个函数 和 至少一个可迭代对象 PyErr_SetString(PyExc_TypeError, "map() must have at least two arguments."); return NULL; } // 申请一个元组, 容量为 numargs - 1 //用于存放传递的所有可迭代对象对应的迭代器 iters = PyTuple_New(numargs-1); // 为NULL表示申请失败 if (iters == NULL) return NULL; // 依次循环 for (i=1 ; i<numargs ; i++) { //表示获取索引为 i 的可迭代对象 //然后拿到对应的迭代器 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; } //让 lz 的 iters 字段等于 iters lz->iters = iters; //获取第一个参数, 也就是函数 func = PyTuple_GET_ITEM(args, 0); //增加引用计数,因为该函数被作为参数传递给 map 了 Py_INCREF(func); //让 lz 的 func 字段等于 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 0x00000167F4552E80> print(m.func) # <function <lambda> at 0x0000023ABC4C51F0> print(m.func(2, 3)) # 5
print( m.iters ) # [<list_iterator object at 0x00...>, <list_iterator object at 0x00...>] 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; //函数调用的返回值 PyObject *result = NULL; //获取当前的线程状态对象 PyThreadState *tstate = _PyThreadState_GET(); //获取 iters 的长度,也就是迭代器的数量 //当然同时也是调用函数时的参数数量 const Py_ssize_t 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(tstate); return NULL; } } //走到这里说明一切顺利,那么下面就开始迭代了 Py_ssize_t nargs = 0; //依次遍历,得到每一个迭代器 for (Py_ssize_t 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 这个label中 if (val == NULL) { goto exit; } //将 val 设置在数组索引为i的位置中,然后进行下一轮循环 //也就是获取下一个迭代器中的元素 stack[i] = val; //nargs++,和参数个数(迭代器个数)保持一致 //如果可迭代对象个数小于5,比如3,那么stack会申请在栈区 //但是在栈区申请的话,长度默认为5,因此后两个是元素是无效的 //而在调用的时候需要指定有效的参数个数 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] */ //进行调用,tstate 是线程状态对象 //lz -> func 就是函数,stack 指向函数的首个参数 //nargs 就是参数个数 result = _PyObject_FastCall(tstate, lz->func, stack, nargs, NULL);
exit: //调用完毕之后,将stack里面指针指向的对象的引用计数减1 for (Py_ssize_t i=0; i < nargs; i++) { Py_DECREF(stack[i]); } //不相等的话,说明该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 的实现原理,看完了 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是否为真 //另外我们在写 if 语句的时候经常会写 if item: 这种形式 //但是很少会写 if bool(item): //因为这两者底层都是执行了 PyObject_IsTrue //但是对于 if 而言,bool(item)多了一次调用,速度稍微慢一些 ok = PyObject_IsTrue(item); } else { //否则的话, 会调用我们传递的func //这里的 good 就是函数调用的返回值 PyObject *good; // 调用函数, 将返回值赋值给good 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,说明将 lz -> func(item)的结果为真 //那么将 item 返回 if (ok > 0) return item; //同时减少其引用计数 Py_DECREF(item); //小于0的话,表示PyObject_IsTrue调用失败了,返回-1 //但我们使用 Python 时不会发生,除非解释器本身处 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 的底层实现同样很简单,我们来看一下: 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) { //zip 对象的指针 zipobject *lz; //循环变量 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); //为NULL表示申请失败 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) { // 为NULL表示获取失败,减少ittuple的引用计数,返回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 //Py_None就是Python中的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); //申请失败减少引用计数, 返回NULL 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); } } else { // 否则的话同样的逻辑, 只不过需要自己重新手动申请一个tuple 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],)
其实在使用 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, lst)) 实现即可,效率更高,因为它走的是相当于是 C 的循环。 但如果执行的逻辑比较复杂的话,那么对于 map、filter 而言就要写匿名函数。比如获取大于 3 的元素,那么就需要使用 list(filter(lambda x: x > 3, lst)) 这种形式了。而我们说它的效率此时是不如列表解析 [x for x in lst if x > 3] 的,因为前者多了一层函数调用。 但是在工作中,这两种方式都是可以的,使用哪一种就看个人喜好。到此我们发现,如果排除那一点点效率上的差异,那么确实有列表解析式就完全足够了,因为列表解析式可以同时实现 map、filter 的功能,而且表达上也更加地直观。只不过是 map、filter 先出现,然后才有的列表解析式,但是前者依旧被保留了下来。 当然 map、filter 返回的是一个可迭代对象,它不会立即计算,可以节省资源;不过这个功能,我们也可以通过生成器表达式来实现。
|