Last Updated: 2/6/2024, 5:44:57 AM

# イテラブル, iterable ってなに?

for 文で繰り返せる
オブジェクト
またはそのクラス

# 1. 具体的に言えば...

例えば range, リスト, タプル, 集合, 辞書, 文字列は、イテラブルです。 もう少しちゃんと言えば、for 文の in に書き込めるオブジェクトです。

以下のコードは、コピペしてエラーを起こすことなく実行できます。 文字列も for 文で回せるイテラブルだったのは驚きでした。

# range
for e in range(3):
    print(e)

# リスト
for e in [0, 1, 2]:
    print(e)

# タプル
for e in (0, 1, 2):
    print(e)

# 集合
for e in {0, 1, 2}:
    print(e)

# 辞書
for e in {0:'a', 1:'b', 2:'c'}:
    print(e)

# 文字列
for e in '012':
    print(e)

イテラブルは for ループの中で ... で使われます。
Iterables can be used in a for loop ...
iterable - Glossary (opens new window)

# 2. 細かく言えば...

for 文で繰り返せるオブジェクトとはなんでしょうか? 次のいずれかに該当するオブジェクトが iterable です。

  1. __iter__ メソッドを定義したオブジェクト、または
  2. __getitem__ メソッドをシーケンスとして定義したオブジェクト

あるいはユーザが __iter__ メソッドもしくはシーケンスの動作をする __getitem__ メソッドを実装した全てのクラスです
, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements Sequence semantics.
iterable - Glossary (opens new window)

WARNING

細かいことなので知らなくても、困ることはほとんどありません。 なので、読まなくても大丈夫です。

上には小難しいことが書かれていますが、小難しいことが書かれている方が正確という訳ではありません。 なので、読まなくても大丈夫です。

「iterable が for 文で繰り返せるオブジェクトまたはそのクラス」と言うことと、完全に同じことを意味しています。 なので、読まなくても大丈夫です。

# 2.1. __iter__ メソッドを定義したオブジェクト

例えばこのように書いた時

#
# 対話モード >>> に
# コピペで動きます。
#
イテラブル = [0, 1, 2]
for 要素 in イテラブル:
    print(要素)

内部ではこのように動いています。

#
# 対話モード >>> に
# コピペで動きます。
#
イテラブル = [0, 1, 2]
イテレータ = イテラブル.__iter__()
while True:
    try:
        要素 = イテレータ.__next__()
    except StopIteration:
        break
    print(要素)

for 文は内部では  イテラブル  から  イテレータ  を取り出し、 繰り返し処理を行なっています。

__iter__ はイテレータを返すように定められています。

container.__iter__() (opens new window)
イテレータオブジェクトを返します。

イテレータについては以下の記事で、ご紹介させていただきました。 イテレータとは何かは、地味に結構重たいです。

# 2.2. __getitem__ メソッドをシーケンスとして定義したオブジェクト

例えば以下のようなオブジェクトです。

#
# 対話モード >>> に
# コピペで動きます。
#
class Sequence:
    def __getitem__(self, index):
        if 0 <= index <= 3:
            return index * 2
        else:
           raise IndexError

sequence = Sequence()

sequence[0]  # 0
sequence[1]  # 2
sequence[2]  # 4
sequence[3]  # 6


for i in sequence:
    i
>>> for i in sequence:
...     i
... 
0
2
4
6
>>> 

シーケンス (opens new window) とは sequence[0], sequence[1] と数字で要素を参照できるオブジェクト、またはそのようなオブジェクトを生成する型を指します。 組み込み型 (opens new window) では list, tuple, str が該当します。import しなくても使える型のことを組み込み型と言います。

# __getitem__ メソッド

__getitem__ メソッドを定義すると添字表記 obj[key] ができるようになります。 __getitem__ は 'a', 'b', 'c' などの int 以外のオブジェクトも受け取れるので、 そのような場合には for 文で受け取れず iterable ではありません。

# 2.3. 公式の用語集

