Pythonのオブジェクト指向プログラミングを完全理解 (2)
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 は、各インスタンスに対して宣言された変数に必要な記憶領域を確保し、dict と weakref が自動的に生成されないようにします。 |
__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 すべきモジュール名をリストとして限定できます。 |
表で示したものの一部は関数オブジェクトが所有する属性です。Pythonは全てがオブジェクトなので、関数もオブジェクトになり、第一級関数であるプログラミング言語です。その他に、モジュールに使われる属性もありますが、__init__.py
ファイルの中に定義して使うことができます。また、上記の表で示したもの以外に、特定のモジュールに使われている属性もあります。
クラスのメンバを参照したい時は、vars()
とdir()
が使えます。vars()
はオブジェクトの__dict__
属性を参照しますので、継承されたメンバは表示されません。それに対して、dir()
はオブジェクトの__dir__
メソッドを呼び出します。__dir__
メソッドのデフォルトの実装はスコープ内にある名前を全部返すため、継承されたメンバも取得できます。そして、メンバの値も一緒に参照したい時はinspect.getmembers()
が使えます。inspect.getmembers()
はメンバとその値を格納したリストを返します。また、inspect.getmembers(obj, inspect.ismethod)
で、メソッドだけ絞り込むこともできます。他にも、is
から始まるinspectモジュールの関数がありまして、それらを使用して特定のメンバを取得できます。詳しくはドキュメントを参照してください。
5-2 タイプとオブジェクト
Pythonのtype
とobject
は「鶏が先か、卵が先か」のような関係性を持っています。つまり、どれが先かははっきり説明できないです。そして、type
とobject
は共生関係で、常に同時に出てきます。
まず、Pythonは「全てがオブジェクト」のプログラミング言語です。そして、3. オブジェクト指向に関する概念で紹介したように、オブジェクト指向の枠組みには主に以下の2種類の関係性が存在します。
- 継承関係。子クラスは親クラスを継承し、is-aの関係性を作ります。例えば、
reptile
を継承したsnake
クラスがあるとして、「snake is a kind of reptile」と言えます。親クラスを参照したい時は、__base__
が使用できます。 - クラス・インスタンス関係。あるタイプのクラスをインスタンス化するとこの関係が生まれます。例えば、
Squasher
というsnake
のインスタンスを作ることができ、「Squasher is an instance of snake」と言えます。ここのsnake
はSquasher
のタイプクラスと定義します。インスタンスのタイプクラスを参照したい時は、__class__
か、type()
関数が使用できます。
この2種類の関係性を図で表すと以下のようになります。
次に、type
とobject
を見てみます。
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'>,)
続いて、list
、dict
、tuple
などのビルトインデータクラスについて見てみます。
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
クラスのインスタンスにも親クラスが存在しません。
ここまでの各種の関係性を図にすると以下のようになります。ここでは、実線は継承関係を表し、矢印は親クラスを指します。点線はクラス・インスタンス関係を表し、矢印はインスタンスのタイプクラスを指します。
上記の検証から、以下の結果に辿り着きました。
- 全ての
object
はtype
のインスタンスです。 -
type
の直属のインスタンスはobject
やobject
を継承したクラスで、これらは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にはtype
とobject
両方必要だろうと思うかもしれません。例えば、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
自身もメタクラスです。メタクラス、クラス、インスタンスの関係性は以下の図のようになります。
type
関数は特殊なメタクラスです。実はclass
を使ってクラスを作成する時に、Pythonは裏でtype
を使っています。そのため、全てのobject
はtype
のインスタンスになるわけです。
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つの問題点があります。
- 変数に対して、
property
とx.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のオブジェクト指向の基本で紹介したproperty
、classmethod
、staticmethod
デコレーターと同じ機能を実現できます。
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)
の処理になります。そして、Property
のself.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__
で、get
とpost
メソッドを持つクラスを子クラスとして判定するよう実装しました。これで、明示的な継承をせずに、クラスのインターフェースに対してある程度の制約をかけることができます。
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.abstractmethod
はclassmethod
、staticmethod
、property
などと併用することができます。
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_value
:Functional API
やenum.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__
も定義してくれて、比較対象はインスタンス変数のタプルになります。
詳しい紹介を省きますが、attrs
はtypes.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)オブジェクトではない時に問題が生じるかもしれません。デフォルトではeq
とfrozen
両方がTrueの時に、__hash__
メソッドは自動的に追加されるので、フラグによる制御は不要です。eq=True
、frozen=False
の場合は親クラスの__hash__
を継承し、eq=False
、frozen=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
はフィールドのデフォルト値を提供する引数です。default
とdefault_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),)
asdict
とastuple
はデータクラスのインスタンスを辞書とタプルに変換します。
@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
まで辿り着き、D
はmethod
を実装しているので、それを継承して探索を終えます。継承関係を見ると、C
のほうがA
に近いので、本来はC
のmethod
を継承するのが正しいであるため、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では、クラスA
はB
から、横にある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
例えば、上記のような場合、本来ならA
はB
からD
に辿り着き、そのmethod
を継承するのです。BFSになると、C
のmethod
が先に探索されるため、D
までは辿り着かなくなります。B
とB
の親の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つの性質を持っています。
- precedence graph(有向非巡回グラフ)
- 局所の優先順位を保持
- 単調写像
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