👨‍💻

Pythonのオブジェクト指向プログラミングを完全理解 (2)

2023/02/13に公開

Pythonのオブジェクト指向プログラミングを完全理解 (1)に続き

5. Pythonのオブジェクト指向の発展

Pythonのオブジェクト指向プログラミングの基本的な形式を見てきました。実務においても、4. pythonのオブジェクト指向の基本の内容でほぼ事足ります。しかし、高度な機能の実現、モジュールの自作またはデザインパターンに則った綺麗なシステムを作成したいなら、もう少し発展した内容を知る必要があります。

5-1. 特殊メソッド

3-14. 演算子オーバーロードで少し触れましたが、Pythonのクラスには__init__のような、前後に2つのアンダースコアの付いた「特殊メソッド」、「マジックメソッド」または「__dunder__ダンダー:ダブルアンダースコア)」と呼ばれるメソッドや変数がたくさん存在します。これらのメソッドや変数は一部または全てのオブジェクト共通のもので、様々な機能を実現できます。

import collections
import copy
import math
import operator
import pickle
import sys
import asyncio


class Dunder:
    def __abs__(self):
        # abs(Dunder()); 絶対値を計算する時に呼び出される
        return self.x

    def __add__(self, other):
        # Dunder() + 123; 加算をする時に呼び出される
        return self.x + other

    async def __aenter__(self):
        # `__aenter__`と`__aexit__`は一緒に実装しなければならない
        # async with Dunder() as coro; awaitable object限定
        await asyncio.sleep(1)

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # `__aenter__`と`__aexit__`は一緒に実装しなければならない
        # async with Dunder() as coro; awaitable object限定
        await asyncio.sleep(1)

    def __aiter__(self):
        # `__aiter__`と`__anext__`は一緒に実装しなければならない
        # async for _ in Dunder()
        return self

    def __and__(self, other):
        # Dunder() & 123; 論理積演算をする時に呼び出される
        return self.x & other

    async def __anext__(self):
        # `__aiter__`と`__anext__`は一緒に実装しなければならない
        # async for _ in Dunder(); 要素がなくなったら、StopAsyncIterationを引き起こすべき
        # awaitable object限定
        val = await self.readline()
        if val == b'':
            raise StopAsyncIteration
        return val

    def __await__(self):
        # await Dunder(); 戻り値はiterator限定
        return self.z  # `__next__`と`__iter__`を実装したクラス

    def __call__(self, *args, **kwargs):
        # Dunder()(); callable(Dunder()) == True; 関数のように呼び出せる
        return self.x

    def __init__(self, **kwargs):
        # Dunder(y=2); イニシャライザ
        self.x = 1
        self.y = kwargs.get('y')
        self.z = [1, 2, 3]

    def __bool__(self):
        # bool(Dunder()) == True; ブール演算する時に呼び出される
        return True

    def __bytes__(self):
        # bytes(Dunder()); バイト列
        return bytes('123', encoding='UTF-8')

    def __ceil__(self):
        # math.ceil(Dunder()); 切り上げ計算する時に呼び出される
        return math.ceil(self.x)

    def __class_getitem__(cls, item):
        # Dunder[int] == "Dunder[int]"; このメソッドは自動的にクラスメソッドになる
        return f"{cls.__name__}[{item.__name__}]"

    def __complex__(self):
        # complex(Dunder()); 複素数
        return complex(self.x)

    def __contains__(self, item):
        # item not in Dunder(); item in Dunder()
        return True if item in self.z else False

    def __copy__(self):
        # copy.copy(Dunder()); 浅いコピーをする時に呼び出される
        return copy.copy(self.z)

    def __deepcopy__(self, memodict={}):
        # copy.deepcopy(Dunder()); 深いコピーをする時に呼び出される
        return copy.deepcopy(self.z)

    def __del__(self):
        # dunder = Dunder(); del dunder;
        # オブジェクトを削除する時に呼び出される。ガベージコレクションにも対応
        del self

    def __delattr__(self, item):
        # del self.params; インスタンス変数を削除する時に呼び出される
        del self.item

    def __delete__(self, instance):
        # class Owner: dunder = Dunder()
        # del Owner().medusa; ディスクリプタメソッド
        # 所有者クラスの属性として削除する時に呼び出される
        del self.x

    def __delitem__(self, key):
        # del Dunder()['some_key']
        self.__dict__.pop(key)

    def __dir__(self):
        # dir(Dunder()); オブジェクトの全ての属性を格納するiterable objectを返す
        return super().__dir__()

    def __divmod__(self, other):
        # divmod(Dunder(), 123); 割り算の商と余りを同時に取得
        return divmod(self.x, other)

    def __enter__(self):
        # with Dunder() as dunder: pass
        return self

    def __eq__(self, other):
        # Dunder() == 123; 等価演算をする時に呼び出される
        return self.x == other

    def __exit__(self, exc_type, exc_val, exc_tb):
        # with Dunder() as dunder: pass; 引数はそれぞれTypeError、ValueError、Traceback
        return True

    def __float__(self):
        # float(Dunder()); 浮動小数にする
        return float(self.x)

    def __floor__(self):
        # math.floor(Dunder()); 小数点を切り捨てる
        return math.floor(self.x)

    def __floordiv__(self, other):
        # Dunder() // 123; 切り捨て除算する時に呼び出される
        return self.x // other

    def __format__(self, format_spec):
        # '{:x}'format(Dunder()); format(Dunder(), 'x')
        if format_spec == 'x':
            return '{}'.format(self.x)
        return '{}'.format(self.y)

    def __fspath__(self):
        # os.fspath(Dunder()) == '/var/www/html/mysite'; ファイルシステムパスを返す
        return '/var/www/html/mysite'

    def __ge__(self, other):
        # Dunder() >= 123
        return self.x >= other

    def __get__(self, instance, owner):
        # class Test: dunder = Dunder(); ディスクリプタメソッド
        # `Test().dunder`または`Test.dunder`をする時にの時に呼び出される
        return self.x

    def __getattr__(self, item):
        # Dunder().a; 未定義のメンバーにアクセスする時に呼び出される
        return f'object has no attribute "{item}"'

    def __getattribute__(self, item):
        # Dunder().a; 未定義・定義済みにかかわらず、全てのメンバーにアクセスする時に呼び出される
        # `return self.x`などすると無限ループになるのでご注意ください
        return super().__getattribute__(item)

    def __getitem__(self, item):
        # Dunder()[item]
        return self.__dict__.get(item)

    def __getnewargs__(self):
        # pickle.loads(pickle.dumps(Dunder())); unPickleする時に、`__new__`メソッドに渡される引数を定義できる
        # Python 3.6以前にpickle protocol 2または3を利用する時に使われる
        # Python 3.6以降にpickle protocol 2または3を利用する時に`__getnewargs_ex__`が使われる
        # 直接呼び出されるわけではなく、`__reduce__`メソッドを構成している
        return (2 * self.x, )

    def __getstate__(self):
        # pickle.dumps(Dunder()); Pickle処理する時に、オブジェクトの状態を取得できる
        # 直接呼び出されるわけではなく、`__reduce__`メソッドを構成している
        return self.__dict__.copy()

    def __gt__(self, other):
        # Dunder() > 123
        return self.x > 123

    def __hash__(self):
        # hash(Dunder()); ハッシュ値を計算する時に呼び出される
        return hash(self.x)

    def __iadd__(self, other):
        # dunder = Dunder(); dunder += 123; in-placeの加算をする時に呼び出される
        self.x += other
        return self

    def __iand__(self, other):
        # dunder = Dunder(); dunder &= 123; in-placeの論理積演算をする時に呼び出される
        self.x &= other
        return self

    def __ifloordiv__(self, other):
        # dunder = Dunder(); dunder //= 123; in-placeの切り捨て除算をする時に呼び出される
        self.x //= other
        return self

    def __ilshift__(self, other):
        # dunder = Dunder(); dunder <<= 123; in-placeのビット左シフトを計算する時に呼び出される
        self.x <<= other
        return self

    def __imatmul__(self, other):
        # dunder = Dunder(); dunder @= 123; in-placeのバイナリ演算をする時に呼び出される
        # numpyではドット積として実装している
        self.x @= other  # 標準ライブラリでは機能未実装
        return self

    def __imod__(self, other):
        # dunder = Dunder(); dunder %= 123; in-placeの剰余演算をする時に呼び出される
        self.x %= other
        return self

    def __imul__(self, other):
        # dunder = Dunder(); dunder *= 123; in-placeの乗算をする時に呼び出される
        self.x *= 123
        return self

    def __index__(self):
        # slice(Dunder(), Dunder() * 2); bin(Dunder()); hex(Dunder()); oct(Dunder())
        # operator.index(Dunder()); 戻り値は整数限定で、`operator.index`関数から呼び出される
        # また、整数を必要とする`slice`、`bin()`、`hex()`、`oct()`はこのメソッドを呼び出す
        return self.x

    def __init_subclass__(cls, **kwargs):
        # class Test(Dunder, **kwargs): ...; 継承される時に呼び出される
        super().__init_subclass__()
        cls.x = kwargs.get('x', 1)

    def __instancecheck__(self, instance):
        # class MetaClass(type):
        #     def __new__(cls, name, bases, namespace):
        #         return super().__new__(cls, name, bases, namespace)
        #
        #     def __instancecheck__(self, other):
        #         return True
        #
        # class Test(metaclass=MetaClass): ...
        # isinstance(int, Test) == True
        # このメソッドはクラスのタイプ(メタクラス)で定義しないと呼び出されない
        # また、`type(other) == self`の場合は直接Trueになり、呼び出されない
        pass

    def __int__(self):
        # int(Dunder()); 整数に変換する時に呼び出される
        return int(self.x)

    def __invert__(self):
        # ~Dunder(); ビット反転を計算する時に呼び出される
        return ~self.x

    def __ior__(self, other):
        # dunder = Dunder(); dunder |= 123; in-placeの論理和演算をする時に呼び出される
        self.x |= other
        return self

    def __ipow__(self, other):
        # dunder = Dunder(); dunder ** 2; in-placeの冪乗を計算する時に呼び出される
        self.x ** other
        return self

    def __irshift__(self, other):
        # dunder = Dunder(); dunder >>= 2; in-placeのビット右シフトを計算する時に呼び出される
        self.x >>= other
        return self

    def __isub__(self, other):
        # dunder = Dunder(); dunder -= 2; in-placeの減算をする時に呼び出される
        return self

    def __iter__(self):
        # dunder = iter(Dunder()); next(dunder); iterable objectを作成するためのメソッド
        # `__next__`と一緒に実装しなければならない
        self._i = 0
        return self.z[self._i]  # self.zはリストとして定義している

    def __itruediv__(self, other):
        # dunder = Dunder(); dunder /= 123; in-placeの除算をする時に呼び出される
        self.x /= other
        return self

    def __ixor__(self, other):
        # dunder = Dunder(); dunder ^= 123; in-placeの排他的論理和演算をする時に呼び出される
        self.x ^= other
        return self

    def __le__(self, other):
        # dunder = Dunder(); dunder <= 123
        return self.x <= other

    def __len__(self):
        # len(Dunder())
        return len(self.z)

    def __lshift__(self, other):
        # Dunder() << 123; ビット左シフトを計算する時に呼び出される
        return self.x << other

    def __lt__(self, other):
        # Dunder() < 123
        return self.x < other

    def __matmul__(self, other):
        # Dunder() @ 123; バイナリ演算をする時に呼び出される
        return self.x @ other  # 標準ライブラリでは機能未実装

    def __missing__(self, key):
        # class Dict(dict):
        #     def __missing__(self, key):
        #         return f'__missing__({key})'
        # dunder = Dict({'key': 1})
        # print(dunder['unk_key'])
        # 辞書内にキーが存在しない時に呼び出されるメソッド
        pass

    def __mod__(self, other):
        # Dunder() % 123; 剰余演算をする時に呼び出される
        return self.x % other

    def __mro_entries__(self, bases):
        # クラス定義の親リストにクラスオブ ジェクトではないものが指定された時に呼ばれる
        # 型アノテーションの実装で、継承関係を正しくするためのメソッド
        # https://www.python.org/dev/peps/pep-0560/#mro-entries
        pass

    def __mul__(self, other):
        # Dunder() * 123; 乗算をする時に呼び出される
        return self.x * ohter

    def __ne__(self, other):
        # Dunder() != 123; 不等価演算をする時に呼び出される
        return self.x != other

    def __neg__(self):
        # -Dunder(); 反数を計算する時に呼び出される
        return -self.x

    def __new__(cls, *args, **kwargs):
        # Dunder(); コンストラクタ
        # __init__や他のインスタンスメソッドで使われるself(インスタンスそのもの)を作成する
        return super().__new__(cls)

    def __next__(self):
        # dunder = iter(Dunder()); next(dunder); iterable objectを作成するためのメソッド
        # `__iter__`と一緒に実装しなければならない
        self._i += 1
        return self.z[self._i]

    def __or__(self, other):
        # Dunder() | 132; 論理和演算をする時に呼び出される
        return self.x | other

    def __pos__(self):
        # +Dunder(); 正数に変換する時に呼び出される
        return +self.x

    def __post_init__(self):
        # データクラス用のメソッドで、`__init__`が定義されている場合のみ、`__init__`の後に呼び出される
        pass

    def __pow__(self, power, modulo=None):
        # Dunder() ** 123; 冪乗を計算する時に呼び出される
        if modulo:
            return self.x ** power % modulo
        else:
            return self.x ** power

    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        # class MetaClass(type):
        #     def __new__(cls, name, bases, namespace):
        #         return super().__new__(cls, name, bases, namespace)
        #
        #     @classmethod
        #     def __prepare__(cls, name, bases, **kwargs):
        #         return dict()
        #
        # class Test(metaclass=MetaClass): ...
        # namespace = MetaClass.__prepare__(name, bases, **kwargs)
        # クラス本体を評価する前に呼び出されて、クラスメンバを格納する辞書形オブジェクト(名前空間)を返す
        # 通常`types.prepare_class`と一緒に使用する
        # このメソッドはメタクラスでクラスメソッドとして定義しないと呼び出されない
        return collections.OrderedDict()

    def __radd__(self, other):
        # 123 + Dunder(); 被演算子が反射した加算をする時に呼び出される
        return other + self.x

    def __rand__(self, other):
        # 123 & Dunder(); 被演算子が反射した論理積演算をする時に呼び出される
        return other & self.x

    def __rdiv__(self, other):
        # 123 / Dunder(); 被演算子が反射した除算をする時に呼び出される
        return other / self.x

    def __rdivmod__(self, other):
        # divmod(123, Dunder()); 被演算子が反射した割り算の商と余りを同時に取得
        return divmod(other, self.x)

    def __reduce__(self):
        # pickle.dumps(Dunder())
        # `__getstate__`、`__setstate__`、`__getnewargs__`を利用し、Pickleの挙動をコントロールできる
        # なるべく`__reduce__`を直接定義せず、上記のメソッドを定義するこること
        # 後方互換の`__reduce_ex__`が定義されると優先的に使用される
        return super().__reduce__() # return super().__reduce_ex__(protocol)

    def __repr__(self):
        # repr(Dunder()); オブジェクトの印字可能な表現を含む文字列を返す
        return super().__repr__()

    def __reversed__(self):
        # reversed(Dunder()); 反転したiterator objectを返す
        new_instance = copy.deepcopy(self)
        new_instance.z = new_instance.z[::-1]
        return new_instance

    def __rfloordiv__(self, other):
        # 123 // Dunder(); 被演算子が反射した切り捨て除算をする時に呼び出される
        return other // self.x

    def __rlshift__(self, other):
        # 123 << Dunder(); 被演算子が反射したビット左シフトを計算する時に呼び出される
        return '__rlshift__'

    def __rmatmul__(self, other):
        # 123 @ Dunder(); 被演算子が反射したバイナリ演算をする時に呼び出される
        return other @ self.x  # 標準ライブラリでは機能未実装

    def __rmod__(self, other):
        # 123 % Dunder(); 被演算子が反射した剰余演算をする時に呼び出される
        return other % self.x

    def __rmul__(self, other):
        # 123 * Dunder(); 被演算子が反射した乗算をする時に呼び出される
        return other * self.x

    def __ror__(self, other):
        # 123 | Dunder(); 被演算子が反射した論理和演算をする時に呼び出される
        return other | self.x

    def __round__(self, n=None):
       # round(Dunder()); 四捨五入
        return round(self.x)

    def __rpow__(self, other):
        # 123 ** Dunder(); 被演算子が反射した冪乗を計算する時に呼び出される
        return other ** self.x

    def __rrshift__(self, other):
        # 123 >> Dunder(); 被演算子が反射したビット右シフトを計算する時に呼び出される
        return other >> self.x

    def __rshift__(self, other):
        # Dunder() >> 123; ビット右シフトを計算する時に呼び出される
        return self.x >> other

    def __rsub__(self, other):
        # 123 - Dunder(); 被演算子が反射した減算をする時に呼び出される
        return other - self.x

    def __rtruediv__(self, other):
        # 123 / Dunder(); 被演算子が反射した除算をする時に呼び出される
        return other / self.x

    def __rxor__(self, other):
        # 123 ^ Dunder(); 被演算子が反射した排他的論理和を計算する時に呼び出される
        return other ^ self.x

    def __set__(self, instance, value):
        # class Test: dunder = Dunder(); ディスクリプタメソッド
        # `Test().dunder=123`または`Test.dunder=123`をする時にの時に呼び出される
        instance.x = value

    def __set_name__(self, owner, name):
        # ディスクリプタの変数名のアサイン
        # class Test: pass; オーナークラスが作成される時に自動的に呼び出されるが、
        # dunder = Dunder(); 後でバインディングする時は明示的に呼び出す必要がある
        # Test.dunder = dunder
        # dunder.__set_name__(Test, 'dunder')
        # dunderというディスクリプタをTestクラスの命名空間の'dunder'にアサインする
        owner.__dict__[name] = self

    def __setattr__(self, key, value):
        # dunder = Dunder(); dunder.x = 123; 属性設定する時に呼び出される
        self.__dict__[key] = value

    def __setitem__(self, key, value):
        # dunder = Dunder(); dunder['x'] = 123; ; 添字で属性を設定する時に呼び出される
        self.__dict__[key] = value

    def __setstate__(self, state):
        # pickle.loads(pickle.dumps(Dunder()))
        # unPickleする時に、`__getstate__`で取得しといたオブジェクトの状態を利用できる
        # 直接呼び出されるわけではなく、`__reduce__`メソッドを構成している
        self.__dict__.update(state)

    def __sizeof__(self):
        # sys.getsizeof(Dunder()); オブジェクトのサイズを返す
        return super().__sizeof__()

    def __str__(self):
        # str(Dunder())
        # print(Dunder())
        # オブジェクトの文字列表現を定義する
        return f'{self.x}'

    def __sub__(self, other):
        # Dunder() - 123; 減算をする時に呼び出される
        return self.x - other

    def __subclasscheck__(self, subclass):
        # class MetaClass(type):
        #     def __new__(cls, name, bases, namespace):
        #         return super().__new__(cls, name, bases, namespace)
        #
        #     def __subclasscheck__(self, subclass):
        #         return True
        #
        # class Test(metaclass=MetaClass): ...
        # issubclass(int, Test) == True
        # このメソッドはクラスのタイプ(メタクラス)で定義しないと呼び出されない
        return NotImplemented

    @classmethod
    def __subclasshook__(cls, subclass):
        # class Test: x = 1; # クラス変数を定義
        # issubclass(Test, Dunder) == True
        # このメソッドは仮想基底クラスのクラスメソッドとして定義しなければならない
        if cls is Dunder:
            return hasattr(subclass, 'x')

    def __truediv__(self, other):
        # Dunder() // 123; 切り捨て除算をする時に呼び出される
        return self.x // other

    def __trunc__(self):
        # math.trunc(Dunder()); 端数処理をする時に呼び出される
        return int(self.x)

    def __xor__(self, other):
        # Dunder() ^ 123; 排他的論理和演算をする時に呼び出される
        return self.x ^ other