以下に引用します。

iterable (opens new window)
一度に一つずつ、自分が持つ要素を返すことができるオブジェクトです。iterable の例には、次の型に属するオブジェクトが含まれます。 まず list, str, tuple などの全てのシーケンス型や、また dict, file object などのシーケンスでない型、 あるいはユーザが __iter__ メソッドもしくはシーケンスの動作をする __getitem__ メソッドを実装した全てのクラスです
An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements Sequence semantics.

iterable は for ループの中で、 また他の場所ではシーケンスが必要とされる場所(zip, map 関数の引数として...)で使われます。 iterable なオブジェクトが、組み込み関数 iter に実引数として渡された時、 iter 関数はそのオブジェクトに対するイテレータを返します。 このイテレータは、iterable が持つ値の集合を1つ1つ辿る処理に適しています。 iterable を使う時、必ずしも iter 関数を呼び出したり、 もしくはイテレータオブジェクトそのものを取り扱う必要はありません。 for 文は、プログラマのために自動的にそう言ったことを実行してくれます、 for ループの間、イテレータを保持するための名前のない一時変数を生成します。 iterator, sequence そして generator の項も参照してください。
Iterables can be used in a for loop and in many other places where a sequence is needed (zip(), map(), …). When an iterable object is passed as an argument to the built-in function iter(), it returns an iterator for the object. This iterator is good for one pass over the set of values. When using iterables, it is usually not necessary to call iter() or deal with iterator objects yourself. The for statement does that automatically for you, creating a temporary unnamed variable to hold the iterator for the duration of the loop. See also iterator, sequence, and generator.

文中で「型」と「クラス」で表記が揺れています。これはなぜでしょうか? 何故なら、昔の Python は組み込み型を type, ユーザ定義クラスを class と表現していたためです。

いまは組み込み型、ユーザ定義クラス共に、 型 type と呼んでも良いし、クラス class と呼んでもよくなっています。 ややこしいですね。

# 3. イテラブルを判定したい

「イテラブルを判定する」と小難しく書きましたが、話はそこまで複雑ではありません。

__iter__ メソッドを実装してたら、 イテラブルって言っていいですよね。 でも __getitem__ が定義されてたら、 イテラブルとは必ずしも言えないですよね。 なぜなら  添え字には数字 int だけじゃなくて文字 str が入ってくるかもしれないから 

という話を延々としています。 どこで必要になるかというと型ヒントを書きながら、ダックタイピングがしたいな、 と思った時に必要になる知識です。 具体的には、標準ライブラリ typing の Protocol を使うときに必要になります。

Protocol は、重箱の隅をつついた機能ですし、 さらには「使わなくて済むなら使わない方が良いよ」っていうくらいの 知らなくても大丈夫な機能だと思っています。

# 3.1. イテラブルをテストする。

container が iterable であるかどうかをテストするコードです。 とにかく for 文が動けば問題なく iterable であると言えます。

def test_iterable(container):
    try:
        for container:
            pass
    except:
        # 例外が投げられれば
        # iterable ではない。
        return False
    else:
        # とにかく for 文が動けば
        # iterable である。
        return True

# 3.2. イテラブルを型検査する。

container が iterable であるかどうかを静的(コードを実行せず)に型検査するコードです。

def is_iterable(container):
    if hasattr(container, '__iter__'):
        return True
    elif hasattr(container, '__getitem__'):
        raise Exception  # <--- わからないから例外を投げる。
    else:
        return False

# 3.3. どういうこと?

後半の isiterable 関数では container が __getitem__ 属性を持っていた場合、 例外を投げています。 これはどういうことでしょうか?

iterable であるかどうかテストすることはできますが、 iterable であるかどうか静的に(コードを実行せずに)型検査することはできないということです。

この後 Python の生みの親である Guido van Rossum のメールを引用しつつ、 確認していきます。

# 4. 根拠はどこに?

# 4.1. __iter__ があれば iterable と言っていいの?

