概要
- 抽象基底 (ABC) クラスは抽象クラスとは関係ない。「抽象的な意味で基底になるクラス」という意味。
- インスタンスが求める性質を持つか否か (例えば、必要なメソッドを持つか否か) を検査する統一的方法を提供する。
- ABC は直接の継承関係にないクラスに対して、意味的なインスタンス関係やサブクラス関係を検査できる。
- 前記検査は、ABC
C
に対してisinstance(x, C)
などを呼ぶことにより行う、と取り決める。
Motivating Example
次のような関数 has_positive_size()
を考えます。x
のサイズ (または長さ) が正であるならば True
を返す関数です。
def has_positive_size(x):
"returns True if x's length is positive."
return len(x) > 0
assert not has_positive_size([])
assert has_positive_size([1, 2])
assert has_positive_size((1, 2))
assert has_positive_size('Hello, world!')
assert has_positive_size({'id': 1, 'name': 'John'})
# TypeError: object of type 'int' has no len()
assert not has_positive_size(300)
各種コンテナ型 (配列、タプル、文字列、辞書など) に対してこの関数は期待通りに振る舞います。しかし、この関数を 300
に対して適用すると例外、つまり実行時エラーが発生します。
引数に対して len(x)
が呼べるか否かの検査を行い、実行時エラーを優雅に避けたい というのが解きたい問題です。
静的型付けなオブジェクト指向言語ではこうする
例えば Java であれば回避は簡単です。引数に適切な型を指定すればよいです。抽象クラス AbstractSized
やインタフェース ISized
を定義して、それを引数 x
の型としておけばよいです。
この方法は動的言語である Python には使えません。Python のインスタンスはいつでも属性を追加したり削除したりできるので、あくまでも実行時に検査する方法が必要です。
あるインスタンスがある性質を持つか否か、を検査したい
たとえばこのような感じで検査の結果により処理を分岐できれば目的を達成できます。では ...
の部分には何を書けばよいでしょうか。
def has_positive_size(x):
"returns True if x has length and its length is positive or not."
if ...: # <- what predicate should be written?
return len(x) > 0
else:
print(f'warning: {x} does not have size.')
return False
Python の中級者以上は知っていると思いますが、Python の仕組みにより、len(x)
は特殊メソッドの呼び出し x.__len()__
に変換されます。つまり、x
がメソッド __len__
を持つか否か を検査できればよいことになります。より一般的には、あるインスタンスがある性質を持つかどうかを検査できると、このような優雅なコードを書きやすいということが言えます。
属性アクセスによる naive な検査は推奨されない
Python の中級者以上なら getattr(x, '__len__')
するとかいくつか方法を思いつくと思いますが、PEP 3119 が言うところのフォーマリズムの観点からは好ましくないと言えます。
- 属性のアクセス方法はいろいろあって、アクセス自体にも (ディスクリプタなど) Python の処理がさらに介入するので求める結果が得られるか不確定であるし、統一的な方法がないとプログラマによって異なる処理になりえる。
- 属性の存在以外にも検査したい性質はあり得る。
PEP 3119: 抽象基底クラスと isinstance()
による統一的な検査機構の導入
それらを踏まえて、PEP 3119 で共通の検査機構が導入されました。まず動かしてみるのがいいと思います。
from collections.abc import Sized
assert isinstance([1, 2], Sized)
assert issubclass(list, Sized)
assert isinstance((1, 2), Sized)
assert isinstance(set({1, 2}), Sized)
assert not isinstance(3, Sized)
isinstance(_, Sized)
により、前述した、__len__
を持つか否かの検査を行えるようになっています。この検査の結果が True
であれば len(x)
を安心して呼ぶことができます1。また issubclass()
によるサブクラス検査も行えます。
ここで注意するべきは、[1, 2]
は実際には Sized
クラスのインスタンスではない ということです。実際には、[1, 2]
は list
クラスのインスタンスであり、list
には Sized
との間に継承関係はありません。
あくまでも **このインスタンス関係/継承関係は抽象的なものです。**よって、Sized
などの基底クラスは 抽象基底クラス (Abstract Base Class; ABC) と呼ばれます。Java や C++ の抽象クラスの「抽象」とは別の意味です。
これを用いると、望ましい実装は次のようになります。
from collections.abc import Sized
def has_positive_size(x):
"returns True if x has length and its length is positive or not."
if isinstance(x, Sized):
return len(x) > 0
else:
print(f'warning: {x} does not have size.')
return False
assert not has_positive_size([])
assert has_positive_size([1, 2])
assert has_positive_size((1, 2))
assert has_positive_size('Hello, world!')
assert has_positive_size({'id': 1, 'name': 'John'})
# does not raise TypeError
assert not has_positive_size(300)
300
が渡されたときには warning を出力しますが、実行時エラーにはなりません。
実装
どのように実装されているかざっくりと説明します。
Sized
のメタクラスは ABCMeta
です。この設定により、isinstance(x, Sized)
は最終的に Sized.__subclasshook__(Sized, type(x))
または Sized.__subclasshook__(Sized, x.__class__)
を呼び出します。この実装の中では関数呼び出し _check_methods(Sized, "__len__")
が行われ、属性 __len__
の探索が動的に行われます。
詳しくはソースコードの _collections_abc.py を見るとわかります。
ユーザ定義クラスと ABC
ユーザが定義したクラスに対しても ABC を用いた検査は有効です。
たとえば、ランダムな正の整数を返す __len__()
を持つクラス、MySized
を作ってみます。
これに対しても Sized
のインスタンス判定は成功します。
(Sized.register(MySized)
が必要だと思っていましたが不要です。)
from collections.abc import Sized
from random import randint
class MySized:
def __len__(self):
# returns random length, which is between 1 and 10
return randint(1, 10)
ms = MySized()
assert isinstance(ms, Sized)
assert has_positive_size(ms)
ユーザ定義 ABC
ABC を自作することもできます。説明は省略します。ABCMeta
をメタクラスとするクラスを作ると楽です。
この方法のメリット
このような検査をインスタンス判定とサブクラス判定に落とし込むことのメリットの1つとして、型検査との相性がよいということがあります。
今回対象とした関数を、
def has_positive_size(x: Sized) -> bool:
...
と、型ヒントを使って宣言することができるようになります (ただし、このような ABC を使った型指定が mypy
に通るか未確認です。Protocol
が必要だったりするかもしれません)。
-
ここは実際よりも楽天的に表現しています。どうやら、
__len__
属性の存否は検査しているけれど、それが callable であるかどうかは検査していません。また、x.__len__()
の返り値が整数であるか否か、さらに言えば 0 以上の整数であるか否か、も検査されていません。そこは開発者の良識に委ねられています。 ↩