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






記事を移転しました。今後ともどうぞ、よろしくお願いいたします。

https://python.ms/iterable/







以下、旧版





for 文で繰り返せる
オブジェクト







例えば 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















イテラブルという言葉自体は for 文で使える、というごく簡単なものです。 しかし「そのオブジェクトがイテラブルであるかどうかをどうやって判定するのか」という話になると、すこし話が難しくなります。

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


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

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


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

# コピペで動きます。
イテラブル = [0, 1, 2]
イテレータ = iter(イテラブル)
while True:
    try:
        要素 = next(イテレータ)
    except StopIteration:
        break
    print(要素)


Python の for 文は中でイテレータが動いているので、思いのほか難しいのです。 自分はここまで理解するのに10年かかりました。 下のコメントを見た時、ちょっとだけフフッと笑ってしまいました。

10:風吹けば名無し 2018/03/23(金) 22:40:27.09 ID xAC5A7ze0
pythonにしろ直感的にわかりやすい
【悲報】プログラマーのワイ、for文がわからなくて怒鳴られる


実は、この記事は for 文を理解する5講座の5講座目になり、 これまでの4講座を通して for 文とは何かについて考えてきました。 「1. for 文ってなに?」では for 文の考え方を再整理して、「2. ジェネレータってなに?」、「3. map, filter ってなに?」ではイテレータの具体例に触れて、 「4. イテレータってなに?」では抽象的なイテレータの概念そのものに触れ、そしてイテレータを自作しました。

f:id:domodomodomo:20181103193404j:plain

もし Python の for 文ってどうやって動いてるんだろう?と言うことに、 ご興味がありましたら是非 「for 文ってなに?」 から、斜め読みで十分なので読み進めてみてください。

もし残念ながらご興味がなければ知らなくても業務においては問題にならないので、無理せず読み進められなくても大丈夫なので、ご安心ください。

ここから先は「そのオブジェクトがイテラブルであるかどうかをどうやって判定するのか」と言う少し小難しい話をします。 わかると大したことないのですが。

しかしこの知識が一体なんの役に立つのでしょうか? mypy を使ってコーディングするときに、いくらか理解のお役に立てるかもしれません。 mypy とは型を明示して書かれた Python のコードを検査してくれるツールです。

mypy を使った簡単な型検査の例を示します。 ここでは mypy そのものの、詳しいご説明は省略させていただきます。

$ # pip install しないと使えない。
$ pip install mypy
# sample.py
def f(a: int) -> int:
    return 2 * a

print(f('Hello, world!'))
$ # int を引数に取る関数 f に str を代入していると警告される。
$ mypy sample.py 
sample.py:6: error: Argument 1 to "f" has incompatible type "str"; expected "int"
$


たぶん、こんなコードでは、これがなんの役に立つのか、わからないと思います。 個人、少人数で書いたり小さいコードを書くときにはあまり効果を発揮しません。 これは、型を書くのは思いのほか面倒だからです。

しかし、大人数で大きなコードを書くときにはとても有効だったりします。 型を書くことで型に関わるエラーだけについては、実行する前に発見することができるからです。

型を明示することは、完全解ではありません。 しかし、それでも実際に動かしてみるまでエラーが出るかわからないという不安を、部分的に改善してくれます。

しかし、イテラブルと mypy に一体なんの関係があるのでしょうか?








Python の iterable ってなに?









1. iterable であるかどうか判定したい。

簡単には、次の関数で iterable であるかどうか判定できます。

def isiterable(container):
    return hasattr(container, '__iter__')


iterable な組み込み型の一覧を取得しました。 import しなくても使えるクラスを 組み込み型と言います。 この組み込み型のうち for 文で使えるのは、次のクラスです。

# 1) 組み込み型の一覧
builtin_types = (
    cls
    for cls
    in __builtins__.__dict__.values()
    if isinstance(cls, type)
)

# 2) iterable な組み込み型の一覧
builtin_iterbale_types = (
    cls
    for cls
    in builtin_types
    if hasattr(cls, '__iter__')
)