答え: 言っていいです。

Python では __iter__ は全てのオブジェクトが iterator を返す様にマニュアルで定められているからです。

container.__iter__() (opens new window)
イテレータオブジェクトを返します。

前後左右に2つのアンダースコアで挟まれた変数、または属性は __*__ 、マニュアルで記載された以外の用途で使ってはならないことになっています。

例えば __init__ メソッドを初期化以外の別の用途で使ったら、大変なことになってしまいます。

このドキュメントで明記されている用法に従わない、 あらゆる __*__ の名前は、いかなる文脈における利用でも、警告無く損害を引き起こすことがあります。
2.3.2. 予約済みの識別子 - Python 言語リファレンス (opens new window)

# 4.2. __getitem__ があると判定できないの?

答え: 難しいと思います。

シーケンスかマッピングか区別することができません。 シーケンスは seq[0], seq[1] と数字で参照できるもの。例えば list, tuple, str がそれに当たります。 マッピングは mpg['a'], mpg['b'] と数字以外でも参照できるもの。例えば dict がそれに当たります。

Guido もできないと言っています。そのため例外 Exception を投げるコードを書きました。

しかし、もし、クラスインスタンスなら、最善の方法は __getitem__ を定義しているかどうかを確認し、オブジェクトが辞書でないことを望むしかありません!
but if it is a class instance, the best you can do is check whether it defines __getitem__ and hope it isn't a dictionary!
なぜイテレータは __iter__ メソッドを持たないといけないのか (opens new window)
Why must an iterator have an __iter__ method?

クラスインスタンス (class instance)
クラスインスタンスは、クラスオブジェクト (上記参照) を呼び出して生成します。 クラスインスタンスは辞書で実装された名前空間を持っており、属性参照の時にはまずこの辞書が探索されます。
3.2. 標準型の階層 - Python 言語リファレンス (opens new window)

# 5. iterator.__iter__

なんで iterator にも自分自身を返すメソッド __iter__ を実装するのでしょうか?
答え: iterator であるかどうかを判定するために、実装します。

でも __next__ メソッドの有無さえ確認すれば iterator かどうかの判定ができるのではないでしょうか? これに対する答えは next メソッドを実装しているだけでは、イテレータであるかどうかを判定するのに不十分だからです。

これは Python 2 の話に戻ります。昔は __next__ メソッドではなく、next メソッドを実装していました。

# Python 2
class Reverse(object):
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    
    def __iter__(self):
        return self
    
    def next(self):  # <- Python 2 では __next__ ではない
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

for c in Reverse('nihao'):
    print(c)

# o
# a
# h
# i
# n



そんな囲われていない next メソッドが実装されているかどうかだけで、イテレータかどうか判定しようとすると、 プログラマが next を別の用途で実装していた場合と区別がつきません。以下の文章は Guido のメールからの抜粋です。

なぜイテレータは __iter__ メソッドを持たないといけないのか (opens new window)
Why must an iterator have an __iter__ method? (fwd)

いま、質問の本題に戻りましょう。なぜ iterator オブジェクトは (iterator であるかどうかを判定されるために)<iter> を実装しなければならないのでしょうか? (<next> の有無だけで判定できないのでしょうか?) 私のこれに対する理由は、"for item in iterator" と書いたときに、 for-loop の実装が、私が会議の討論会 (BOF) で 型を盗み見る (type sniffing) と呼んだものを実行しなくて良いようにするためです。 for-loop は単純に <iter> を実行して、iterator 自身を返します、 そしたら for-loop はハッピーですよね。もし、オブジェクトが <iter> を実装していなければ、for-loop は、そのオブジェクトを受けつけません。
Now let's go back to the question in the subject: why must iterator objects implement <iter>? My reason for this was so that when we write "for item in iterator", the for-loop implementation doesn't have to do what I called "type sniffing" in the BOF at the conference. It simply invokes <iter> on the iterator, which returns the iterator itself, and the for-loop is happy. If the object doesn't implement <iter>, it's not acceptable input for a for-loop.

