ユーニックス総合研究所

  • home
  • archives
  • python-zip

Pythonのzipの使い方: 複数のイテラブルからイテレーターを作る

  • 作成日: 2020-09-07
  • 更新日: 2024-01-02
  • カテゴリ: Python

Pythonのzip関数

Pythonには組み込み関数のzipがあります。

この関数は複数のリストなどのイテラブルなオブジェクトから、1つのzipオブジェクトを作るときに使われます。
zipオブジェクトはfor文などで回すと、引数の各リストなどの各要素を1つのタプルにしてくれます。
たとえば複数のリストを1つのfor文で回したい時などに有用です。

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog', 'bird']  

for a, b in zip(lis1, lis2):  
    print(a, b)  

実行結果。

1 cat  
2 dog  
3 bird  

zipオブジェクトの作成

組み込み関数のzip()は複数のイテラブルなオブジェクト(リストやタプル、辞書など)から1つのzipオブジェクトを作成します。

zip(*iterables, strict=False)  

zipオブジェクトは遅延評価されるオブジェクトです。
実際にfor文で回されたりnext()でイテレーターを進めたりしない限り、引数iterablesの要素を処理することはありません。

iterablesは複数のイテレーション可能なオブジェクトを指します。
イテレーション可能なオブジェクトとはlisttuplesetdictなどです。
これらの複数のオブジェクトを混合してzip()に渡すことができます。

z = zip([1, 2], (3, 4), {5, 6}, {'a': 1, 'b': 2})  

print(z)  
# <zip object at 0x000001F060102600>  

print(list(z))   
# [(1, 3, 5, 'a'), (2, 4, 6, 'b')]  

zip()strictキーワード引数はバージョン3.10から追加されました。
strictTrueにするとzipオブジェクトをfor文で回したときに特定の条件でValueError例外が送出されるようになります。

try:  
    for item in zip([1, 2], (3, 4, 5), strict=True):  
        print(item)  
except ValueError as e:  
    print(e)  
# (1, 3)  
# (2, 4)  
# zip() argument 2 is longer than argument 1  

特定の条件とはzip()に渡すiterablesの各オブジェクトの要素数が異なる時です。
上記の例では[1, 2](3, 4, 5)を渡していますが、前者は要素数が2で後者は要素数が3になっているのでエラーになります。
このエラーですがfor文などでイテレーターを進めると現れますので、単純にzip objectを作っただけではエラーは現れません。

zip([1, 2], (3, 4, 5), strict=True)  

zipオブジェクトはリストやタプルに変換可能なほか、nextにも渡せるしfor文で回すこともできます。

CPythonのzip_new関数の実装

CPython3.13ではzip()で呼ばれる関数はcpython/Python/bltinmodule.czip_new()関数です。
この関数ではまず最初にキーワード引数のチェックを行っています。
このキーワード引数のチェックでstrict引数の確認を行っています。

それからiterablesの各オブジェクトのイテレーターを取得します。
そしてresult holderと呼ばれる結果を格納するタプルを作成しています。
zipobjectというオブジェクトを作成し、これらのイテレーターやresult holderをオブジェクトに格納しています。

cpython/Python/bltinmodule.c at main · python/cpython · GitHub

zipオブジェクトをリストに変換する場合

zipオブジェクトをリストに変換する場合はlistクラスに渡します。

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog', 'bird']  

it = zip(lis1, lis2)  

print(list(it))  

出力結果。

[(1, 'cat'), (2, 'dog'), (3, 'bird')]  

zipオブジェクトをタプルに変換する場合

zipオブジェクトをタプルに変換する場合はtupleクラスに渡します。

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog', 'bird']  

it = zip(lis1, lis2)  

print(tuple(it))  

出力結果。

((1, 'cat'), (2, 'dog'), (3, 'bird'))  

zipオブジェクトを辞書に変換する場合

zipオブジェクトを辞書に変換する場合はdictクラスに渡します。

names = ['Mike', 'Tama', 32, 23]  
ages = [3, 7, 1, 4]  

it = zip(names, ages)  
name_and_ages = dict(it)  

print(name_and_ages)  

出力結果。

{'Mike': 3, 'Tama': 7, 32: 1, 23: 4}  

nextで要素を取り出す場合

zipオブジェクトをnextに渡すとzipされた要素を順次取得できます。
内部のポインタが最後まで進んだ場合、StopIterationが発生します。

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog', 'bird']  

it = zip(lis1, lis2)  

try:  
    print(next(it))  
    print(next(it))  
    print(next(it))  
    print(next(it))  # <- ここでStopIterationが発生  
except StopIteration:  
    print('stop!')  

出力結果。

(1, 'cat')  
(2, 'dog')  
(3, 'bird')  
stop!  

CPythonのnextの実装

