note.nkmk.me

Pythonのhasattr(), 抽象基底クラスABCによるダックタイピング

Date: 2019-12-08 / tags: Python

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モジュールの他にも以下のような組み込みの抽象基底クラスを提供するモジュールがある。

上述のように、独自の抽象基底クラスを生成することも可能。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事