上記、別の提案では、(イテレータであるかどうかを判定するために)異なる方法を提案しています。
The alternative proposal above suggests a different approach:

  1. look for <next>
  2. look for <iter>
  3. look for <getitem>

私は、このやり方は特定の場合においてのみ理にかなっていると思っています。 もし確実に <next> を判定できる場合、言い換えるなら、 もし確実にあるオブジェクトをイテレータであるかどうか区別できる場合ような場合です。
I think this could only done reasonably, if we can reliably check for <next>, in other words, if we can reliably tell if something is an iterator.

しかし、これは Python においては一般的な問題です。 例えば、どうやって CPython のコードから、あるオブジェクトがシーケンスであるかどうかを判定しますか? オブジェクトがクラスインスタンスでない場合、tp_getitem が定義されているかどうかを調べるでしょう。 しかし、もし、クラスインスタンスなら、最善の方法は __getitem__ を定義しているかどうかを確認し、オブジェクトが辞書でないことを望むしかありません!
But this is a general problem in Python! How do you check (from Ccode) if something is a sequence? If it's not a class instance, you check whether it defines tp_getitem; but if it is a class instance, the best you can do is check whether it defines __getitem__ and hope it isn't a dictionary!

(ワイの注釈: ここで言っている "クラスインスタンス" とは、 ユーザが定義したインスタンスオブジェクトを指しています。 "クラスインスタンスでない" というのは、組み込み型のインスタンスオブジェクトを指しています。 昔の Python は、ユーザが定義したものをクラス class, 組込型を type と言って区別していました。 組み込み型の場合、CPython の mapping, sequence プロトコルのうちどちらの tp_getitem という 構造体のフィールドに関数ポインタが代入されているかだけで、mapping か sequence かを判定できます。 しかし、ユーザが定義したクラスの場合は __getitem__ メソッドを実装している オブジェクトが辞書かシーケンスかを判定することができません。)

言い換えるなら、オブジェクトが特定のプロトコルを実装しているかどうかをテストするのは、難しいし曖昧です。プロトコルを使うことは簡単です。
In other words, testing whether an object implements a particular protocol is hard, or ill-defined. Using a protocol is easy.

type sniffing - ワイの注釈
元の資料がないので、正確なことはわからないのですが、おそらく型の判定, ここでは iterable であるかどうかの判定だと思われます。 どの型かを知りたいなら type 関数を使えばわかります。 では type sniffing 型を盗み見る とは、何を盗み見ているのでしょうか? iterator, sequence, mapping などの 構造型 を判定することを指していると思われます。 構造型については後述します。

BOF(birds of feather) - alc (opens new window)
同じ興味を持つ人たちの集まり、特定のテーマの自由討論会 ◆特にIT関連のフォーラムなどで、特定のテーマに関連や関心のある人が集まり、 自由に議論したり情報交換したりする場を指す。会議のプログラムとして予定されているものもあれば、非公式のものもある。

birds of a feather flock together - weblio (opens new window)
《諺》 同じ羽毛の鳥は相寄る、「類は友を呼ぶ」、類は友を呼ぶ

メールにはまだ続きがあり、このあとは <next> か <iter> のどちらかひとつだけを実装していれば、 iterable だって判定させる実装よりは、<iter> が実装されていればイテレータだと判断する方が、簡単、みたいなそんな文章が続いています。

アンダースコアがないと、普通のユーザが定義したメソッドとプロトコルのメソッドの区別が、つかないやろって話は len 関数の時にも同じような話をしていました。

# 6. PEP 3114

# 6.1. next から __next__ へ

最初は、呼び出すときも iterator.next() として呼び出していました。Python 2.6 から next(iterator) という書き方が登場します。 Python 3 になってから __next__ メソッドが、登場します。

