zip
函数是Python的内置函数,在拙做《跟老齐学Python:轻松入门》有必定的介绍,可是,考虑到那本书属于Python的入门读物,并无讲太深。可是,并不意味着这个函数不能应用的很深刻,特别是结合迭代器来理解此函数,会让人有豁然开朗的感受。同时,可以巧妙地解决某些问题。html
本文试图对zip
进行深刻探讨,同时兼顾对迭代器的理解。python
看下面的操做。数组
>>> list(zip([1, 2, 3], ['a', 'b', 'c']))
[(1, 'a'), (2, 'b'), (3, 'c')]
复制代码
[1, 2, 3]
和 ['a', 'b', 'c']
是列表,全部列表都是可迭代的。这意味着能够一次返回一个元素。bash
zip函数最终获得一个zip对象——一个迭代器对象。而这个迭代器对象是由若干个元组组成的。函数
那么这些元组是如何生成的呢?测试
对照上面的代码,从开始算起。按照Python中的技术习惯,开始的那个元组是用0来计数的,即第0个。ui
[1,2,3]
)当前指针所指的值,刚开始读取,那就是1;['a', 'b', 'c']
)当前指针所指的值,也是刚刚开始读取,应该是a;因而组成了zip对象的第0个元组(1, 'a')
。(2, 'b')
。请注意上面的叙述,若是把元组中的组成对象来源归纳一句话,那就是:元组中的第i个元素就来自于第i个参数中指针在当前所指的元素对象——请细细品味这句话的含义,后面有大用途。编码
对于zip
函数,上面的过程,貌似“压缩”同样,那么,是否有反过程——解压缩。例如从上面示例的结果中分别恢复出来原来的两个对象。lua
有。通常认为是这这么作:spa
>>> result = zip([1, 2, 3], ["a", "b", "c"])
>>> c, v = zip(*result)
>>> print(c, v)
(1, 2, 3) ('a', 'b', 'c')
复制代码
这是什么原理。
result
应用的是一个迭代器对象,不过为了可以显示的明白,也能够理解为是[(1, 'a'), (2, 'b'), (3, 'c')]
。接下来使用了zip(*result)
,这里的符号*
的做用是收集参数,在函数的参数中,有对此详细阐述(请参阅《跟老齐学Python:轻松入门》)。
>>> def foo(*a): print(a)
...
>>> lst = [(1,2), (3,4)]
>>> foo(*lst)
((1, 2), (3, 4))
>>> foo((1,2), (3,4))
((1, 2), (3, 4))
复制代码
仿照这个示例,就能明晰下面两个操做是等效的。
>>> lst = [(1, 'a'), (2, 'b'), (3, 'c')]
>>> zip(*lst)
<zip object at 0x104c8fb48>
>>> zip((1, 'a'), (2, 'b'), (3, 'c'))
<zip object at 0x104f27308>
复制代码
从返回的对象内存编码也能够看出,两个是一样的对象。
既然如此,咱们就能够经过理解zip((1, 'a'), (2, 'b'), (3, 'c'))
的结果产生过程来理解zip(*lst)
了。而前者生成结果的过程前面已经阐述过了,此处再也不赘述。
原来,所谓的“解压缩”和“压缩”,计算的方法是同样的。豁然开朗。
除了zip
函数,还有一个内置函数iter
,它以可迭代对象为参数,会返回迭代器对象。
>>> iter([1, 2, 3, 4])
<list_iterator object at 0x7fa80af0d898>
复制代码
本质上,iter
函数调用参数的每一个元素,而后借助于__next__
函数返回迭代器对象,并把结果集成到一个元组中。
内置函数map
是另一个返回迭代器对象的函数,它以只有一个参数的函数对象为参数,这个函数每次从可迭代对象中取一个元素。
>>> list(map(len, ['abc', 'de', 'fghi']))
[3, 2, 4]
复制代码
map
函数的执行原理是:用__iter__
函数调用第二个参数,并用__next__
函数返回执行结果。在上面的例子中,len
函数要调用后面的列表中的每一个元素,并返回一个迭代器对象。
既然迭代器是可迭代的,就能够把zip
返回的迭代器对象用到map
函数的参数中了。例如,用下面的方式计算两个列表中对应元素的和。
>>> list(map(sum, zip([1, 2, 3], [4, 5, 6])))
[5, 7, 9]
复制代码
迭代器对象有两个主要功效,一是节省内存,而是提升执行效率。
有一个列表,由一些正整数组成,如[1, 2, 3, 4, 5, 6]
,写一个函数,函数的一个参数n
表示要将列表中几个元素划为一组,假设n=2,则将两个元素为一组,最终返回[(1, 2), (3, 4), (5, 6)]
。
若是用简单的方式,能够这样写此函数:
def naive_grouper(inputs, n):
num_groups = len(inputs) // n
return [tuple(inputs[i*n:(i+1)*n]) for i in range(num_groups)]
复制代码
测试一下,此函数可以如咱们所愿那样工做。
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> naive_grouper(nums, 2)
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
复制代码
可是,在上面的测试中,所传入的列表元素个数是比较小的,若是列表元素个数不少,好比有100万个。这就须要有比较大的内存了,不然没法执行运算。
可是,若是这样执行此程序:
def naive_grouper(inputs, n):
num_groups = len(inputs) // n
return [tuple(inputs[i*n:(i+1)*n]) for i in range(num_groups)]
for _ in naive_grouper(range(100000000), 10):
pass
复制代码
把上面的程序保存为文件naive.py
。能够用下面的指令,测量运行程序时所占用的内存空间和耗费的时长。注意,要确保你本地机器的内存至少5G。
$ time -f "Memory used (kB): %M\nUser time (seconds): %U" python3 naive.py
Memory used (kB): 4551872
User time (seconds): 11.04
复制代码
注意:Ubuntu系统中,你可能要执行 /usr/bin/time
。
把列表或者元素传入naïve_grouper
函数,须要计算机提供4.5GB的内存空间,才能执行range(100000000)
的循环。
若是采用迭代器对象,就会有很大变化了。
def better_grouper(inputs, n):
iters = [iter(inputs)] * n
return zip(*iters)
复制代码
这个简短的函数中,内涵仍是很丰富的。因此咱们要逐行解释。
表达式[iters(inputs)] * n
建立一个迭代器对象,它包含了n个一样的列表对象。
下面以n=2
为例说明。
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> iters = [iter(nums)] * 2
>>> list(id(itr) for itr in iters) # 内存地址是同样的
[139949748267160, 139949748267160]
复制代码
在iters
中的两个迭代器对象是同一个对象——认识到这一点很是重要。
结合前面对zip
的理解,zip(*iters)
和zip(iter(nums), iter(nums))
是同样的。为了可以以更直观的方式进行说明,就能够认为是zip((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
。按照前文所述的zip
工做流程,其计算过程以下:
(1, 2)
。>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(better_grouper(nums, 2))
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
复制代码
上面的函数better_grouper()
的优势在于:
len()
,可以以任何可迭代对象为参数把上述流程保存为文件better.py
。
def better_grouper(inputs, n):
iters = [iter(inputs)] * n
return zip(*iters)
for _ in better_grouper(range(100000000), 10):
pass
复制代码
而后使用 time
在终端执行。
$ time -f "Memory used (kB): %M\nUser time (seconds): %U" python3 better.py
Memory used (kB): 7224
User time (seconds): 2.48
复制代码
对比前面执行 naive.py
,不论在内存仍是执行时间上,都表现很是优秀。
对于上面的better_grouper
函数,深刻分析一下,发现它还有问题。它只能分割可以被列表长度整除的列表中的数字,若是不能整除的话,就会有一些元素被舍弃。例如:
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(better_grouper(nums, 4))
[(1, 2, 3, 4), (5, 6, 7, 8)]
复制代码
若是要4个元素一组,就会有9和10不能分组了。之因此,仍是由于zip
函数。
>>> list(zip([1, 2, 3], ['a', 'b', 'c', 'd']))
[(1, 'a'), (2, 'b'), (3, 'c')]
复制代码
最终获得的zip对象中的元组数量,是参数中长度最小的对象决定。
若是你感受这样作不爽,可使用itertools.zip_longest()
,这个函数是以最长的参数为基准,若是有不足的,默认用None
填充,固然也能够经过参数fillvalue
指定填充对象。
>>> import itertools
>>> x = [1, 2, 3]
>>> y = ["a", "b", "c", "d"]
>>> list(itertools.zip_longest(x, y))
[(1, 'a'), (2, 'b'), (3, 'c'), (None, 'd')]
复制代码
那么,就能够将better_grouper
函数中的zip
用zip_longest()
替代了。
import itertools as it
def grouper(inputs, n, fillvalue=None):
iters = [iter(inputs)] * n
return it.zip_longest(*iters, fillvalue=fillvalue)
复制代码
再跑一下,就是这样的结果了。
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> print(list(grouper(nums, 4)))
[(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, None, None)]
复制代码