上記のものは一般的な特殊メソッドです。全てを覚える必要はなく、こういうものもあったなぐらいでちょうど良いと思います。その他に、もう少し特殊な属性やメソッドも存在します。

属性 意味
__dict__ オブジェクトの (書き込み可能な) 属性を保存するために使われる辞書またはその他のマッピングオブジェクトです。ビルトイン関数vars()でその辞書を参照できます。
__class__ クラスインスタンスが属しているクラスです。
__bases__ クラスオブジェクトの基底クラス(親クラス)のタプルです。
__name__ クラス、関数、メソッド、デスクリプタ、ジェネレータインスタンスまたはモジュールの名前です。
__qualname__ クラス、関数、メソッド、デスクリプタ、ジェネレータインスタンスの修飾名です。
__mro__ この属性はメソッドの解決時に基底クラス(親クラス)を探索する時に考慮されるクラスのタプルです。
    mro() このメソッドは、メタクラスによって、そのインスタンスのメソッド解決の順序をカスタマイズするために、上書きされるかも知れません。このメソッドはクラスのインスタンス化時に呼ばれ、その結果は__mro__に格納されます。
__subclasses__ それぞれのクラスは、それ自身の直接のサブクラスへの弱参照を保持します。このメソッドはそれらの参照のうち、生存しているもののリストを返します。
__doc__ クラスや関数のドキュメンテーション文字列で、ドキュメンテーションがない場合はNoneになります。サブクラスに継承されません。
__module__ クラスや関数が定義されているモジュールの名前です。モジュール名がない場合はNoneになります。
__defaults__ デフォルト値を持つ引数に対するデフォルト値が収められたタプルで、デフォルト値を持つ引数がない場合にはNoneになります
__code__ コンパイルされた関数本体を表現するコードオブジェクトです。
__globals__ 関数のグローバル変数の入った辞書 (への参照) です --- この辞書は、関数が定義されているモジュールのグローバルな名前空間を決定します。
__closure__ Noneまたは関数の個々の自由変数 (引数以外の変数) に対して値を束縛しているセル(cell)群からなるタプルになります。セルオブジェクトは属性 cell_contents を持っています。 これはセルの値を設定するのに加えて、セルの値を得るのにも使えます。
__annotations__ 型アノテーション情報が入った辞書です。辞書のキーはパラメータ名で、返り値の注釈がある場合は、'return'がそのキーとなります。
__kwdefaults__ キーワード専用パラメータのデフォルト値を含む辞書です。
__slots__ このクラス変数には、インスタンスが用いる変数名を表す、文字列、イテラブル、または文字列のシーケンスを代入できます。slots は、各インスタンスに対して宣言された変数に必要な記憶領域を確保し、dictweakref が自動的に生成されないようにします。
__weakref__ 主にガベージコレクションのための属性で、弱参照を格納しています。
__func__ クラスメソッドが持つ属性で、メソッドの実体である関数オブジェクトを返します。
__self__ クラスメソッドが持つ属性で、自身の所属するオブジェクトを返します。
__isabstractmethod__ 抽象基底クラスにおいて、抽象メソッドかどうかを判断するための属性です。
__members__ 列挙型クラス専用の属性で、各要素を保存するために使われる辞書です。
__loader__ from package import *の時に、importすべきモジュール名をリストとして限定できます。
__package__ パッケージの場合は__name__に、パッケージじゃない場合はトップレベルのモジュールは空の文字列に、サブモジュールは親パッケージの__name__にすべきです。
__spec__ python -m <module> <file>の時に、パッケージやモジュールのスペック情報を格納する属性です。
__path__ importする時にモジュールを探す場所で、リストとして定義できます。__path__を定義すると、モジュールがパッケージになります。
__file__ モジュールの絶対パスを格納する変数です。
__cached__ .pycファイルとしたコンパイルされたパッケージのパスを格納する変数です。
__all__ from package import *の時に、importすべきモジュール名をリストとして限定できます。