next() (.next()) はよく使われる関数ですが、以下は言及する価値のある構文の変化(そして実装の変化)です。 Python 2.7.5 で関数とメソッドの構文を使えるところでは、Python 3 では > next() 関数しか残っていません。(.next() メソッドを呼ぶと AttributeError になります)
next() 関数 と .next() メソッド - Python 2.7.x と 3.x の決定的な違いを例とともに (opens new window)

一体何が価値ある構文の変化なのかというと、特殊メソッド (opens new window) は、 全て二重のアンダースコアで囲われます __method__。 Python 2 では next だけ囲われていませんでした。命名規則に一貫性を持たせることになりました。

# 6.2. Python 3 では不要になったんじゃない?

答え: わからない。ちゃんと実装されてます。

Python 2 では next メソッドを定義していたのが Python 3 では __next__ メソッドで定義するようになったので、 iterator 本体に __iter__ メソッドは必要ないんじゃないかなと思いました。

しかし、組み込み型である list の iterator の list_iterator, dict の iterator の dict_keyiterator, str の iterator の str_iterator は、 ちゃんと __iter__ メソッドを実装しています。

#
# 1) list_iterator
#
type(iter([]))
# <class 'list_iterator'>
hasattr(iter([]), '__iter__')
# True

#
# 2) dict_keyiterator
#
type(iter({}))
# <class 'dict_keyiterator'>
hasattr(iter({}), '__iter__')
# True

#
# 3) str_iterator
#
type(iter(''))
# <class 'str_iterator'>
hasattr(iter(''), '__iter__')
# True

Python 2 から 3 に進化はしたけど、修正するのが面倒で、尾てい骨のように残っているのかなと思ったりします。 この __iter__ は、Python のインタープリタ以外で、使いどころはあるのでしょうか?

例えば Effective Python (opens new window) の「項目17: 引数に対してイテレータを使うときには確実さを尊ぶ」で コンテナとイテレータを区別する際に、この __iter__ を活用する書き方を紹介しています。

個人的に思うのは Python でイテレータを直接 for 文で受けられるようにしたのは、 間違いだったのかなと思ったりします。

コンテナではなくイテレータを直接 for 文で受けるとイテレータは空になります。 ネットを彷徨ってると、このイテレータが空になる動作に引っかかっている人を、何回か見かけました。

またイテレータだけでなくシーケンスも直接 for 文で受けられるようにしなくても、 良かったのかなと思ったりします。 これによってイテラブルの説明が結構煩雑になります。

説明が難しいのなら、その実装は良くないということ
If the implementation is hard to explain, it's a bad idea.
PEP 20 - LIFE WITH PYTHON (opens new window)

# 7. 基本型と派生型

親クラスのことを基本型と言います。 子クラスのことを派生型と言います。 また、派生型には2種類あります。公称型と構造型です(いずれも Wikipedia 調べ)。

# 1. 基本形 ... 親クラス
class Iterator:
    def __iter__(self):
        raise NotImplementedError
    
    def __next__(self):
        raise NotImplementedError

# 2. 派生型 ... 子クラス
# 2.1. 公称型 ... 明示的に親クラスを継承した子クラス
class IteratorA(Iterator):
    def __iter__(self):
        return self
    
    def __next__(self):
        raise StopIteration

# 2.2. 構造型 ... 明示的に親クラスを継承していないが、
#                 親クラスが持つメソッドを実装した子クラス
class IteratorB:
    def __iter__(self):
        return self
    
    def __next__(self):
        raise StopIteration

コンピュータサイエンスにおいて、データ型 S が他のデータ型 T と is-a 関係にあるとき、 S をT の 派生型(はせいがた、subtype)であるという。またT はS の 基本型(きほんがた、supertype)であるという。 ... 型理論の研究者は、派生型であると宣言されたもののみを派生型とする nominal subtyping(nominative; 公称型)と、 2 つの型の構造によって派生型関係にあるかが決まる structural subtyping(structural; 構造型)を区別する。
派生型 - Wikipedia (opens new window)

# 7.1. イテレータ

