Pythonの組み込み関数 zip() は、引数に指定したリストオブジェクトなどのイテラブルオブジェクトから要素を一つづつ取得し、組み合わせてタプルを作成します。
>>> list1 = [1, 2, 3, 4]
>>> list2 = ['a', 'b', 'c', 'd']
>>> list3 = ['あ', 'い', 'う', 'え']
>>> for tp in zip(list1, list2, list3):
... print(tp)
...
(1, 'a', 'あ')
(2, 'b', 'い')
(3, 'c', 'う')
(4, 'd', 'え')
この例では、zip() に 3つのリストオブジェクトを指定し、それぞれから順に取得した要素でタプルを作成しています。
zip()に長さの異なるリストを指定すると、最も短いリストの要素数分、処理を繰り返します。次の例では、list1 には4つの要素がありますが、list2 には2つしか無いので、ループも2回しか実行されません。
>>> list1 = [1, 2, 3, 4]
>>> list2 = ['a', 'b']
>>> for tp in zip(list1, list2):
... print(tp)
...
(1, 'a')
(2, 'b')
strict引数¶
実際のプロジェクトで zip() を利用するときには、引数に指定した全てのイテラブルオブジェクトで要素数は同じ、という前提で処理が行われるのが一般的です。その場合には、要素数が異なる場合はバグとして検出する必要がありますが、すべてのイテラブルの要素数を効率的にチェックするのは意外と面倒で、実際に要素数のチェックが行われることはあまりありませんでした。
そこで、PEP 618, Add Optional Length-Checking To zip では、zip() にあたらしいキーワード専用引数 strict を追加しました。strict = True と指定した場合、引数のイテラブルの要素数が異なるときは、ValueEror 例外が発生します。
>>> list1 = [1, 2, 3, 4]
>>> list2 = ['a', 'b']
>>> for tp in zip(list1, list2, strict=True):
... print(tp)
...
(1, 'a')
(2, 'b')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: zip() argument 2 is shorter than argument 1
itertools.zip_longest()¶
zip() は、引数に指定したイテラブルのうち、一番短いイテラブルの要素数分だけ結果をかえします。では、逆に「一番長いイテラブルの要素数分だけ結果を返してほしい」という場合にはどうすればよいでしょう?
itertools.zip_longest() は、zip() とよく似た処理を行いますが、引数に指定したイテラブルのうち、一番長いイテラブルの要素数分の結果を返します。
>>> from itertools import zip_longest
>>> list1 = [1, 2, 3, 4]
>>> list2 = ['a', 'b', 'c']
>>> list3 = ['あ', 'い']
>>> for tp in zip_longest(list1, list2, list3, fillvalue=None):
... print(tp)
...
(1, 'a', 'あ')
(2, 'b', 'い')
(3, 'c', None)
(4, None, None)
この例では、引数に指定した list1 が要素数4で最も長く、ロープは4回繰り返されます。list2、list3 の要素が使い果たされた後は、代わりに filevalue引数に指定した値 None が使われます。
map()の場合¶
あまり使われている場面を見かけませんが、組み込み関数 map() でも複数のイテラブルを指定して、zip() のように要素を組み合わせるなどの処理を行えます。
>>> list1 = [1, 2, 3, 4]
>>> list2 = ['a', 'b', 'c']
>>> list3 = ['あ', 'い']
>>> def zipoid(l1, l2, l3):
... return (l1, l2, l3)
>>> for tp in map(zipoid, list1, list2, list3):
... print(tp)
(1, 'a', 'あ')
(2, 'b', 'い')
ついでに map() にも strict を追加しても良さそうな感じですが、map() はあんまり複数のイテラブル指定して使わないからいっか…… ということで見送られました。
(まめちしき) map() と zip() の歴史¶
大昔、Python1.x時代にはまだ zip() は存在しませんでした。代わりに、map() の第一引数を None にすると、zip() のようにイテラブルの要素からタプルをつくる仕組みになっていました。
>>> list1 = [1, 2, 3, 4]
>>> list2 = ['a', 'b', 'c']
>>> list3 = ['あ', 'い']
>>> for tp in map(None, list1, list2, list3):
... print(tp)
(1, 'a', 'あ')
(2, 'b', 'い')
(3, 'c', None)
(4, None, None)
この、None を使った書き方はあまりわかりやすくありません。また、Python1~2時代の map() は前出のitertools.zip_longest() と同様、イテラブルから全ての要素を取り出す仕様になっていました。この仕様はこういった用途には使いにくく、PEP 201 -- Lockstep Iteration で現在の zip() が導入されました。
その後、Python3.0でmap()の仕様が変更され、第一引数に None を指定できなくなりました。
また、map() に複数のイテラブルを指定した場合の動作も、zip() と同様に最短のイテラブルの要素数に従うように変更されました。現在のPython3でPython1~2時代の map() と同じ処理を行うには、次のように記述します。
>>> list1 = [1, 2, 3, 4]
>>> list2 = ['a', 'b', 'c']
>>> list3 = ['あ', 'い']
>>> def func(l1, l2, l3):
... return f'{l1}:{l2}:{l3}'
>>> for s in map(lambda tp:func(*tp), zip_longest(list1, list2, list3)):
... print(s)
1:a:あ
2:b:い
3:c:None
4:None:None