ふとアンパック代入ってどうやって実現されているんだろう、と試行錯誤してみた結果をメモしておきます。
__getitem__()
を実装する
>>> class MyTuple:
... def __getitem__(self, key):
... print(key)
... if key < 3:
... return str(key)
... else:
... raise IndexError
...
アンパック代入には __getitem__()
メソッドが利用されるようです 1。
キーとしてインデックス番号が渡されます。
普通の代入の場合
__getitem__()
は呼び出されません。
>>> x = MyTuple()
>>> x
<__main__.MyTuple object at 0x10a6bd850>
要素が少ないアンパック代入の場合
左辺の要素の数 + 1 回分、__getitem__()
が呼び出されます。
いずれの呼び出しでも IndexError
が発生しない場合、右辺の要素が多いとみなし、ValueError
が発生します。
>>> x, y = MyTuple()
0
1
2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)
要素の数が一致するアンパック代入の場合
左辺の要素の数 + 1 回分、__getitem__()
が呼び出されます。
最後の呼び出しで IndexError
が発生した場合、左辺の要素と右辺の要素が一致しているとみなし、代入が成功します。
>>> x, y, z = MyTuple()
0
1
2
3
>>> x, y, z
('0', '1', '2')
要素が多いアンパック代入の場合
左辺の要素の数 + 1 回分、__getitem__()
が呼び出されます。
呼び出しの途中で IndexError
が発生した場合、右辺の要素が不足しているとみなし、代入が失敗します。
>>> v, x, y, z = MyTuple()
0
1
2
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 4, got 3)
*
付きのアンパック代入の場合
左辺に *
付きの要素がある場合、IndexError
が発生するまで __getitem__()
が呼び出されます 2。
その結果を左辺の変数にいい感じに代入します。
>>> a, *b = MyTuple()
0
1
2
3
>>> a, b
('0', ['1', '2'])
いい感じの例
>>> *a, b = MyTuple()
0
1
2
3
>>> a, b
(['0', '1'], '2')
ひとつぐらい要素が足りなくてもいい感じにしてくれます。
>>> *w, x, y, z = MyTuple()
0
1
2
3
>>> w, x, y, z
([], '0', '1', '2')
>>> w, *x, y, z = MyTuple()
0
1
2
3
>>> w, x, y, z
('0', [], '1', '2')
>>> w, x, *y, z = MyTuple()
0
1
2
3
>>> w, x, y, z
('0', '1', [], '2')
>>> w, x, y, *z = MyTuple()
0
1
2
3
>>> w, x, y, z
('0', '1', '2', [])
でも、ふたつ足りない場合はダメです。
>>> *v, w, x, y, z = MyTuple()
0
1
2
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected at least 4, got 3)
__iter__()
を実装する
__getitem__()
の代わりに __iter__()
を実装してもアンパック代入できます。
>>> class MyTuple2():
... def __iter__(self):
... return iter([1, 2, 3])
...
実装はかなり端折っていますが、3 つの要素を持つ配列のイテレータを返しています。
ちゃんと動きます。
>>> x, y, z = MyTuple2()
>>> x, y, z
(1, 2, 3)
まとめ
__getitem__()
でも __iter__()
のどちらでもよいということはつまり、iterable であればアンパック代入可能と言い換えることができますね。
反復可能オブジェクトの例には、(list, str, tuple といった) 全てのシーケンス型や、 dict や ファイルオブジェクト といった幾つかの非シーケンス型、 あるいは Sequence 意味論を実装した
__iter__()
メソッドか__getitem__()
メソッドを持つ任意のクラスのインスタンスが含まれます。
-
ドキュメントを探してみたのだけど、ぱっと出てこなかったので実験して調べました。 ↩
-
いい感じ is PEP 3132 -- Extended Iterable Unpacking です。 ↩