いままで見てきたジェネレータイテレータ, map, filter から インスタンス化されたインスタンスオブジェクトは、イテレータです。

from typing import Iterator

g = (i for i in range(0))
m = map(lambda x: x**2, range(0))
f = filter(lambda x: x**2, range(0))

isinstance(g, Iterator)  # True
isinstance(m, Iterator)  # True
isinstance(f, Iterator)  # True

イテレータとは __iter__ メソッドと __next__ メソッドを持っているインスタンスオブジェクトです。

iterator - 用語集 (opens new window)
(イテレータ) データの流れを表現するオブジェクトです。 イテレータの __next__() メソッドを繰り返し呼び出す (または組み込み関数 next() に渡す) と、 流れの中の要素を一つずつ返します。データがなくなると、 代わりに StopIteration 例外を送出します。 その時点で、イテレータオブジェクトは尽きており、 それ以降は __next__() を何度呼んでも StopIteration を送出します。 イテレータは、そのイテレータオブジェクト自体を返す __iter__() メソッドを実装しなければならないので、 イテレータは他の iterable を受理するほとんどの場所で利用できます。 はっきりとした例外は複数の反復を行うようなコードです。 (list のような) コンテナオブジェクトは、 自身を iter() 関数にオブジェクトに渡したり for ループ内で使うたびに、 新たな未使用のイテレータを生成します。 これをイテレータで行おうとすると、 前回のイテレーションで使用済みの同じイテレータオブジェクトを単純に返すため、 空のコンテナのようになってしまします。 詳細な情報は イテレータ型 (opens new window) にあります。

# 7.2. イテラブル

次のいずれかに該当するオブジェクトが iterable です。

  1. __iter__ メソッドを定義したオブジェクト、または
  2. __getitem__ メソッドをシーケンスとして定義したオブジェクト

あるいはユーザが __iter__ メソッドもしくはシーケンスの動作をする __getitem__ メソッドを実装した全てのクラスです
, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements Sequence semantics.
iterable - Glossary (opens new window)

# 7.3. シーケンスとマッピング

Python ではたとえ継承していなくても、クラスが特定のメソッドを "全て正しく" 実装していれば、 そのクラスは sequence だ、mapping だと言えます。 sequence, mapping が実装するべきメソッドの一覧は以下を参照してください。

Python の公式マニュアルでは実体を持たないのにシーケンス、マッピングがそれぞれ実体のある基本型として紹介されているように見えました。 最初にこれを読んだ時、一体、どこにマッピング型があるのだろうか?と全く理解できませんでしたし、探し回っていました。 そしてわからないまま、とても長い年月が過ぎ去って行きました。

基本的なシーケンス型は 3 つあります: リスト、タプル、range オブジェクトです。
4.6. シーケンス型, list, tuple, range - Python 標準ライブラリ (opens new window)

マッピング (mapping) オブジェクトは、ハッシュ可能 (hashable) な値を任意のオブジェクトに対応付けます。 ... 現在、標準マッピング型は辞書 (dictionary) だけです。
4.10. マッピング型, dict - Python 標準ライブラリ (opens new window)

このようにしてメソッドの集まりから類推的に決定される型を サブクラス subclass または派生型 subtype と表現されているのを目にします。 またサブクラス subclass や派生型 subtype が備えるべきメソッドの集まりを プロトコル protocol と呼んでいるのを目にします。 プロトコル protocol は Java でいうところのインターフェイス interface に相当するかと思います。

# 8. 構造的派生型

型ヒント, 標準ライブラリ typing, mypy を使うことで静的型検査を行えるようになりました。 標準ライブラリ typing の Protocol について見ていきます。

# 8.1. 昔の mypy

例えば for 文は Iterable の子クラスしか実行できないはずです。 昔の mypy はそれを検知していました。 昔の mypy をインストールして動作を確認して見たいと思います。

#
# structural.py
#
class Foo(object):
    def __iter__(self):
        yield 1
        yield 2

for x in Foo():
    print(x)