(参照:特殊属性標準型の階層__slots__

表で示したものの一部は関数オブジェクトが所有する属性です。Pythonは全てがオブジェクトなので、関数もオブジェクトになり、第一級関数であるプログラミング言語です。その他に、モジュールに使われる属性もありますが、__init__.pyファイルの中に定義して使うことができます。また、上記の表で示したもの以外に、特定のモジュールに使われている属性もあります。

クラスのメンバを参照したい時は、vars()dir()が使えます。vars()はオブジェクトの__dict__属性を参照しますので、継承されたメンバは表示されません。それに対して、dir()はオブジェクトの__dir__メソッドを呼び出します。__dir__メソッドのデフォルトの実装はスコープ内にある名前を全部返すため、継承されたメンバも取得できます。そして、メンバの値も一緒に参照したい時はinspect.getmembers()が使えます。inspect.getmembers()はメンバとその値を格納したリストを返します。また、inspect.getmembers(obj, inspect.ismethod)で、メソッドだけ絞り込むこともできます。他にも、isから始まるinspectモジュールの関数がありまして、それらを使用して特定のメンバを取得できます。詳しくはドキュメントを参照してください。

5-2 タイプとオブジェクト

Pythonのtypeobjectは「鶏が先か、卵が先か」のような関係性を持っています。つまり、どれが先かははっきり説明できないです。そして、typeobjectは共生関係で、常に同時に出てきます。

まず、Pythonは「全てがオブジェクト」のプログラミング言語です。そして、3. オブジェクト指向に関する概念で紹介したように、オブジェクト指向の枠組みには主に以下の2種類の関係性が存在します。

  • 継承関係。子クラスは親クラスを継承し、is-aの関係性を作ります。例えば、reptileを継承したsnakeクラスがあるとして、「snake is a kind of reptile」と言えます。親クラスを参照したい時は、__base__が使用できます。
  • クラス・インスタンス関係。あるタイプのクラスをインスタンス化するとこの関係が生まれます。例えば、Squasherというsnakeのインスタンスを作ることができ、「Squasher is an instance of snake」と言えます。ここのsnakeSquasherのタイプクラスと定義します。インスタンスのタイプクラスを参照したい時は、__class__か、type()関数が使用できます。

この2種類の関係性を図で表すと以下のようになります。
                                                  image.png

次に、typeobjectを見てみます。

print(object)
# 実行結果:<class 'object'>
print(type)
# 実行結果:<class 'type'>

Pythonの世界では、objectは継承関係の頂点であり、全てのクラスの親クラスになります。それに対して、typeはクラス・インスタンス関係の頂点で、全てのオブジェクトのタイプクラスになります。2者の関係性を「object is an instance of type」と表現できます。

print(object.__class__)
# 実行結果:<class 'type'>
print(object.__bases__)  # 継承関係の頂点なので、それ以上は存在しない
# 実行結果:()
print(type.__class__)  # type自身もtypeのインスタンス
# 実行結果:<class 'type'>
print(type.__bases__)
# 実行結果:(<class 'object'>,)

続いて、listdicttupleなどのビルトインデータクラスについて見てみます。

print(list.__bases__)
# 実行結果:(<class 'object'>,)
print(list.__class__)
# 実行結果:<class 'type'>
print(dict.__bases__)
# 実行結果:(<class 'object'>,)
print(dict.__class__)
# 実行結果:<class 'type'>
print(tuple.__bases__)
# 実行結果:(<class 'object'>,)
print(tuple.__class__)
# 実行結果:<class 'type'>

同じく、親クラスはobjectで、typeのインスタンスになります。listをインスタンス化して検証してみましょう。

mylist = [1, 2, 3]
print(mylist.__class__)
# 実行結果:<class 'list'>
print(mylist.__bases__)
# 実行結果:
# ---------------------------------------------------------------------------
# AttributeError                            Traceback (most recent call last)
# <ipython-input-21-0b850541e51b> in <module>
# ----> 1 print(mylist.__bases__)
#
# AttributeError: 'list' object has no attribute '__bases__'

インスタンス化したlistには親クラスがないらしいです。次に、自分でクラスを定義して、そのインスタンスについて見てみましょう。

class C:  # Python3ではクラスはデフォルトでobjectを継承する
    pass


print(C.__bases__)
# 実行結果:(<class 'object'>,)
c = C()
print(c.__class__)
# 実行結果:<class '__main__.C'>
print(c.__bases__)
# 実行結果:
# ---------------------------------------------------------------------------
# AttributeError                            Traceback (most recent call last)
# <ipython-input-30-bf9b854689d5> in <module>
# ----> 1 print(c.__bases__)
#
# AttributeError: 'C' object has no attribute '__bases__'

ここのCクラスのインスタンスにも親クラスが存在しません。

ここまでの各種の関係性を図にすると以下のようになります。ここでは、実線は継承関係を表し、矢印は親クラスを指します。点線はクラス・インスタンス関係を表し、矢印はインスタンスのタイプクラスを指します。
Screen Shot 2020-11-02 at 22.24.34.png

上記の検証から、以下の結果に辿り着きました。

  • 全てのobjecttypeのインスタンスです。
  • typeの直属のインスタンスはobjectobjectを継承したクラスで、これらはPythonのオブジェクト指向においての「クラス」です。
  • typeの直属のインスタンス、つまり「クラス」の更なるインスタンスは__bases__を持たないクラスで、これらはPythonのオブジェクト指向においての「インスタンス」です。

では、typeを継承したクラスはどんなものになるでしょうか?

class M(type):
    pass


print(M.__class__)
# 実行結果:<class 'type'>
print(M.__bases__)
# 実行結果:(<class 'type'>,)

ここのMクラスのタイプクラスも親クラスもtypeです。上記の図のルールでは、1列目に置くべきですね。しかし、Mのインスタンスはどこに置くべきでしょうか?

class TM(metaclass=M):
    pass


print(TM.__class__)
# 実行結果:<class '__main__.M'>
print(TM.__bases__)
# 実行結果:(<class 'object'>,)

実はこのMメタクラスというクラスのクラスです。メタクラスMから作成したTMは上記の図の2列目の「クラス」に所属するでしょう。メタクラスの使い方に関しては後でまた紹介します。

  • typeは全てのメタクラスの親で、typeを継承してメタクラスを作成できます。
  • objectは全ての「クラス」の親で、ほとんどのビルトインデータクラスはこの「クラス」です。
  • 「クラス」をインスタンス化して作られたのは「インスタンス」で、継承やインスタンス化に使用できません。

なぜPythonにはtypeobject両方必要だろうと思うかもしれません。例えば、typeがないと、上の図は2列になり、1列目が「タイプクラス」、2列目が「インスタンス」になります。静的オブジェクト指向プログラミング言語は大体この2列の構造です。Pythonが3列構造になったのは、ランタイムでクラスを動的に作成するためです。2列目のobjectはただtypeのインスタンスなので、ランタイムでメソッドや属性を変更できるわけです。この性質を実現するために、3列構造が必要になります。

5-3. メタクラス

5-3-1. クラスはオブジェクト

Pythonのクラスはsmalltalkから拝借したものです。殆どのオブジェクト指向プログラミング言語では、クラスというのは「オブジェクトをどう生成するか」を記述したコードになります。

class ObjectCreater:
    pass


my_object = ObjectCreater()
print(my_object)
# 実行結果:<__main__.ObjectCreater object at 0x7fbc76f9a970>

しかし、繰り返しにはなりますが、Pythonのクラスはクラスであると同時に、オブジェクトでもあります。class予約語を実行する時に、Pythonはメモリ上オブジェクトを作成します。上記のコードでは、ObjectCreaterというオブジェクトが作成されました。この「クラス」オブジェクトは「インスタンス」オブジェクトを作成することができます。これが「クラス」の役割です。そして、オブジェクトであるため、ObjectCreaterに対して以下の操作が可能です。

  • 他の変数に代入する
  • コピーする
  • 属性を増やす
  • 引数として関数に渡す
class ObjectCreator:
    pass


def echo(obj):
    print(obj)


echo(ObjectCreator)  # 引数として渡す

ObjectCreator.new_attr = 'foo'  # 属性を増やす
assert hasattr(ObjectCreator, 'new_attr') == True

ObjectCreatorMirror = ObjectCreator  # 他の変数に代入する

5-3-2. クラスの動的作成

クラスもオブジェクトなので、ランタイムでの作成は他のオブジェクトと同じくできるはずです。まず、class予約語を使って、クラスを作成する関数を作ってみます。

def choose_class(name):
    if name == 'foo':
        class Foo:
            pass
        return Foo
    else:
        class Bar:
            pass
        return Bar


MyClass = choose_class('foo')
print(MyClass)
print(MyClass())

実行結果:

<class '__main__.choose_class.<locals>.Foo'>
<__main__.choose_class.<locals>.Foo object at 0x7fad2abc8340>

クラスを条件分岐で作成できました。しかし、この方法はそれほど「動的」とは言えないですね。クラスもオブジェクトなら、クラスを作る何かがあるはずです。実は、その「何か」が5.2 タイプとオブジェクトで紹介したtypeです。

殆どの人は使ったことがあると思いますが、Pythonにはtypeという関数があります。

print(type(1))
# 実行結果:<class 'int'>
print(type('1'))
# 実行結果:<class 'str'>
print(type(ObjectCreatorMirror))
# 実行結果:<class 'type'>

しかし、typeにはもう1つの機能があります。それは、ランタイムでクラスを作成するという機能です。なぜ1つの関数に2つの機能があるかというと、3-1. クラスで紹介したようにPython 2には、typeを継承した古いクラスが存在します。その後方互換のために、typeに2つの機能を持たせました。

MyShinyClass = type("MyShinyClass", (), {})
print(MyShinyClass)
print(MyShinyClass())

実行結果:

<class '__main__.MyShinyClass'>
<__main__.MyShinyClass object at 0x7f9cd02bddc0>

typeでクラスを作る時に、3つの引数が必要です。

  • クラス名
  • クラスが継承するクラスのタプル
  • クラスの属性を格納する辞書型オブジェクト(名前空間)

次に、typeの使い方をもう少し見ていきます。

class Foo:
    bar = True

    def echo_bar(self):
        print(self.bar)

上記と同じ構造のクラスをtypeで作ると以下のようになります。

def echo_bar(self):
    print(self.bar)


Foo = type('Foo', (), {'bar': True, 'echo_bar': echo_bar})

継承関係のあるクラスを作成します。

class FooChild(Foo):
    pass

typeで作ると以下のようになります。

FooChild = type('FooChild', (Foo, ), {})

5-3-3. メタクラスの定義

前述のように、メタクラスはクラスのクラスで、クラスを作るクラスになります。「typeは全てのメタクラスの親で、typeを継承してメタクラスを作成できます。」というのを説明しましたが、実はtype自身もメタクラスです。メタクラス、クラス、インスタンスの関係性は以下の図のようになります。
                     image.png

type関数は特殊なメタクラスです。実はclassを使ってクラスを作成する時に、Pythonは裏でtypeを使っています。そのため、全てのobjecttypeのインスタンスになるわけです。

x = 30
print(x.__class__)
# 実行結果:<class 'int'>
print(x.__class__.__class__)
# 実行結果:<class 'type'>

typeはビルトインのメタクラスです。メタクラスの自作については5-2. タイプとオブジェクトでも説明しましたが、以下のようになります。

class Meta(type):
    pass


class Foo(metaclass=Meta):
    pass

5-3-4. メタクラスの使い方

メタクラスを使う目的は、クラスの作成時に、自動的に何らかのカスタマイズをすることです。例えば、あるモジュールにおいて、全てのクラスの属性名を大文字にしたい時に、このようなメタクラスが作れます。

class UpperAttrMetaClass(type):
    # __new__はインスタンスselfを作成するコンストラクタ
    # __init__は作成されたインスタンスselfを初期化するイニシャライザ
    def __new__(cls, new_class_name,
                new_class_parents, new_class_attr):
        uppercase_attr = {}
        for name, val in new_class_attr.items():
            # 特殊メソッドを除く
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val
        return type.__new__(cls, new_class_name, new_class_parents, new_class_attr)
        # 下の書き方と同様
        # return super().__new__(cls, new_class_name, new_class_parents, new_class_attr)

メタクラスはデータ型のチェックや継承のコントロールなどに使うことができます。メタクラスを導入すると、コードがやや複雑になるかもしれませんが、メタクラスの役割自体はシンプルです。クラスのデフォルトの作成過程に割り込み、修正を加え、修正後のクラスを返すだけです。

また、Pythonの標準ライブラリにはtypesというモジュールがあり、メタクラスやクラス生成に関する関数が提供されています。

types.prepare_class(name, bases=(), kwds=None)はこれから作る新しいクラスの適切なメタクラスを選ぶ関数です。関数の戻り値はmetaclass, namespace, kwdsのタプルになります。types.new_class(name, bases=(), kwds=None, exec_body=None)は新しいクラスを作成する関数で、exec_bodyは新しいクラスの名前空間を構築するためのコールバック関数を受け取ります。例えば、exec_body=lambda ns: collections.OrderedDict()で順序付き辞書を使って名前空間を構築できます(Python 3.6以降は不要)。

import types


class A(type):
    expected_ns = {}

    def __new__(cls, name, bases, namespace):
        return type.__new__(cls, name, bases, namespace)

    @classmethod
    def __prepare__(cls, name, bases, **kwargs):
        expected_ns.update(kwargs)
        return expected_ns


B = types.new_class("B", (object,))
C = types.new_class("C", (object,), {"metaclass": A})

# メタクラスの継承チェーンの1番下はtypeではなく、Aになる
meta, ns, kwds = types.prepare_class("D", (B, C), {"metaclass": type, 'x': 1})

assert meta is A  # 継承チェーンの1番下にあるメタクラスAが選択されたのが確認できる
assert ns is expected_ns  # Aの__prepare__が使用されているのが確認できる
print(kwds)  # metaclassキーワード引数が削除されたのが確認できる(適切なメタクラスを戻り値として返したため)
# 実行結果:{'x': 1}

メタクラスの実用例として、ORMが挙げられます。ここでは、DjangoのORMを例として見てみましょう。

from django.db import models


class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()


guy = Person.objects.get(name='bob')
print(guy.age)  # output is 35

DjangoのORMは上記のように非常に簡単に使えます。Djangoはメタクラスを使用して、データベースの複雑なクエリなどを実現しています。後でORMの実装例も紹介しますので、Django ORMの詳細はdjango.db.models.base.ModelBaseを参照してください。

5-4. ディスクリプタ

5-4-1. ディスクリプタの基本

4-2. プロパティのところで、propertyデコレーターについて見てきました。propertyはメソッドをインスタンス変数のようにするだけではなく、値の代入などに対してチェックを行うこともできます。

class Student:
    def __init__(self, name, score):
        self.name = name
        self._score = score

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            print('Please input an int')
            return
        self._score = value

propertyデコレーターでの値チェックには2つの問題点があります。

  • 変数に対して、propertyx.setterデコレーターをそれぞれ適用する必要があり、変数が多いととコードが冗長になります。
  • 初期化の時点でのチェックができません。

ディスクリプタ(Descriptor)はこの問題を解決するためのソリューションです。ディスクリプタはオブジェクトの属性の参照、保存と削除をカスタマイズするためのものです。クラスの中に、__get____set____delete__のどれか1つを実装すればそれがディスクリプタになります。使用する時はがディスクリプタを所有クラスのクラス変数として定義しなければなりません。

ディスクリプタは以下の2種類があります。

  • __get__のみ実装したクラスはノンデータディスクリプタ(non-data descriptor)と言います。
  • __get____set__を実装したクラスはデータディスクリプタ(data descriptor)と言います。

5-4-2. ノンデータディスクリプタとデータディスクリプタ

ディレクトリーのファイル数を取得する簡単なディスクリプタを作ってみまょう。

import os


class DirectorySize:
    def __get__(self, instance, owner):
        return len(os.listdir(instance.dirname))


class Directory:
    size = DirectorySize()  # ディスクリプタ

    def __init__(self, dirname):
        self.dirname = dirname


debug = Directory('debug')
print(debug.size)

ディスクリプタメソッド__get__self以外に、自身を所有するクラスownerとそのインスタンスinstanceの2つの引数を受け取ります。Directoryクラスの中でディスクリプタDirectorySizeをインスタンス化し、クラス変数sizeに入れます。そして、sizeを呼び出すと、DirectorySize__get__メソッドが呼び出されます。

print(vars(debug))
# 実行結果:{'dirname': 'debug'}

上記のコードを見れば分かりますが、ノンデータディスクリプタはインスタンスの名前空間には存在しません。

次に、__get____set__を実装したデータディスクリプタを実装してみます。

import logging


logging.basicConfig(level=logging.INFO)


class LoggedAgeAccess:
    def __get__(self, instance, owner):
        value = instance._age
        logging.info('Accessing %r giving %r', 'age', value)
        return value

    def __set__(self, instance, value):
        logging.info('Updating %r to %r', 'age', value)
        instance._age = value


class Person:
    age = LoggedAgeAccess()  # ディスクリプタ

    def __init__(self, name, age):
        self.name = name  # 普通のインスタンス変数
        self.age = age  # ディスクリプタを呼び出す

    def birthday(self):
        self.age += 1  # __get__と__set__両方が呼び出される


mary = Person('Mary M', 30)
mary.age
mary.birthday()

実行結果:

INFO:root:Updating 'age' to 30
INFO:root:Accessing 'age' giving 30
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31

ディスクリプタメソッド__set__は、所有クラスのインスタンスinstanceとディスクリプタに代入する値valueを受け取ります。ディスクリプタに値を代入すると__set__メソッドが呼び出されます。そして、__init__の初期化の時も同じです。

print(vars(mary))
# 実行結果:{'name': 'Mary M', '_age': 31}

データディスクリプタで、インスタンス変数に値を代入すると、名前空間に現れます。

ディスクリプタには、__set_name__というメソッドがあります。ディスクリプタにアサインされた変数名(上の例ではage)を取得でき、修正を加えることもできます。下記の例はメタクラスと__set_name__を使用したデータディスクリプタで実装したシンプルなORMです。

import sqlite3


conn = sqlite3.connect('entertainment.db')


class MetaModel(type):
    def __new__(cls, clsname, bases, attrs):
        table = attrs.get('table')
        if table:
            col_names = [k for k, v in attrs.items() if type(v) == Field]
            # ダミーのデータ型を付与
            col_names_with_type = [f'{c} {attrs[c].datatype} PRIMARY KEY' if attrs[c].is_primary_key
                                   else f'{c} {attrs[c].datatype}'
                                   for c in col_names]
            # テーブルの作成
            create_table = f"CREATE TABLE IF NOT EXISTS {table} ({', '.join(col_names_with_type)});"
            conn.execute(create_table)
            conn.commit()
            attrs['_columns_'] = col_names  # 各モデルのカラム名を格納

        return super().__new__(cls, clsname, bases, attrs)


class Model(metaclass=MetaModel):
    def __init__(self, *col_vals):
        self.col_vals = col_vals  # レコードの各カラムの値を格納
        cols = self._columns_
        table = self.table
        pk = self.primary_key
        pk_val = self.primary_key_value = col_vals[cols.index(pk)]
        record = conn.execute(f'SELECT * FROM {table} WHERE {pk}=?;',
                              (pk_val,)).fetchone()
        if not record:
            params = ', '.join(f':{c}' for c in cols)
            conn.execute(f"INSERT INTO {table} VALUES ({params});", col_vals)
            conn.commit()
        else:
            params = ', '.join(f"{c}=?" for c in cols)
            update_col_vals = col_vals + (pk_val,)
            conn.execute(f"UPDATE {table} SET {params} WHERE {pk}=?;", update_col_vals)


class Field:
    def __init__(self, datatype, primary_key=False):
        self.datatype = datatype
        self.is_primary_key = primary_key

    def __set_name__(self, owner, name):
        if self.is_primary_key:
            owner.primary_key = name
        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.primary_key}=?;'
        self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.primary_key}=?;'

    def __get__(self, instance, owner):
        return conn.execute(self.fetch, [instance.primary_key_value]).fetchone()[0]

    def __set__(self, instance, value):
        conn.execute(self.store, [value, instance.primary_key_value])
        conn.commit()
        if self.is_primary_key:
            instance.primary_key_value = value


