初心者データサイエンティストの備忘録

調べたことは全部ここに書いて自分の辞書を作る

【Python】抽象基底クラスについて勉強したこと

Pythonの抽象基底クラスについて勉強したので、まとめようと思います。

抽象基底クラスとは

抽象基底クラスの定義については、抽象クラスを使うメリット - Qiitaが理解しやすかったです。この記事によれば、抽象基底クラスとは、

  • 専ら他のクラスに継承されることによって使用される。
  • インスタンスを持たない。

であると説明されています。クラスから具体的なインスタンスを作り出さないことから「抽象」の名前が、専ら他のクラスに継承されることによって使用されることから「基底」という名前がついているのではないかと私は推測しています。

上記のように説明されてもどんなクラスが抽象基底クラスなのかわかりにくいので、具体例を挙げてみます。

from abc import ABCMeta

class Parent(metaclass=ABCMeta):
    def print_name(self):
        pass
    
    def print_age(self):
        pass

上記のParentクラスが抽象基底クラスです。Parent内で定義されるメソッドprint_name、print_ageは処理が定義されていません。これらのメソッドは、子クラスで定義されることが期待されており、抽象基底クラスでは定義しません。 なお、Parentの引数でmetaclass=ABCMetaとしていますが、この設定をすることでParentは抽象基底クラスであると定義されることになります。

デコレータ@abstractclassmethod

抽象基底クラス内のメソッドに@abstractclassmethodというデコレータをつけることができます。このデコレータを付与されたメソッドは、必ず子クラスで定義しなければなりません。実装しないとインスタンス作成時にエラーが出力されます。 これも例を挙げます。

上記のスクリプトを見るとTreatUserInfoWITHPasswordクラスではちゃんとcombine_user_infoメソッドが定義されており、インスタンス作成時にエラーの出力がありません。しかし、TreatUserInfoWITHOUTPasswordクラスにはcombine_user_infoメソッドが定義されておらず、それによってエラーが出力されています。

抽象基底クラスの使いどころ

ここまで抽象基底クラスを定義してきましたが、どのようなときに使うのでしょうか?それは、子クラスに必ず同じ名前のメソッドを持たせたいときです。

また、例を挙げて説明します。

上記のスクリプトでは、各メソッドは下記のような関係になっています。

処理内容が子クラス共通 処理内容が子クラス共通ではないが、必ず実装したい
print_name combine_user_info

このように、処理内容が子クラス共通ではないが、必ず実装したいメソッドがある際に抽象基底クラスを使います。

おまけ

あとは、自分が勉強したときに疑問に感じたことと、その回答をまとめていきます。

抽象規定クラスではないクラスの継承と抽象規定クラスの継承の違いはなに?

一言でいえば、抽象基底クラスではないクラスの継承だと、メソッドの実装し忘れに気付きにくいです。それが、抽象基底クラスの@abstractclassmethodを使うことで、実装し忘れに気付くことができます。

ABCとABCMetaの違いはなに?

ABCはABCMetaを継承したヘルパークラスです。これを使って抽象基底クラスを作ることもできます。

from abc import ABCMeta, ABC

# ABCMetaを使って抽象基底クラスを作成
class Parent(metaclass=ABCMeta):
    pass

# ABCを使って抽象基底クラスを作成
class Parent(ABC): # ABCを継承して作る
    pass

どちらを使っても挙動は(多分)同じです。

ダックタイピングとの関係性はなに?

ダックタイピングとは、異なるインスタンスに対して、同じメソッドで処理を呼び出せることを言います。
(過去記事) aisinkakura-datascientist.hatenablog.com

抽象基底クラスを使うことで、ダックタイピングも実装しやすくなります(抽象基底クラスを使わなくてもダックタイピングできるが、あった方が実装しやすい)。その理由としては、@abstractclassmethodを使うことで同じ名前のメソッドを子クラスに定義することを強制できるからです。

from abc import ABCMeta, abstractclassmethod

class Animal(metaclass=ABCMeta):
    @abstractclassmethod
    def cry(self):
        pass
    
class Dog(Animal):
    def cry(self):
        print("wow")
        
class Cat(Animal):
    def cry(self):
        print("nya")

# クラスを問わず実行できるメソッド
def animal_cry(obj):
    obj.cry()

# インスタンス作成
dog = Dog()
cat = Cat()

# 実行
animal_cry(dog)
animal_cry(cat)

Animalクラスで定義するcryに@abstractclassmethodがついていることで、DogかCatのどちらかでcryを実装し忘れていても、animal_cryの実行を待たず、インスタンス作成時に気付くことができます。 これは、現実の開発現場では結合テストを待たず、単体テスト実行時に実装し忘れに気付くことにつながります。