CPytohn3.13におけるnext()の実装はcpython/Python/bltinmodule.czip_next()です。
この関数ではzipオブジェクトのresultメンバ変数に処理の結果を格納しています。
このresultはタプル型のオブジェクトです。

resultの参照カウントが1か、それ以外で分岐しています。
1の場合はzipオブジェクトのresultにそのまま要素を格納していきます。
それ以外の場合はresultの代わりにタプルを新しく作りそれに要素を格納して返しています。

要素の格納処理が終わったらcheckラベルにgoto(ジャンプ)して、エラーチェックしています。
つまり前半で格納処理、後半でエラーチェック、という感じです。

cpython/Python/bltinmodule.c at main · python/cpython · GitHub

複数のイテラブルなオブジェクトをzipする

冒頭のコードですが、zipで複数のリストをまとめられます。

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog', 'bird']  

for a, b in zip(lis1, lis2):  
    print(a, b)  

実行結果。

1 cat  
2 dog  
3 bird  

また、異なるタイプのイテラブルなオブジェクトもまとめることができます。

lis = [1, 2, 3]  
tpl = ['cat', 'dog', 'bird']  
dic = {'a': 1, 'b': 2, 'c': 3}  

for a, b, c in zip(lis, tpl, dic):  
    print(a, b, c)  

実行結果。

1 cat a  
2 dog b  
3 bird c  

イテラブルなオブジェクトの長さが違う場合

長さが違うイテラブルなオブジェクトをzipした場合、zipは長さが短いほうに合わせます。

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog']  

for a, b in zip(lis1, lis2):  
    print(a, b)  
1 cat  
2 dog  

itertools.zip_longestで足りない分の要素を埋める

itertoolsモジュールのzip_longestを使うと、イテラブルなオブジェクト群の長さの長いほうに合わせて要素が生成されます。
デフォルトでは足りない要素はNoneになります。

from itertools import zip_longest  

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog']  # <- こっちが短い  

for a, b in zip_longest(lis1, lis2):  
    print(a, b)  

実行結果。

1 cat  
2 dog  
3 None  

fillvalue引数に、足りない要素を埋めるときの値を指定することができます。

from itertools import zip_longest  

lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog']  

for a, b in zip_longest(lis1, lis2, fillvalue='unknown'):  
    print(a, b)  

実行結果。

1 cat  
2 dog  
3 unknown  

zipしたイテラブルなオブジェクトをもとに戻す

zipオブジェクトに*を使うとzipしたイテラブルなオブジェクトを気持ち元に戻すことができます。


lis1 = [1, 2, 3]  
lis2 = ['cat', 'dog', 'bird']  

a, b = zip(*zip(lis1, lis2))  
print(a)  
print(b)  

出力結果。

(1, 2, 3)  
('cat', 'dog', 'bird')  

リストがタプルになっているので正確には「元に戻ってる」とは言えませんが、支障はないでしょう。

zipの評価タイミング

zipは事前にイテラブルなオブジェクトをすべてまとめることはせず、必要になった時点で要素をまとめます。
たとえばzipに巨大なrangeを渡したとしても、zipは要素を事前作成しないので処理が重くなることはありません。
↓の場合、要素がまとめられるのはnextが呼び出された時点です。

# 巨大なrangeを渡してもすぐ戻り値にアクセスできる  
it = zip(range(100000000), range(100000000))  

print(next(it))  
print(next(it))  
print(next(it))  

実行結果。

(0, 0)  
(1, 1)  
(2, 2)  

もちろん巨大なrangeを使ったzipオブジェクトをlisttupleなどに渡せば一括変換が発生するので処理は重くなります。
処理を軽くしたい場合はnextを使うなどして工夫しましょう。

zipの使用例

zipの使用例です。

文字列のリストと整数のリストから辞書を作る

名前の入ったリストと年齢の入ったリストから、名前をキーにした辞書を生成します。

names = ['Mike', 'Tama', 'Pochi']  
ages = [3, 7, 1]  

it = zip(names, ages)  
d = dict(it)  

for k, v in d.items():  
    print(k, v)  

出力結果。

Mike 3  
Tama 7  
Pochi 1  

問題

Q1: zipの戻り値はなにか答えよ

  1. リスト
  2. 辞書
  3. zipオブジェクト

Q2: zipの評価タイミングとして正しいものを答えよ

  1. zipは事前生成せずに必要になったら要素を生成する
  2. zipはzipが呼ばれた時点ですべての要素を生成する
  3. zipはnextに渡されると事前生成しておいた要素を返す

Q3: zipに異なる長さのイテラブルなオブジェクトを渡したときの挙動を答えよ

  1. 長いほうに合わせる
  2. 短いほうに合わせる
  3. エラーが発生する

正解

Q1: 3
Q2: 1
Q3: 2