pip install mypy==0.540
mypy -V
mypy structural.py
$ mypy -V
mypy 0.540
$ mypy structural.py 
structural.py:6: error: Iterable expected
$
$ # Foo() が Iterable ではないと
$ # mypy から弾き返されています。

Iterable ではないとはどういうことでしょうか? どのようにすればいいのかと言うと typing.Iterable を継承する必要がありました。 このように明示的に型を指定し、それを元に判定することを nominal subtyping と PEP 544 では表現されています。

#
# nominal.py
#
from typing import Iterator, Iterable

class Foo(Iterable):
    def __iter__(self) -> Iterator[int]:
        yield 1
        yield 2

for x in Foo():
    print(x)
mypy -V
mypy nominal.py 
$ mypy -V
mypy 0.540
$ mypy nominal.py 
$ 
$ # 指定してあげると何も起こりません。

# 補足: len 関数

ちなみに len 関数についても弾かれます。

#
# structural_len.py
#
class Item(object):
    def __len__(self):
      return 1

print(len(Item()))
# 1
pip install mypy==0.540
mypy -V
mypy structural_len.py
$ mypy structural_len.py 
structural_len.py:6: error: Argument 1 to "len" has incompatible type "Item"; expected "Sized"
$

このエラーを解消するにはどうすればいいでしょうか? typing.Sized を使います。

#
# nominal_len.py
#
from typing import Sized
class Item(Sized):
    def __len__(self):
      return 1

print(len(Item()))
# 1
$ mypy nominal_len.py
$

# 8.2. 今の mypy

確かに for 文で回せないオブジェクトが in にはいっていたら、 型チェックで弾き返して欲しいです。 しかし、これは酷く面倒ではないでしょうか? 今の mypy では、組み込み型については structural subtyping ができるようになっています。

pip install --upgrade mypy
mypy -V
mypy structural.py
$ mypy -V
mypy 0.720
$ mypy structural.py
$ 
$ # いまの mypy では、何も起こりません。


ちなみに、この機能 statick duck typing については PEP 544 にて議論されました。

# 8.3. duck typing

Python では __iter__ メソッドさえ定義されていれば Iterable クラスだと言えます。 反対に Java では明示的にクラスを継承していないと、その型として実行することができません。

Python では、例えば Iterable というクラスが継承されていなくても、 同じ名前のメソッドさえ定義されていれば、Iterable というクラスとして振舞うことができます。 このようにして名前さえ同じならコードが問題なく動いてくれることを duck typing (opens new window) と呼ばれたりしているのを目にします。

そいつが Iterable というクラスを継承していなくても(duck という クラスが継承されていなくても)、 そいつが Iterable のように振る舞うなら(duck のように鳴き、よちよち歩くなら)、 そいつは Iterable だ!(duck だ!)という意味合いだそうです。

static typing が静的型付け, dynamic typing が動的型付けと訳されるなら、 duck typing は鴨的型付けって訳になるんですかね..。

# 8.4. statick duck typing - 静的鴨的型付け

型ヒントを書く世界でも statick duck typing がしたいです。 どうやって書けばいいのでしょうか? やり方は以下の記事で記述させていただきました。

しかし静的鴨的型付けの静的とは何でしょうか? これはおそらく事前に Protocol を継承したクラスを静的に言い換えれば事前に定義することを指していると思われます。

また、あまりちゃんと読んでいないのですが、正確に structural subtyping, メソッドの名前だけで型の判定ができるのか、 というのができるのかなという疑問があります。本当に重箱の隅をつつくようなことですが。

例えば、イテレータクラスを実装する際に next から __next__ に変わりました。 ユーザ定義の next とイテレータとしての __next__ の判別がつかないからです。

__next__ メソッドが定義されていれば、名前だけでイテレータだと判定できます。 同じことは __next__ メソッドについても言えて __iter__ があればイテラブルだと言えます。

名前に対してメソッドの動作が定義されている時にはいいのですが、 名前が同じだけでメソッドの動作も同じと定義されているのは、 例えば __init__ などの二重のアンダーバー _ で囲われいてる特殊メソッドしかありません。

