Python: 抽象クラスを実装する (abc)
お仕事のPythonコードを読んでいて、抽象基底クラス (abstract base class: ABC) で実装されているクラスがありました。そういえば使ったことがなかったので、この機に整理しましたのでその覚書です。
$ python --version Python 3.6.8
Pythonでは抽象基底クラスを実装するための標準ライブラリとして、abcが提供されています。以下の例を実装しながら使い方を見ていきます。
- IDを使ってユーザの情報 (名前など) にアクセスできるUsersを、インタフェースのみの抽象クラスとして実装
- ユーザの情報の実体を持ちインタフェースを具象化するUsersDictを、抽象クラスを継承して実装
まず抽象クラス側の実装です。キモは2点です。
- ABCMetaをmetaclassに渡している
- 抽象メソッドを
@abstractmethod
アノテーションを付与している (中身は空)- 具象メソッド (下では
get_family_name
) があってもOK
- 具象メソッド (下では
from abc import ABCMeta, abstractmethod class Users(metaclass=ABCMeta): @abstractmethod def get_name(self, user_id: int) -> str: pass # あるいは raise NotImplementedError() @abstractmethod def put_name(self, user_id: int, name: str): pass def get_family_name(self, user_id) -> str: return self.get_name(user_id).split()[0]
以下のように抽象クラスをインスタンス化しようとするとエラーとなります。
- インスタンス生成時にABCMetaの
__new__
メソッドが自身と親の抽象メソッドをリストアップして、未実装のメソッドが無いかチェックしてます
u = Users()
# -> TypeError: Can't instantiate abstract class Users with abstract methods get_name, put_name
次にそれを継承する具象クラスです。抽象メソッドを同じシグネチャで実装しています。
- もちろん具象クラス固有のメソッド (get_dict) を定義してもOKです
class DictUsers(Users): def __init__(self, d: dict): self._d = d def get_name(self, user_id: int) -> str: return self._d[user_id] def put_name(self, user_id: int, name: str): self._d[user_id] = name def get_dict(self): return self._d
もし以下のように抽象クラスを実装しない場合、TypeErrorとなります。Usersをインスタンス化しようとしたときと同様です。
class DictUsers(Users): def __init__(self, d: dict): self._d = d # -> TypeError: Can't instantiate abstract class DictUsers with abstract methods get_name, put_name
DictUsersを生成すると、あとは通常のクラスオブジェクトと同様に扱えます。
- Usersを継承してますので、issubclassやisinstanceはTrueが返ります
- Usersで定義された具象メソッド (get_family_name) にもアクセスできます (中ではDictUsersのget_nameにアクセスしてます)
dict_users = DictUsers({1: "田中 はじめ", 2: "梅田 よしお"}) print(issubclass(dict_users.__class__, Users)) # True print(isinstance(dict_users, Users)) # True print(dict_users.get_name(1)) # 田中 はじめ print(dict_users.get_dict()) # {1: '田中 はじめ', 2: '梅田 よしお'} print(dict_users.get_family_name(2)) # 梅田
仮にdictではなくPandasのDataFrameでデータを保持したいとなった場合でも、同様にUsersを継承して抽象クラスを実装するだけで対応できます。
import pandas as pd class DataFrameUsers(Users): def __init__(self, df: pd.DataFrame): self._df = df def get_name(self, user_id: int) -> str: return self._df.loc[user_id, "name"] def put_name(self, user_id: int, name: str): self._df.loc[user_id, "name"] = name df_users = DataFrameUsers(pd.DataFrame( {"name": ["田中 はじめ", "梅田 よしお"]}, index=[1, 2] )) print(df_users.get_name(1)) # 田中 はじめ df_users.put_name(2, "高木 よしお") print(df_users.get_family_name(2)) # 高木
利用できるケースは多いかと思いますので、覚えておくと良いかと思います。