class MovieModel(Model):
    table = 'Movie'
    title = Field(datatype='TEXT', primary_key=True)
    director = Field(datatype='TEXT')
    year = Field(datatype='INTEGER')


class MusicModel(Model):
    table = 'Music'
    title = Field(datatype='TEXT', primary_key=True)
    artist = Field(datatype='TEXT')
    year = Field(datatype='INTEGER')
    genre = Field(datatype='TEXT')


star_wars = MovieModel('Star Wars', 'George Lucas', 1977)
print(f'{star_wars.title} released in {star_wars.year} by {star_wars.director}')
star_wars.director = 'J.J. Abrams'
print(star_wars.director)
country_roads = MusicModel('Country Roads', 'John Denver', 1973, 'country')
print(f'{country_roads.title} is a {country_roads.genre} song of {country_roads.artist}')

実行結果:

Star Wars released in 1977 by George Lucas
J.J. Abrams
Country Roads is a country song of John Denver

このように、メタクラスとデータディスクリプタを組み合わせればORMも簡単に実装できます。もちろん両方とも使わなければならないという制約はなく、例えばDjangoのFieldはディスクリプタを使用していません。実際のORMはできる限りDBとの通信回数を減らすように、アプリケーション層での型評価やキャッシュなどもっと複雑な機能が実装されています。