# 3) iterable な組み込み型の一覧を表示する。
print(*builtin_iterbale_types, sep='\n')
>>> # 3) iterable な組み込み型の一覧を表示する。
... print(*builtin_iterbale_types, sep='\n')
range
list
tuple
set
dict
str
zip
filter
map
bytearray
bytes
enumerate
frozenset
reversed
>>>



公式の用語集を覗いてみます。

iterable - 用語集
一度に一つずつ、自分が持つ要素を返すことができるオブジェクトです。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 と表現していたためです。 いまは完全に統一されています。
Unifying types and classes in Python 2.2
Python : terminology 'class' VS 'type'


注釈②
「ユーザが ... シーケンスの動作をする __getitem__ メソッドを実装した ... クラス」 と言うのは、 次のようなクラスです。

class Seq(object):
    def __getitem__(self, index):
        if 0 <= index <= 3:
            return index * 2
        else:
           raise IndexError

seq = Seq()

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


for i in seq:
    i

# 0
# 2
# 4
# 6



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

シーケンスも iterable なので __iter__ の有無だけで isiterable の判定は、正確と言う訳ではなさそうです。

2. もっと正確に iterable であるかどうか判定したい

# iterable であるかどうかを判定するコード
def isiterable(container):
    if hasattr(container, '__iter__'):
        return True
    elif hasattr(container, '__getitem__'):
        raise Exception
    else:
        return False
# iterable であるかどうかをテストするコード
#   -> 実際に使って動かせば判定できないこともない...
#   -> 動かさずに判定、静的には型検査をすることはできない...
assert all(a == b for a,b in zip(container, iterable))

2.0. どういうオブジェクトなら in の中で使うことができるの?

  1. __iter__ を実装したクラスに属するオブジェクト
  2. __geitem__ をシーケンスとして実装したクラスに属するオブジェクト

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

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

答え: 言っていいです。

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

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


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

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

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

2.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__ メソッドを持たないといけないのか
Why must an iterator have an __iter__ method?

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

3. なんで 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__ メソッドを持たないといけないのか
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 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 , 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
同じ興味を持つ人たちの集まり、特定のテーマの自由討論会 ◆特にIT関連のフォーラムなどで、特定のテーマに関連や関心のある人が集まり、 自由に議論したり情報交換したりする場を指す。会議のプログラムとして予定されているものもあれば、非公式のものもある。

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



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

アンダースコアがないと、普通のユーザが定義したメソッドとプロトコルのメソッドの区別が、つかないやろって話は len 関数の時にも同じような話をしていました。
Python の len の意味、なんでメソッドではなく関数なの? - いっきに Python に詳しくなるサイト


3.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 の決定的な違いを例とともに


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

3.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 の「項目17: 引数に対してイテレータを使うときには確実さを尊ぶ」で 内部イテレータと外部イテレータを区別する際に、この __iter__ を活用する書き方を紹介しています。

4. 基本型と派生型

子クラスのことを難しい言葉で派生型と言います。

反対に、親クラスのことを難しい言葉で基本型と言います。 また、派生型には2種類あります。公称型と構造型です。

# 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

4.1. シーケンスとマッピング

Python ではたとえ継承していなくても、クラスが特定のメソッドを "全て正しく" 実装していれば、 そのクラスは sequence だ、mapping だと言えます。 sequence, mapping が実装するべきメソッドの一覧は以下を参照してください。
8.4. collections.abc — コレクションの抽象基底クラス

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

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

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


このようにしてメソッドの集まりから類推的に決定される型を、サブクラス subclass また派生型 subtype と表現されています。 また、派生型 subtype が備えるべきメソッドの集まりをプロトコル protocol と呼びます。

4.2. イテレータ

いままで見てきたジェネレータイテレータ, 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


Python を習いたての人に、ジェネレータ式について質問された時のことを考えて見ます。

「この括弧 ( ) の中に for 文がはいってるのはなに?」と質問された時、「イテレータだよ。」と答えます。 「イテレータってなに?」と質問された時に「__next__, __iter__ があるオブジェクトだよ。」と言っても、 きっとわからなくて不安な思いをさせてしまいます。

__next__, __iter__ メソッドを実装していれば iterator だと言えます。 これは、とても簡潔で、おそらく正確な回答だと思います。 そして、いまこの記事を読んでいただいてる方にとって、 「イテレータの説明としては、この説明で十分じゃないかな」という感覚になっていたら幸いです。