メソッド名が同じであれば動作はします (duck typing) 。 しかし、メソッド名が同じだからといって、それが正確に同じ型に分類されるかというと(structural subtyping)、これはまた別問題というわけです。 実際 mypy も structural subtyping ではなく nominal subtyping を使うように、推薦しています。

duck typing を mypy で使えますか? (opens new window)
Can I use duck typing with mypy?

mypy は nominal subtyping も structural subtyping も使えます。 structural subtyping は、"static duck typing" として考えられます。 Python のような duck typing の言語は structural subtyping が適していると主張する人もいます。
Mypy provides support for both nominal subtyping and structural subtyping. Structural subtyping can be thought of as “static duck typing”. Some argue that structural subtyping is better suited for languages with duck typing such as Python.

しかしながら mypy では、主に nominal subtyping を使用し、structural subtyping についてはほとんどの場合、mypy の設定を有効にしないと使えません (ただし Iterable のような組み込みプロトコルの場合は除きます。組み込みプロトコルについては structural subtyping が常に有効になっています)。
Mypy however primarily uses nominal subtyping, leaving structural subtyping mostly opt-in (except for built-in protocols such as Iterable that always support structural subtyping).
Here are some reasons why:

  • nominal subtype を使えば、短くてわかりやすいエラーメッセージを生成しやすい 。 これは型推論を使う時に特に重要です。
    It is easy to generate short and informative error messages when using a nominal type system. This is especially important when using type inference.

  • Python は nominal subtyping された型に対して組み込み関数 isinstance() を使いテストをすることができます、そしてプログラムの中で広く使われています。 structural subtyping された型に対して組み込み関数 isinstance() が使えるのは、ごく限定されていて nominal subtyping に基づくテストよりも型安全ではありません。
    Python provides built-in support for nominal isinstance() tests and they are widely used in programs. Only limited support for structural isinstance() is available, and it’s less type safe than nominal type tests.

  • 多くのプログラマはすでに static, nominal subtyping に習熟していて、 Java, C++, C# で上手く使われています。 structural subtyping を採用した言語は、ほとんどありません。
    Many programmers are already familiar with static, nominal subtyping and it has been successfully used in languages such as Java, C++ and C#. Fewer languages use structural subtyping.

しかしながら structural subtyping は、有効でもあります。 例えばもしプロトコルによって型が決定されるなら、"public API" はより柔軟なものになるかもしれません。 またプロトコルによって型が決定されるなら、 ABC の実装を明示的に宣言する必要も無くなります。
However, structural subtyping can also be useful. For example, a “public API” may be more flexible if it is typed with protocols. Also, using protocol types removes the necessity to explicitly declare implementations of ABCs.

可能な限り nominal classes を使い、どうしても必要なら protocols を使うことを、私たちは経験的にオススメしています。 protocol types と structural subtyping に関する詳細は Protocols and structural subtyping (opens new window)PEP 544 (opens new window) をご確認ください。
As a rule of thumb, we recommend using nominal classes where possible, and protocols where necessary. For more details about protocol types and structural subtyping see Protocols and structural subtyping and PEP 544.

thought of as (opens new window)
《be ~》~(である)と考えられる

# 9. おわりに

for 文からはじめて、かなり長々と書き、 最終的に構造的派生型 structural subtyping にまでたどり着いてしまいました。

しかしイテレータは Python にとって for 文そのものであり、もっともよく使われている機能です。 これを掘り下げて理解することは、そこまで悪いことではないのかなと思ったりもします。

こことは別に if 文についても書いてるのですが、そこでも最終的に型の話に落ち着きました。 そういうのって、ちょっと面白いなと思ったりもします。 結局 if 文の話は bool 型, for 文の話は Iterator 型と Iterable 型について考えていた訳です。

この次の記事では try 文について触れていきます。 そこでは Optional 型について、考えていきます。