5-4-3. ディスクリプタの仕組み

5-1. 特殊メソッドで、__getattribute__について触れました。__getattribute__は「未定義・定義済みにかかわらず、全てのクラスメンバーにアクセスする時に呼び出される」の機能を持っているメソッドで、ディスクリプタを使用したクラスに対して、b.xのような呼び出しをtype(b).__dict__['x'].__get__(b, type(b))のような処理に置き換えています。

class Descriptor:
    def __get__(self, instance, owner):
        return self._x

    def __set__(self, instance, value):
        self._x = value


class B:
    x = Descriptor()

    def __init__(self, x):
        self.x = x

    def __getattribute__(self, key):
        attr = type(self).__dict__[key]
        if hasattr(attr, '__get__'):
            return attr.__get__(self, type(self))
        else:
            return attr

そのため、__getattribute__をカスタマイズすると、ディスクリプタが使えなくなります。そして、当たり前かもしれませんが、__get____set__を実装したデータディスクリプタは、変数の代入にチェックを入れるので、常にインスタンス変数を上書きします。上記の例では、b = B(1); b.x = 2にしても、b.xはディスクリプタのままです。それに対して、__get__だけ実装したノンデータディスクリプタは変数の代入をチェックしないので、クラス変数を直接更新すれば、ディスクリプタが上書きされます。

5-4-4. ディスクリプタの使い方

実はディスクリプタを使って、4. pythonのオブジェクト指向の基本で紹介したpropertyclassmethodstaticmethodデコレーターと同じ機能を実現できます。

5-4-4-1. property

propertyは以下のように実装できます。

class Property:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

上記のディスクリプタをデコレーター@Propertyの形でsomemethodに使う時は、実はsomemethod = Property(somemethod)の処理になります。そして、Propertyself.fgetに第一引数のfgetを代入し、インスタンスを作ります。次に、@somemethod.setterで作成済みのPropertyインスタンスのsetterメソッドで、fsetをインスタンス引数に代入します。続いて、 @somemethod.deleterで同じく、fdelをインスタンスに代入できます。この流れは4-2. プロパティと同じですね。

5-4-4-2. methodとfunction

4-1. クラスの変数とメソッドMethodTypeを簡単に紹介しました。同じ機能をPythonコードで実装すると、以下のようになります。

class MethodType:
    def __init__(self, func, obj):
        self.__func__ = func
        self.__self__ = obj

    def __call__(self, *args, **kwargs):
        func = self.__func__
        obj = self.__self__
        return func(obj, *args, **kwargs)

そして、クラスの内部で関数をメソッドに変えるディスクリプタはこんな感じで作成できます。

class Function:
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return MethodType(self, obj)

5-1. 特殊メソッドで、「instance.method.__func__はメソッドの実体である関数オブジェクトを返す」を説明しました。しかし、instance.methodでアクセスすると、メソッドオブジェクトが返ってきます。この挙動は上記のディスクリプタでシミュレートできます。

5-4-4-3. classmethodとstaticmethod

この2つのデコレーターは上記のMethodTypeを使って、非常に簡単に実現できます。まず、classmethodは以下のように実装できます。

class ClassMethod:
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        if hasattr(obj, '__get__'):
            return self.f.__get__(cls)
        return MethodType(self.f, cls)

@ClassMethodの形で使用すると、somemethod = ClassMethod(somemethod)になり、somemethodをインスタンスではなく、クラスにバインディングできます。

次に、staticmethodについて見ていきます。

class StaticMethod:
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

Pythonの静的メソッドstaticmethodの実体は普通の関数です。上記のStaticMethod@StaticMethodの形で使用すると、同じくsomemethod = StaticMethod(somemethod)になり、単純にディスクリプタのインスタンス変数self.fに関数somemethodを保存し、クラスやインスタンスにバインディングされるのを阻止します。そして、呼び出された時はself.fをそのまま返します。

5-4-4-5. types.DynamicClassAttribute

あまり知られてないですが、Pythonの標準ライブラリにはtypes.DynamicClassAttributeというディスクリプタがあります。使い方は、propertyと同じです。このディスクリプタはインスタンスからアクセス時は普通のpropertyと全く一緒で、クラスからアクセスする時のみ機能が変わります。クラスからアクセスすると、クラスの__getattr__メソッドに、__getattr__が定義されてない時は、メタクラスの__getattr__に振り替えられます。

from types import DynamicClassAttribute


class EnumMeta(type):
    def __new__(cls, name, bases, namespace):
        reserved_names = ('name', 'value', 'values')
        enum_namespace = namespace.copy()
        enum_namespace['_member_map_'] = {}
        enum_namespace['_member_map_']['values'] = []
        for k, v in namespace.items():
            if not (k in reserved_names or k.startswith('_')):
                member_namespace = namespace.copy()
                member_namespace.update({"_name_": k, "_value_": v})
                member_cls = super().__new__(cls, name, bases, member_namespace)
                enum_namespace['_member_map_']['values'].append(v)
                enum_namespace['_member_map_'][k] = member_cls()
                enum_namespace[k] = enum_namespace['_member_map_'][k]

        return super().__new__(cls, name, bases, enum_namespace)

    def __getattr__(self, item):
        return self._member_map_[item]


class Enum(metaclass=EnumMeta):
    @DynamicClassAttribute
    def name(self):
        return self._name_

    @DynamicClassAttribute
    def value(self):
        return self._value_

    @DynamicClassAttribute
    def values(self):
        return self._values_


class Color(Enum):
    red = 1
    blue = 2
    green = 3


print(Color.red.value)
# 実行結果:1
Color.red._values_ = [1]
print(Color.red.values)  # インスタンスのvalues
# 実行結果:[1]
print(Color.values)  # クラスのvalues
# 実行結果:[1, 2, 3]

上記は自作の簡易版列挙型です。列挙型については後で詳しく紹介します。ここのEnumクラスの各クラス変数はメタクラスEnumMetaによってEnumのインスタンスに変換されました。そして、types.DynamicClassAttributeによって、クラスのvaluesとインスタンスのvaluesはお互い干渉せずに共存できました。このように、クラスとインスタンスに違う動作を実現させたい時はtypes.DynamicClassAttributeを使用すると楽です。

5-4-4-5. __slots__

Pythonには特殊な属性__slots__が存在し、既存クラスに対して、モンキーパッチでの新しい属性の追加を阻止できます。使い方としては以下のようになります。

class Student:
    __slots__ = ('name', 'age')


student = Student()

student.name = 'Mike'
student.age = 20

student.grade = 'A'
# 実行結果:
# Traceback (most recent call last):
#   File "oop.py", line 10, in <module>
#     student.grade = 'A'
# AttributeError: 'Student' object has no attribute 'grade'

公式ドキュメントによると、この機能も実はディスクリプタによって実現されたのです。ここでは実装しませんが、メタクラスとディスクリプタを組み合わせることでこのような機能も実現できます。

5-5. 抽象基底クラス

3-18-7. 純粋人工物で少し触れた概念ではあるが、抽象基底クラスはオブジェクト指向プログラミングにおいて非常に強力な武器です。抽象基底クラスを使って、クラスがある特定のインターフェースを提供しているかを判定することができます。Pythonの標準ライブラリにはabcという抽象基底クラスに必要なツールを提供するモジュールがあり、それを使えば抽象基底クラス、抽象メソッドなどを作成できます。