4.3. イテラブル

list や range などの Iterable な container は Iterable という型の派生型だと考えることができます。

派生型 とは 子クラス のことです。 Python ではメソッド名が同じなら、クラスを継承していなくても多態性が使えます。

直接、Iterable クラスを継承していなくても、__iter__ を実装していれば Iterable の子クラスとみなすこと、判定することができます。 このようにして型を判定することを構造的型付け structural subtyping と言います。

# 問題なく実行できる。
class Foo(object):
    def __iter__(self):
        yield 1
        yield 2

for x in Foo():
    print(x)

# 1
# 2


5. mypy

5.1. 昔の mypy

例えば for 文は iterable の子クラスしか実行できないはずです。 昔の mypy はそれを検知していました。

$ # Foo() が iterable ではないと返される
$ mypy sample.py 
sample.py:6: error: Iterable expected
$
$ mypy -V
mypy 0.540
$


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

from typing import Iterator, Iterable

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

for x in Foo():
    print(x)
$ mypy sample2.py 
$
$ mypy -V
mypy 0.540
$

Mypy doesn't recognize objects implementing __iter__ as being Iterable · Issue #2598 · python/mypy · GitHub

5.2. 今の mypy

今の mypy では、structual subtyping ができるようになっています。

class Foo(object):
    def __iter__(self):
        yield 1
        yield 2

for x in Foo():
    print(x)
$ mypy sample.py 
$
$ mypy -V
mypy 0.610
$

5.3. duck typing

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

Python では、例えば iterable というクラスが継承されていなくても、 同じ名前のメソッドさえ定義されていれば、iterable というクラスとして振舞うことができます。

このようにして名前さえ同じなら動いてくれることを duck typing と呼んだりします。

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

static typing が静的型付け, dynamic typing が動的型付けと訳されるなら、 duck typing は鴨的型付けって訳になるんですかね..。
デザインパターン「Iterator」-Qiita (Java での iterator の例)

5.4. structural subtyping

mypy の structural subtyping については PEP 544 にて議論されています。 PEP 544 - Protocols: Structural subtyping (static duck typing)

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

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

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

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

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

duck typing を mypy で使えますか?
Can I use duck typing with mypy?

mypy は nominal subtyping も structural subtyping も使えます。 structural subtyping は、"static duck typing" として考えられます。 Python のような duck typing の言語は structual 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 を使用し、structual 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 subtypingPEP 544 をご確認ください。
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
《be ~》~(である)と考えられる

6. おわりに

for 文からはじめて、かなり長々と書き、最終的に structural subtyping にまでたどり着いてしまいました。 しかしイテレータPython にとって for 文そのものであり、もっともよく使われている機能です。 これを掘り下げて理解することは、そこまで悪いことではないのかなと思ったりもします。

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

それでも、もし Python を習いたての人に型, クラスってなに?と聞かれたら 「オブジェクトに共通の値とメソッドをひとまとまりにしたもの」という説明で十分だと思います。 これはとても簡単で見たままです。

「クラスはオブジェクト間の振る舞いを決める」みたいなことを言うと、おそらく不安にさせてしまうかなと思います。 「振る舞い」という表現は、よく見かける表現ですが知らない人に何か説明をするときに、 抽象的であまり適切ではないかなと思ったりもします。

こんだけ長文を書いててお前が言うなよ、と言う話でもありますが... 自分は、ちゃんと意識してないと光の速さでマウンティングする文章を書きだしてしまいますし、 気付いてるならまだしも気づいてさえいない文章がまだたくさんあると思います。

この記事は感動しました。 最初のタイトルは「新人プログラマをレビューで殺す方法」でした。 おそらく指摘を受けて変えたのだと思います。
新人プログラマをレビューで殺さない方法

はじめて見たときは煽り文句かなと思ったのですが、冷静に考えてみると的確な表現だと思うようになりました。 タイトルを「殺さない」にすると「言葉は人を殺すことができるかもしれない」という生ぬるいニュアンスがはいってしまいます。

「殺す」の方が適切な表現だと感じました。 なぜなら「言葉は人を殺すことができるから」です。