Python: 抽象クラスを実装する (abc)

お仕事のPythonコードを読んでいて、抽象基底クラス (abstract base class: ABC) で実装されているクラスがありました。そういえば使ったことがなかったので、この機に整理しましたのでその覚書です。

$ python --version
Python 3.6.8

Pythonでは抽象基底クラスを実装するための標準ライブラリとして、abcが提供されています。以下の例を実装しながら使い方を見ていきます。

  • IDを使ってユーザの情報 (名前など) にアクセスできるUsersを、インタフェースのみの抽象クラスとして実装
  • ユーザの情報の実体を持ちインタフェースを具象化するUsersDictを、抽象クラスを継承して実装

docs.python.org

まず抽象クラス側の実装です。キモは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))  # 高木

利用できるケースは多いかと思いますので、覚えておくと良いかと思います。