5-5-1. インターフェース

3. オブジェクト指向に関する概念で「インターフェース」という用語を何度も繰り返しました。インターフェースはソフトウェア工学分野において非常に重要な概念です。よく知られているインターフェースとして、API (Application Programming Interface)が挙げられます。他に、最近「マイクロサービス」で多用されている「gRPC」は本質で言うと、インタフェースを「Protocol Buffers」という「インターフェース定義言語」で定義し、サーバー側とクライアント側はそれぞれそのインタフェースを実装することで、相互通信を実現したものです。また、オブジェクト指向においてのインターフェースはオブジェクトレベルのものを指します。

しかし、JavaやC++と違って、Pythonにはビルトインのインターフェースクラスは存在しません。Pythonでインターフェースと同じ機能を実現するためにはいくつかの方法があります。

5-5-2. 仮想基底クラスによるインターフェース

仮想基底クラスは明示的な継承関係がないにも関わらず、インターフェースに制約をかけられるクラスです。Pythonではメタクラスを利用して、仮想基底クラスを実現できます。

class RESTAPIMeta(type):
    def __instancecheck__(cls, instance):
        return cls.__subclasscheck__(type(instance))

    def __subclasscheck__(cls, subclass):
        return (hasattr(subclass, 'get') and
                callable(subclass.get) and
                hasattr(subclass, 'post') and
                callable(subclass.post))


class RESTAPIInterface(metaclass=RESTAPIMeta):
    ...


class ItemList:
    def get(self, id):
        pass

    def post(self, id):
        pass


class UserList:
    def get(self, id):
        pass


print(issubclass(ItemList, RestAPIInterface))
# 実行結果:True
print(ItemList.__mro__)
# 実行結果:(<class '__main__.ItemList'>, <class 'object'>)
print(issubclass(UserList, RestAPIInterface))
# 実行結果:False

上記はRESTAPIを定義する仮想基底クラスの例です。メタクラスRESTAPIMeta__subclasscheck__で、getpostメソッドを持つクラスを子クラスとして判定するよう実装しました。これで、明示的な継承をせずに、クラスのインターフェースに対してある程度の制約をかけることができます。

5-5-3. 抽象基底クラスによるインターフェース

abcモジュールを使って、上記の仮想基底クラスを実装してみましょう。抽象基底クラスはclass MyClass(abc.ABC)またはclass MyClass(metaclass=abc.ABCMeta)の形式で作成できます。

import abc


