Pythonのhasattr(), 抽象基底クラスABCによるダックタイピング
Pythonでダック・タイピングを実現する方法として、例外処理、組み込み関数hasattr()
、抽象基底クラス(ABC: Abstract Base Class)を用いた方法をそれぞれ説明する。
- ダック・タイピングとは
- LBYL: Look Before You Leap
type()
やisinstance()
による判定
- EAFP: Easier to Ask for Forgiveness than Permission
try
およびexcept
による例外処理
- 組み込み関数
hasattr()
hasattr()
による判定
- 抽象基底クラス(ABC: Abstract Base Class)
- コレクションの抽象基底クラス
collections.abc
による判定 - 抽象基底クラスを継承したクラスの作成
- そのほかの抽象基底クラス
- コレクションの抽象基底クラス
ダック・タイピングとは
Python公式ドキュメントの用語集におけるダック・タイピングの説明は以下の通り。
あるオブジェクトが正しいインタフェースを持っているかを決定するのにオブジェクトの型を見ないプログラミングスタイルです。代わりに、単純にオブジェクトのメソッドや属性が呼ばれたり使われたりします。(「アヒルのように見えて、アヒルのように鳴けば、それはアヒルである。」)インタフェースを型より重視することで、上手くデザインされたコードは、ポリモーフィックな代替を許して柔軟性を向上させます。
ダックタイピングは type() や isinstance() による判定を避けます。 (ただし、ダックタイピングを 抽象基底クラス で補完することもできます。) その代わり、典型的に hasattr() 判定や EAFP プログラミングを利用します。 用語集 - duck-typing — Python 3.8.0 ドキュメント
以降、まずダック・タイピングではない例として、
- LBYLスタイル(
type()
やisinstance()
による判定)
そのあと、ダック・タイピングを実現する例として、上で引用した説明にもある、
- EAFPスタイル(
try
およびexcept
による例外処理) hasattr()
による判定- 抽象基底クラス(ABC: Abstract Base Class)を利用した判定
について、それぞれ具体例を挙げて紹介する。
対象のオブジェクトのlen()
の結果をprint()
で出力する、というシンプルな処理を例とする。
なお、どの方法が良いかといった判断は個々の状況や目的によって異なるので、ここでは深追いしない。
LBYL: Look Before You Leap
LBYLは「Look Before You Leap(転ばぬ先の杖)」という意味。
「ころばぬ先の杖 (look before you leap)」 の略です。このコーディングスタイルでは、呼び出しや検索を行う前に、明示的に前提条件 (pre-condition) 判定を行います。 EAFP アプローチと対照的で、 if 文がたくさん使われるのが特徴的です。 用語集 - LBYL — Python 3.8.0 ドキュメント
type()やisinstance()による判定
LBYLスタイルの型判定では、type()
やisinstance()
を使ってオブジェクトが特定の型かどうかを判定する。C言語などに慣れている人にとっては親しみやすい方法。ダック・タイピングではない。
例えば、オブジェクトがリストのときのみlen()
の結果を出力する関数は以下の通り。
def print_len_lbyl_list(x):
if isinstance(x, list):
print(len(x))
else:
print('x is not list')
print_len_lbyl_list([0, 1, 2])
# 3
print_len_lbyl_list(100)
# x is not list
print_len_lbyl_list((0, 1, 2))
# x is not list
print_len_lbyl_list('abc')
# x is not list
タプルにも対応させようとすると以下のようになる。
def print_len_lbyl_list_tuple(x):
if isinstance(x, (list, tuple)):
print(len(x))
else:
print('x is not list or tuple')
print_len_lbyl_list_tuple([0, 1, 2])
# 3
print_len_lbyl_list_tuple(100)
# x is not list or tuple
print_len_lbyl_list_tuple((0, 1, 2))
# 3
print_len_lbyl_list_tuple('abc')
# x is not list or tuple
文字列str
などlen()
の対象となる型はほかにもあるが、それらに対応させる場合は明示的に条件に加える必要がある。
type()
やisinstance()
についての詳細は以下の記事を参照。
EAFP: Easier to Ask for Forgiveness than Permission
EAFPは「Easier to Ask for Forgiveness than Permission(認可をとるより許しを請う方が容易)」という意味。
「認可をとるより許しを請う方が容易 (easier to ask for forgiveness than permission、マーフィーの法則)」の略です。この Python で広く使われているコーディングスタイルでは、通常は有効なキーや属性が存在するものと仮定し、その仮定が誤っていた場合に例外を捕捉します。この簡潔で手早く書けるコーディングスタイルには、 try 文および except 文がたくさんあるのが特徴です。このテクニックは、C のような言語でよく使われている LBYL スタイルと対照的なものです。 用語集 - EAFP — Python 3.8.0 ドキュメント
とりあえず実行してみてうまくいかなかった場合には例外を捕捉して処理する、というスタイル。
tryおよびexceptによる例外処理
EAFPスタイルでは、try
およびexcept
による例外処理を用いる。
import numpy as np
def print_len_eafp(x):
try:
print(len(x))
except TypeError as e:
print(e)
print_len_eafp([0, 1, 2])
# 3
print_len_eafp(100)
# object of type 'int' has no len()
print_len_eafp((0, 1, 2))
# 3
print_len_eafp('abc')
# 3
a = np.arange(3)
print(a)
# [0 1 2]
print_len_eafp(a)
# 3
上の例のNumPy配列ndarray
のようなサードパーティライブラリのクラスのオブジェクトなども含めて、len()
の対象となりうるすべてのオブジェクトに対してprint(len(x))
が実行される。
例外処理についての詳細は以下の記事を参照。
組み込み関数hasattr()
例外処理を用いずにダック・タイピングを実現する方法として、組み込み関数hasatrr()
を用いる方法がある。
hasattr()
(= has attribute)という名前の通り、あるオブジェクトが属性やメソッドを持っているかどうかを判定し、True
またはFalse
を返す。
第一引数にオブジェクト、第二引数に属性やメソッドの名前の文字列を指定する。
l = [0, 1, 2]
print(type(l))
# <class 'list'>
print(hasattr(l, 'append'))
# True
print(hasattr(l, 'xxx'))
# False
hasattr()による判定
組み込み関数len()
は__len__()
メソッドを呼び出すように実装されている。
print(len(l))
# 3
print(l.__len__())
# 3
このため、len()
が実行できるかどうかは、そのオブジェクトが__len__()
メソッドを持っているかどうかで判定できる。
def print_len_hasattr(x):
if hasattr(x, '__len__'):
print(len(x))
else:
print('x has no __len__')
print_len_hasattr([0, 1, 2])
# 3
print_len_hasattr('abc')
# 3
print_len_hasattr(100)
# x has no __len__
a = np.arange(3)
print(a)
# [0 1 2]
print_len_hasattr(a)
# 3
このようにhasatrr()
を使うことで、実際の型が何であれ対象のメソッドや属性を持っていればOK(= アヒルのように鳴けばそれはアヒル)とするダック・タイピングを実現できる。
抽象基底クラス(ABC: Abstract Base Class)
ダック・タイピングを実現するもう一つの方法として、抽象基底クラス(ABC: Abstract Base Class)を利用する方法がある。
抽象基底クラスは duck-typing を補完するもので、 hasattr() などの別のテクニックでは不恰好であったり微妙に誤る (例えば magic methods の場合) 場合にインタフェースを定義する方法を提供します。 用語集 - abstract base class — Python 3.8.0 ドキュメント
次から紹介するように、コレクションや数値などの抽象基底クラスが組み込みで提供されている。
ここでは触れないが、標準ライブラリのabcモジュールを使うと独自の抽象基底クラスを定義することもできる。
コレクションの抽象基底クラスcollections.abcによる判定
collections.abcモジュールは、コレクション(シーケンスやマッピングなど)、つまり、リストや辞書のようなオブジェクトの抽象基底クラスを提供する。
詳細は上記の公式ドキュメントの冒頭の表を参照されたいが、例えば、__len__()
を持つ抽象基底クラスとしてSized
、__len__()
と__getitem__()
を持つ抽象基底クラスとしてSequence
などが定義されている。
これらの抽象基底クラスとisinstance()
を使って判定する。
collections.abc.Sized
の例。__len__()
を持つ型のオブジェクトが対象となる。
import collections
def print_len_abc_sized(x):
if isinstance(x, collections.abc.Sized):
print(len(x))
else:
print('x is not Sized')
print_len_abc_sized([0, 1, 2])
# 3
print_len_abc_sized('abc')
# 3
print_len_abc_sized({0, 1, 2})
# 3
print_len_abc_sized(100)
# x is not Sized
collections.abc.Sequence
の例。リストや文字列は対象となるが、順番を持たない集合set
や辞書dict
は対象とならない。
def print_len_abc_sequence(x):
if isinstance(x, collections.abc.Sequence):
print(len(x))
else:
print('x is not Sequence')
print_len_abc_sequence([0, 1, 2])
# 3
print_len_abc_sequence('abc')
# 3
print_len_abc_sequence({0, 1, 2})
# x is not Sequence
print_len_abc_sequence({'k1': 1, 'k2': 2, 'k3': 3})
# x is not Sequence
collections.abc.MutableSequence
の例。ミュータブルな型のみが対象となるため、文字列やタプルは対象外。
def print_len_abc_mutablesequence(x):
if isinstance(x, collections.abc.MutableSequence):
print(len(x))
else:
print('x is not MutableSequence')
print_len_abc_mutablesequence([0, 1, 2])
# 3
print_len_abc_mutablesequence('abc')
# x is not MutableSequence
print_len_abc_mutablesequence((0, 1, 2))
# x is not MutableSequence
このように、より細かく柔軟な条件指定が可能になる。hasattr()
を複数使っても同じ処理が可能だが、抽象基底クラスを使ったほうがシンプルに書ける。
抽象基底クラスを継承したクラスの作成
抽象基底クラスを継承してクラスを作成すると
- 必要なメソッドが実装されていないときにエラーとなる(気づかせてくれる)
- 追加のメソッドが自動的に使用できるようになる
というメリットがある。
collections.abc.Sequence
を継承する例。
上記公式ドキュメントの表の抽象メソッド
の列に記載されているように、collections.abc.Sequence
には__len__()
と__getitem__()
の実装が必要。
足りていないとインスタンス生成時にエラーが発生する。
class MySequence(collections.abc.Sequence):
def __len__(self):
return 10
# ms = MySequence()
# TypeError: Can't instantiate abstract class MySequence with abstract methods __getitem__
必要なメソッドを実装すると、公式ドキュメントの表のmixin メソッド
の列に記載されているメソッド(index()
や__reversed__()
など)が自動的に生成される。
なお、ここでは簡単のために__len__()
は常に10
を返し、__getitem__()
はインデックスをそのまま返すというだけの内容で実装している。
class MySequence(collections.abc.Sequence):
def __len__(self):
return 10
def __getitem__(self, i):
return i
ms = MySequence()
print(len(ms))
# 10
print(ms[3])
# 3
print(ms.index(5))
# 5
print(list(reversed(ms)))
# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
print(isinstance(ms, collections.abc.Sequence))
# True
print(hasattr(ms, '__len__'))
# True
同じメソッドを実装していても、collections.abc.Sequence
を継承していないと追加のメソッドは使えないし、isinstance()
でもFalse
となる。hasatrr()
はメソッドの有無のみを判定するのでTrue
。
class MySequence_bare():
def __len__(self):
return 10
def __getitem__(self, i):
return i
msb = MySequence_bare()
print(len(msb))
# 10
print(msb[3])
# 3
# print(msb.index(5))
# AttributeError: 'MySequence_bare' object has no attribute 'index'
print(isinstance(msb, collections.abc.Sequence))
# False
print(hasattr(msb, '__len__'))
# True
そのほかの抽象基底クラス
collections.abcモジュールの他にも以下のような組み込みの抽象基底クラスを提供するモジュールがある。
- numbers --- 数の抽象基底クラス — Python 3.8.0 ドキュメント
- io --- ストリームを扱うコアツール — Python 3.8.0 ドキュメント
- importlib.abc --- import の実装 — Python 3.8.0 ドキュメント
上述のように、独自の抽象基底クラスを生成することも可能。