class RestAPIInterface(metaclass=abc.ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        return (hasattr(subclass, 'get') and
                callable(subclass.get) and
                hasattr(subclass, 'post') and
                callable(subclass.post))


class ItemList:
    def get(self, id):
        pass

    def post(self, id):
        pass


class UserList:
    def get(self, id):
        pass


print(issubclass(ItemList, RestAPIInterface))
# 実行結果:True
print(ItemList.__mro__)
# 実行結果:(<class '__main__.ItemList'>, <class 'object'>)
print(issubclass(UserList, RestAPIInterface))
# 実行結果:False

__subclasshook__メソッドはabc.ABCMetaから作られたインスタンスクラスのクラスメソッドとして実装することで、issubclassを呼び出すとhookとして機能します。

それから、ABCMeta.registerを使うと、仮想サブクラスを登録することもできます。

import abc


class RestAPIInterface(metaclass=abc.ABCMeta):
    ...


class UserList:
    def get(self, id):
        pass


RestAPIInterface.register(UserList)
print(issubclass(UserList, RestAPIInterface))
# 実行結果:True

デコレーターとして使うこともできます。

import abc


class RestAPIInterface(metaclass=abc.ABCMeta):
    ...


@RestAPIInterface.register
class UserList:
    def get(self, id):
        pass


print(issubclass(UserList, RestAPIInterface))
# 実行結果:True

また、abc.get_cache_token()で現在の抽象基底クラスのキャッシュトークンを取得できます。このトークンはABCMeta.registerが実行される度に変更されるので、等価性検証に使えます。

import abc


class RestAPIInterface(metaclass=abc.ABCMeta):
    ...


class UserList:
    def get(self, id):
        pass


token_old = abc.get_cache_token()
RestAPIInterface.register(UserList)
token_new = abc.get_cache_token()

print(f'{token_old} >>> {token_new}')
# 実行結果:36 >>> 37

5-5-4. 抽象メソッド

これまでのインターフェイスはあくまでも仮想基底クラスなので、継承関係がなく、子クラスに対しての制限も弱いです。特定のインターフェースを実装しないと、エラーを起こす機能を実現したい時は仮想基底クラスではなく、抽象基底クラスと抽象メソッドを合わせて使う必要があります。

import abc


class RestAPIInterface(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def get(self, id):
        raise NotImplementedError

    @abc.abstractmethod
    def post(self, id):
        raise NotImplementedError


class ItemList(RestAPIInterface):
    def get(self, id):
        pass

    def post(self, id):
        pass


class UserList(RestAPIInterface):
    def get(self, id):
        pass


item_list = ItemList()
user_list = UserList()

実行結果:

Traceback (most recent call last):
  File "resource.py", line 29, in <module>
    user_list = UserList()
TypeError: Can't instantiate abstract class UserList with abstract methods post

また、abc.abstractmethodclassmethodstaticmethodpropertyなどと併用することができます。

import abc


class Model(abc.ABC):
    @classmethod
    @abc.abstractmethod
    def select_all(cls):
        ...

    @staticmethod
    @abc.abstractmethod
    def show_db_name(age):
        ...

    @property
    @abc.abstractmethod
    def row_id(self):
        ...

5-4-4. ディスクリプタの使い方で紹介したようなデコレーターの形で使うディスクリプタと一緒に使う時に、ディスクリプタの__isabstractmethod__を実装することで、abc.abstractmethodと併用できるようになります。

import abc


class StaticMethod:
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

    @property
    def __isabstractmethod__(self):
        return getattr(self.f, '__isabstractmethod__', False)


class Model(abc.ABC):
    @StaticMethod
    @abc.abstractmethod
    def show_db_name():
        ...


class ItemModel(Model):
    pass


item_model = ItemModel()
# 実行結果:
# Traceback (most recent call last):
#   File "oop.py", line 27, in <module>
#     item_model = ItemModel()
# TypeError: Can't instantiate abstract class ItemModel with abstract methods show_db_name

そして、Pythonの抽象メソッドはただのインターフェースではなく、superで継承して、メソッドの中身を獲得できます。

5-5-5. コンテナの抽象基底クラス

標準ライブラリのcollections.abcにはPythonビルトインデータ構造(コンテナ)の抽象基底クラスを提供しています。

ABC 継承しているクラス 抽象メソッド mixinメソッド
Container __contains__
Hashable __hash__
Iterable __iter__
Iterator Iterable __next__ __iter__
Reversible Iterable __reversed__
Generator Iterator send、throw close、__iter____next__
Sized __len__
Callable __call__
Collection Sized、Iterable、
Container
__contains__
__iter____len__
Sequence Reversible、
Collection
__getitem__
__len__
__contains____iter____reversed__
index、count
MutableSequence Sequence __getitem__
__setitem__
__delitem__
__len__、insert
Sequenceから継承したメソッドと、
append、reverse、extend、pop、remove、
__iadd__
ByteString Sequence __getitem__
__len__
Sequenceから継承したメソッド
Set Collection __contains__
__iter____len__
__le____lt____eq____ne____gt__
__ge____and____or____sub____xor__
isdisjoint
MutableSet Set __contains__
__iter____len__
add、discard
Setから継承したメソッドと、clear、pop、
remove、__ior____iand____ixor__
__isub__
Mapping Collection __getitem__
__iter____len__
__contains__、keys、items、values、get、
__eq____ne__
MutableMapping Mapping __getitem__
__setitem__
__delitem__
__iter____len__
Mappingから継承したメソッドと、pop、
popitem、clear、update、setdefault
MappingView Sized __len__
ItemView MappingView、Set __contains____iter__
KeysView MappingView、Set __contains____iter__
ValuesView MappingView、
Collection
__contains____iter__
Awaitable __await__
Coroutine Awaitable send、throw close
AsyncIterable __aiter__
AsyncIterator AsyncIterable __anext__ __aiter__
AsyncGenerator AsyncIterator asend、athrow aclose、__aiter____anext__
(参照:collections.abc — Abstract Base Classes for Containers

使い方は通常の抽象基底クラスと同じです。

from collections.abc import Set


class ListBasedSet(Set):
    def __init__(self, iterable):
        self.elements = []
        for value in iterable:
            if value not in self.elements:
                self.elements.append(value)

    def __str__(self):
        return repr(self.elements)

    def __iter__(self):
        return iter(self.elements)

    def __contains__(self, value):
        return value in self.elements

    def __len__(self):
        return len(self.elements)


s1 = ListBasedSet('abcdef')
s2 = ListBasedSet('defghi')
overlap = s1 & s2  # __and__は継承されたので、そのまま積集合を計算できる
print(overlap)
# 実行結果:['d', 'e', 'f']

上記はリストベースの集合の実装です。Pythonの通常の集合は辞書と同じくハッシュテーブルを利用して実装されたので、時間計算量的にはリストより速いですが、空間計算量は少し多いので、この実装はメモリ消費量を節約したい時に使用できます。

5-6. 列挙型

列挙型は、変数(正式に言うと識別子)などを有限集合として束ねる抽象データ構造です。本来はオブジェクト指向プログラミングとは無関係のものです。例えば、プロセス指向プログラミング言語のCも列挙型をサポートしています。しかし、JavaやPythonのようなオブジェクト指向プログラミング言語では、列挙型はクラスオブジェクトの形で実現されています。

なぜ列挙型が必要かというと、現実世界ではある有限の範囲内に限定されているデータは結構多いからです。例えば、曜日というのは、週単位に限定された7種類の有限のデータになります。同じく、月も年単位で12種類のデータです。ソフトウェアの中でいうと、CSSのカラーネーム、HTTPコード、真偽値、ファイル記述子など有限な状態の集合は無数にあります。これらを訳のわからない数字ではなく、列挙型で表現すると、コードの可読性が向上し、ロジックも綺麗に見えます。

Pythonの標準ライブラリには、enumという列挙型を作成するモジュールが提供されています。まず、基本的な使い方を見てみましょう。

import enum


class Color(enum.Enum):
    red = 1
    green = 2
    blue = 3


print(Color.red)
# 実行結果:Color.red
print(Color['red'])
# 実行結果:Color.red
print(Color(1))
# 実行結果:Color.red
print(Color.red.value)
# 実行結果:1
print(Color.red.name)
# 実行結果:red

for color in Color:
    print(color)
# 実行結果:
# Color.red
# Color.green
# Color.blue

クラス変数と違って、列挙型はiterable objectになっています。

Color.red = 4
# 実行結果:
# Traceback (most recent call last):
#   File "oop.py", line 26, in <module>
#     Color.red = 4
#   File "/Users/kaito/opt/miniconda3/lib/python3.8/enum.py", line 383, in __setattr__
#     raise AttributeError('Cannot reassign members.')
# AttributeError: Cannot reassign members.

列挙型のメンバは外部から修正できません。

print(Color.red is Color.green)
# 実行結果:False
print(Color.red == Color.green)
# 実行結果:False
red = Color.red
print(Color.red == red)
# 実行結果:True
print(Color.red < Color.green)
# 実行結果:
# Traceback (most recent call last):
#   File "oop.py", line 30, in <module>
#     print(Color.red < Color.green)
# TypeError: '<' not supported between instances of 'Color' and 'Color'

enum.Enum列挙型はメンバ間の一致性評価と等価性評価のみサポートします。それ以外の値の比較をしたい時は、enum.IntEnumを使うことができます。

import enum


class Color(enum.IntEnum):
    red = 1
    green = 2
    blue = 3
    purple = enum.auto()  # valueのオートインクリメント


print(Color.purple > Color.blue)
# 実行結果:True

また、ビッド演算でのメンバの組み合わせを実現したい時はenum.Flagが使えます。

import enum


class Color(enum.Flag):
    red = enum.auto()
    green = enum.auto()
    blue = enum.auto()
    purple = enum.auto()


print(Color.__members__)
# 実行結果:
# {'red': <Color.red: 1>, 'green': <Color.green: 2>, 'blue': <Color.blue: 4>, 'purple': <Color.purple: 8>}
print(Color.purple | Color.blue)
# 実行結果:Color.purple|blue
print(Color.purple | 2)
# 実行結果:
# Traceback (most recent call last):
#   File "oop.py", line 13, in <module>
#     print(Color.purple | 2)
# TypeError: unsupported operand type(s) for |: 'Color' and 'int'

enum.Flagはメンバ間のビッド演算をサポートしていますが、整数値との計算はできません。これを実現するために、enum.IntFlagを使う必要があります。

import enum


class Color(enum.IntFlag):
    red = enum.auto()
    green = enum.auto()
    blue = enum.auto()
    purple = enum.auto()


print(Color.__members__)
# 実行結果:
# {'red': <Color.red: 1>, 'green': <Color.green: 2>, 'blue': <Color.blue: 4>, 'purple': <Color.purple: 8>}
print(Color.purple | Color.blue)
# 実行結果:Color.purple|blue
print(Color.purple | 2)
# 実行結果:Color.purple|green

enum.IntFlagはメンバを整数値として扱います。

続いて、普通の列挙型enum.Enumについてもう少し見ていきます。

import enum


class MessageResult(enum.Enum):
    SUCCESS = 1
    INVALID_MESSAGE = 2
    INVALID_PARAMETER = 3
    BAD_MESSAGE = 2


print(MessageResult(2))
# 実行結果:MessageResult.INVALID_MESSAGE


class MessageResult(enum.Enum):
    SUCCESS = 1
    INVALID_MESSAGE = 2
    INVALID_PARAMETER = 3
    INVALID_MESSAGE = 4

# 実行結果:
# Traceback (most recent call last):
#   File "oop.py", line 14, in <module>
#     class MessageResult(enum.Enum):
#   File "oop.py", line 18, in MessageResult
#     INVALID_MESSAGE = 4
#   File "/Users/kaito/opt/miniconda3/lib/python3.8/enum.py", line 99, in __setitem__
#     raise TypeError('Attempted to reuse key: %r' % key)
# TypeError: Attempted to reuse key: 'INVALID_MESSAGE'

列挙型はメンバのnameのユニーク性を保証しますが、valueに対しては制約しません。

import enum


@enum.unique
class MessageResult(enum.Enum):
    SUCCESS = 1
    INVALID_MESSAGE = 2
    INVALID_PARAMETER = 3
    BAD_MESSAGE = 2

# 実行結果:
# Traceback (most recent call last):
#   File "oop.py", line 4, in <module>
#     class MessageResult(enum.Enum):
#   File "/Users/kaito/opt/miniconda3/lib/python3.8/enum.py", line 865, in unique
#     raise ValueError('duplicate values found in %r: %s' %
# ValueError: duplicate values found in <enum 'MessageResult'>: BAD_MESSAGE -> INVALID_MESSAGE

enum.uniqueをデコレーターとしてクラスに適用すれば、valueのユニーク性も保証できるようになります。

import enum


MessageResult = enum.Enum(
    value='MessageResult',
    names=('SUCCESS INVALID_MESSAGE INVALID_PARAMETER'),
)

print(MessageResult.__members__)
# 実行結果:
# {'SUCCESS': <MessageResult.SUCCESS: 1>, 'INVALID_MESSAGE': <MessageResult.INVALID_MESSAGE: 2>, 'INVALID_PARAMETER': <MessageResult.INVALID_PARAMETER: 3>}

ハードコーディングではなく、Functional APIで動的に列挙型を作成することもできます。names引数にスペース区切りの文字列を渡すと、自動採番で数字を割り振ってくれます。

import enum


MessageResult = enum.Enum(
    value='MessageResult',
    names=(('SUCCESS', 3),
           ('INVALID_MESSAGE', 2),
           ('INVALID_PARAMETER', 1))
)

print(MessageResult.__members__)
# 実行結果:
# {'SUCCESS': <MessageResult.SUCCESS: 3>, 'INVALID_MESSAGE': <MessageResult.INVALID_MESSAGE: 2>, 'INVALID_PARAMETER': <MessageResult.INVALID_PARAMETER: 1>}

names引数に階層化したiterable objectを渡すと、それぞれのメンバのvalueを指定できます。

import enum


class Message(enum.Enum):
    DB_SAVE_SUCCESS = ('Saved successfully', 201)
    INTERNEL_ERROR = ('Internal error happened', 500)
    DB_DELETE_SUCCESS = ('Deleted successfully', 200)
    DB_ITEM_NOT_FOUND = ('Item not found', 404)

    def __init__(self, message, code):
        self.message = message
        self.code = code

    @property
    def ok(self):
        if str(self.code).startswith('2'):
            return True
        return False


print(Message.DB_SAVE_SUCCESS)
# 実行結果:Message.DB_SAVE_SUCCESS
print(Message.DB_DELETE_SUCCESS.ok)
# 実行結果:True
print(Message.DB_ITEM_NOT_FOUND.ok)
# 実行結果:False

列挙型のメンバは整数値に限らず、あらゆるデータ型を使うことができます。また、イニシャライザの__init__を実装すると、クラスの評価時にメンバの値は__init__に渡されます。そして、タプルを使えば複数の変数を渡すことができます。

ただし、__init__はメンバの値をカスタマイズすることはできません。メンバをカスタマイズしたい時は、コンストラクタの__new__を実装する必要があります。

import enum


class Coordinate(bytes, enum.Enum):
    def __new__(cls, value, label, unit):
        obj = bytes.__new__(cls, [value])
        obj._value_ = value
        obj.label = label
        obj.unit = unit
        return obj

    PX = (0, 'P.X', 'km')
    PY = (1, 'P.Y', 'km')
    VX = (2, 'V.X', 'km/s')
    VY = (3, 'V.Y', 'km/s')


print(Coordinate.PY.label, Coordinate.PY.value, Coordinate.PY.unit)
# 実行結果:P.Y 1 km
print(Coordinate.PY)
# 実行結果:Coordinate.PY

上記は公式ドキュメントに載っている例で、バイナリオブジェクトでメンバの値とその他の情報をまとめて格納する列挙型になります。

列挙型はクラスなので、内部でメソッドまたはダンダーを実装することができます。しかし、列挙型は普通のクラスと違うところがたくさんあります。まず、列挙型は特殊なメタクラスで実現されていて、メンバ(クラス変数)はクラスのインスタンスになります。そのため、__new____init__はインスタンス化のタイミングではなく、クラス評価時に機能します。それから、列挙型はいくつか特殊な属性を持っています。

  • __members__member_name:memberのマッピングで、読み出し専用です。
  • _name_: メンバ名
  • _value_:メンバの値、__new__で設定や変更できます。
  • _missing_:値が見つからなかった時に使われる検索関数です;オーバーライドできます。
  • _ignore_:リストまたは文字列で、その中身の要素と一致するクラス変数はメンバに変換されなくなります。
  • _order_:メンバの順番を維持するためのクラス属性です;例えば、_order_ = 'red green blue'で定義すると、この順番と違う形でメンバを定義したら、エラーを起こします。
  • _generate_next_valueFunctional APIenum.autoに使用され、あるメンバの適切な値を取得します;オーバーライドできます。

ちなみに、前後に1つのアンダースコアの付いた属性のことを_sunder_というらしいです。

5-7. データクラス

ここまで、Pythonのオブジェクト指向プログラミングのほとんどを見てきました。Pythonだけの問題ではなく、オブジェクト指向プログラミング言語全般の問題ではありますが、クラスの定義は非常に煩雑です。Pythonでは大体の場合、クラスの作成する時に、__init__の定義は最低限必要です。場合によっては他の特殊メソッドも実装しなければなりません。それに加えて、似たようなクラスを大量に作らなければならない時もあります。このようなコードの冗長性をボイラープレート・コードと呼びます。

簡単にクラスを作る1つの手として、types.SimpleNamespaceを使うことができます。

import types

bbox = types.SimpleNamespace(x=100, y=50, w=20, h=20)

print(bbox)
# 実行結果:namespace(h=20, w=20, x=100, y=50)
print(bbox==bbox)
# 実行結果:True

Pythonでtypes.SimpleNamespaceと同じ機能を実装すると以下のようになります。

class SimpleNamespace:
    def __init__(self, /, **kwargs):
        self.__dict__.update(kwargs)

    def __repr__(self):
        items = (f"{k}={v!r}" for k, v in self.__dict__.items())
        return "{}({})".format(type(self).__name__, ", ".join(items))

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

__init____repr____eq__は実装してくれましたが、__hash__や他の比較演算用特殊メソッドを実装したい時は結構面倒で、逆にコードの可読性が下がってしまう可能性があります。types.SimpleNamespaceはあくまでもシンプルなクラス(名前空間)を作るためのものです。

ボイラープレート・コードの問題に気づいたサードパーティライブラリの開発者たちがいて、クラスの定義をシンプルにするツールを開発してくれました。attrsはその1つです。基本的な使い方は以下のようになります。

from attr import attrs, attrib


@attrs
class Person:
    name = attrib(type=str)
    sex = attrib(type=str)
    age = attrib(type=int, default=0)


mary = Person('Mary', 'F', 18)
print(mary)
# 実行結果:Person(name='Mary', sex='F', age=18)
print(mary == mary)
# 実行結果:True

attrsはクラスデコレーターの形で使用できます。ここでは、3つのインスタンス変数を定義し、ageにデフォルト引数を設定しました。attrsは、__init____repr__を自動的に定義してくれます。また、__eq____ne____lt____le____gt____ge__も定義してくれて、比較対象はインスタンス変数のタプルになります。

詳しい紹介を省きますが、attrstypes.SimpleNamespaceより高度なツールで、様々な機能があり、Pythonのオブジェクト指向プログラミングにとっては強力なツールになります。

ボイラープレート・コードを解消するために、公式の動きとしてPython 3.7から導入されたデータクラス(dataclasses)というモジュールがあります。このモジュールは特殊メソッドの生成など、attrsと似たような機能を提供しています。

from dataclasses import dataclass
from typing import ClassVar
from functools import cached_property

import boto3


@dataclass
class S3Image:
    bucket: str
    key: str
    img_id: int = 1
    client: ClassVar = boto3.client('s3')  # クラス変数

    @cached_property
    def image_url(self, http_method: str) -> str:
        return self.client.generate_presigned_url(...)


item_image_1 = S3Image('Image', 'ItemImage')
print(item_image_1)
# 実行結果:S3Image(bucket='Image', key='ItemImage', img_id=1)
print(item_image_1 == item_image_1)
# 実行結果:True

データクラスはPythonの型アノテーションの形式でクラスのメンバ変数を定義できます。そして、__init____repr__を実装してくれます。attrsと違うのは、比較演算用の特殊メソッドはデフォルトでは__eq__しか実装してくれません。そして、実装してほしいメソッドはdataclassクラスデコレーターの引数として定義できます。デフォルトでは以下のようになります。

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
    ...

それぞれの引数の役割はほぼ自明です。

  • order__eq__以外の比較演算用特殊メソッドの__ne____lt____le____gt____ge__の自動追加のフラグです。Trueにするにはeq引数もTrueにしなければなりません。
  • unsafe_hash__hash__の挙動を制御するフラグです。unsafe_hash=Trueにすると、__hash__メソッドが自動的に追加されますが、不変(immutable)オブジェクトではない時に問題が生じるかもしれません。デフォルトではeqfrozen両方がTrueの時に、__hash__メソッドは自動的に追加されるので、フラグによる制御は不要です。eq=Truefrozen=Falseの場合は親クラスの__hash__を継承し、eq=Falsefrozen=Trueの場合は__hash__ = Noneに設定されます。また、__hash__を定義した状態ではunsafe_hash=Trueにすることはできません。
  • frozenは、フィールド(メンバ)に対する値の代入をコントロールするフラグです。Trueの場合は、代入を阻止し、読み出し専用になります。

また、データクラスにはfieldという関数が存在し、フィールドごとのコントロールができます。

from dataclasses import dataclass, field
from typing import List


@dataclass
class C:
    mylist: List[int] = field(default_factory=list)


c = C()
c.mylist += [1, 2, 3]

fieldは以下の引数を受け取り、Fieldオブジェクトを返します。

  • defaultはフィールドのデフォルト値を提供する引数です。defaultdefault_factoryは共存できません。
  • default_factoryは引数なしで呼び出せるオブジェクトを受け取り、フィールドのデフォルトのファクトリーを提供します。例えば、上記の例ではlistが提供され、それでフィールドのデフォルトのデータ構造を作成します。
  • init__init__にフィールドを含むかどうかのフラグです。
  • repr__repr__にフィールドを含むかどうかのフラグです。
  • compare__eq____gt__などの比較演算用特殊メソッドにフィールドを含むかどうかのフラグです。
  • hash__hash__にフィールドを含むかどうかのフラグです。hash=Noneの場合はcompareを使ってハッシュ値を計算します。公式では、hash=Noneが推奨されています。
  • metadataはマッピングまたはNoneを受け取り、マッピングの場合は読み出し専用の辞書types.MappingProxyTypeにラップされます。主に、サードーパーティーのモジュールなどが使用するものです。

fields関数で、データクラスのフィールドオブジェクトをタプルの形で全部取得することができます。

from dataclasses import fields


print(fields(c))
# 実行結果:
# (Field(name='mylist',type=typing.List[int],default=<dataclasses._MISSING_TYPE object at 0x7f8aa098a9a0>,
# default_factory=<class 'list'>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD),)

asdictastupleはデータクラスのインスタンスを辞書とタプルに変換します。

@dataclass
class Point:
    x: int
    y: int


@dataclass
class Points:
    pl: List[Point]


p = Point(10, 20)
assert asdict(p) == {'x': 10, 'y': 20}
assert astuple(p) == ((10, 20))

ps = Points([Point(0, 0), Point(10, 4)])
assert asdict(ps) == {'pl': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}  # 再帰的に処理される

make_dataclassという動的にデータクラスを作る関数もあります。

from dataclasses import dataclass, field, make_dataclass


C = make_dataclass('C',
                   [('x', int),
                     'y',
                    ('z', int, field(default=5))],
                   namespace={'add_one': lambda self: self.x + 1})

# 下の書き方と同様
@dataclass
class C:
    x: int
    y: 'typing.Any'
    z: int = 5

    def add_one(self):
        return self.x + 1

データクラスのインスタンスに修正を加え、同じ型のオブジェクトを新しく作るための関数replaceも提供されています。

from dataclasses import dataclass, replace


@dataclass
class Point:
    x: int
    y: int

    def __post_init__(self):
        print('__post_init__')


p1 = Point(1, 2)
# 実行結果:__post_init__
p2 = replace(p1, x=2)
# 実行結果:__post_init__
print(p2)
# 実行結果:Point(x=2, y=2)

replace__init__を呼び出して新しいインスタンスを作成するのですが、 __post_init__が定義されている場合は__init__の後に呼び出します。

他に、データクラスを判別するための関数is_dataclassも提供されています。データクラスかデータクラスのインスタンスに使用する場合のみ、Trueが返ってきます。

5-8. MRO

MRO(Method Resolution Order、メソッド解決順序)は、多重継承において、継承チェーン上でメソッドを探索する時の順序です。PythonのMROアルゴリズムは、「Python 2.2の古いクラス」、「Python 2.2の新しいクラス」、「Python 2.3以降の新しいクラス」の3種類存在します。

なぜMROが必要かというと、以下の例を見てください。

class D:
    def method(self):
        ...


class C(D):
    def method(self):
        ...


class B(D):
    pass


class A(B, C):
    pass

上記のコードは「菱形継承問題」と呼ばれる現象です。クラスAはmethodをどのクラスから継承すれば良いのかを探索する必要があります。

Python 2.2の古いクラスはDFS(深さ優先探索)を採用しています。イメージとしては以下の図のようになります。
                                            <img width="400" alt="Screen Shot 2020-11-05 at 3.35.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246522/99dadc0d-562a-9a83-a71e-2f240f8dfcae.png">

図の左は継承関係を表すもので、矢印は子クラスを指します。右はMROの探索順序です。DFSでは、クラスAはまず(B, C)の左にあるBから、Dまで辿り着き、Dmethodを実装しているので、それを継承して探索を終えます。継承関係を見ると、CのほうがAに近いので、本来はCmethodを継承するのが正しいであるため、DFSはうまく菱形継承問題を解決できないですね。

Python 2.2の新しいクラスはobjectを継承するクラスです。つまり、全ての新しいクラスは共通の祖先クラスobjectを持ちます。そのため、Mixin継承をする時にDFSではメソッドを正しく継承できないので、その対応としてMROアルゴリズムはDFSからBFS(幅優先探索)に変えられました。
                                            <img width="400" alt="Screen Shot 2020-11-05 at 3.49.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246522/4fae04b4-0741-3b66-2098-7d77752015c6.png">

BFSでは、クラスABから、横にあるCを先に探索しますので、菱形継承の場合は正しくメソッドを継承できます。しかし、通常の継承パターンは少し問題があります。
                                            <img width="400" alt="Screen Shot 2020-11-05 at 3.54.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246522/de8cefd9-b48a-a1ed-00d0-e47c60404af4.png">

上記の図は通常のMixin継承です。右のMROは一見問題ないように見えますが、実は違いますね。

class E:
    pass


class D:
    def method(self):
        ...


class C(E):
    def method(self):
        ...


class B(D):
    pass


class A(B, C):
    pass

例えば、上記のような場合、本来ならABからDに辿り着き、そのmethodを継承するのです。BFSになると、Cmethodが先に探索されるため、Dまでは辿り着かなくなります。BBの親のDから探す順序は単調写像と呼びます。

BFSは単調写像の性質を違反しているため、Python 2.3以降はC3というアルゴリズムでMROを解決するようになりました。C3は「A Monotonic Superclass Linearization for Dylan」という論文で公開されたもので、BFSとDFSの問題点を解決し、単調写像を満たした完璧なアルゴリズムになります。
         <img width="800" alt="Screen Shot 2020-11-05 at 4.09.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246522/a3a8943f-58d6-7400-9411-1e2c403f6989.png">

C3は以下の3つの性質を持っています。

  1. precedence graph(有向非巡回グラフ)
  2. 局所の優先順位を保持
  3. 単調写像

C3アルゴリズムの計算については、「The Python 2.3 Method Resolution Order」の記事に詳しく書かれています。C3は継承チェーンを有向非巡回グラフとして扱うため、循環継承だとエラーになることを注意してください。

また、クラスのMROを参照したい時は、mro()関数や__mro__属性の他に、inspect.getmro()関数を使うこともできます。

まとめ

オブジェクト指向の歴史から、OOPの関連概念、Pythonのオブジェクト指向プログラミングについて隅々まで見てきました。しかし、オブジェクト指向プログラミングは奥深いので、まだまだ学ぶことが多いです。今回はデザインパターンについて詳しく説明できませんでしたが、また別の記事で紹介したいと思います。

参考

Data Model
Built-in Functions
inspect — Inspect live objects
types — Dynamic type creation and names for built-in types
Descriptor HowTo Guide
abc — Abstract Base Classes
collections.abc — Abstract Base Classes for Containers
enum — Support for enumerations
dataclasses — Data Classes
What are metaclasses in Python?
Python Types and Objects(リンク切れ)

Discussion