Pythonのクラスは他言語のクラスと比べると少し異質なものである。その性質は、プロトタイプベースオブジェクト指向プログラミング(以下OOP)をするには十分である。というわけでPythonでプロトタイプベースOOPをしてみる。
Pythonはクラスをも一つのオブジェクトとして扱う
Pythonはとりあえず何でもオブジェクトとして扱おうとする。クラスやモジュールもオブジェクトである。その中でもクラス、インスタンス、モジュール、関数(メソッドは×)には新しい属性を定義することができる。
今回、クラスを使うのでまずはクラスを定義してから定義済みのクラスに新しい属性を定義してみる。
>>> class Object: ... pass ... >>> Object.attr_1 = 'Hello.' >>> print(Object.attr_1) Hello.
Object
クラスを定義した後attr_1
属性に'Hello.'
を代入しようとすると代入できることがわかる。print(Object.attr_1)
で標準出力できることも確認できた。
もちろん関数やラムダ式も代入できる。
>>> def attr_2(): ... print('Call attr_2().') ... >>> Object.attr_2 = attr_2 >>> Object.attr_2() Call attr_2(). >>> Object.attr_3 = lambda: print('Call attr_3().') >>> Object.attr_3() Call attr_3().
これらの属性は継承したクラスも呼び出すことができる。
>>> class Derive(Object): ... pass ... >>> print(Derive.attr_1) Hello.
しかしこれらの属性は、継承することで派生クラスにも定義されるのではない。その証拠に、__dict__.keys()
属性で全属性を確認できるのでそれらをfor
(foreach)文を使って確認してみる。
>>> for key in Object.__dict__.keys(): ... print(key) ... __module__ __dict__ __weakref__ __doc__ attr_1 attr_2 attr_3 >>> for key in Derive.__dict__.keys(): ... print(key) ... __module__ __doc__
つまり派生クラスから基底クラスにあった属性を呼び出そうとすると基底クラスの属性が呼び出される、ということだ*1。
・・・ん?
これってまんまプロトタイプベースじゃん。
本格的なプロトタイプベースらしいOOP
Pythonでプロトタイプベース的なOOPをするためにはクラス変数とクラスメソッドが必要不可欠である。ちなみに先ほどObject
のattr_1
に文字列リテラルを代入することで新しく属性を定義したがこれぞまさしくクラス変数である。すなわちクラス変数は簡単に定義できる。
では、クラスメソッドはどうやって定義するのか?その前にそもそもクラスメソッドとは何なのかについて知る必要があるだろう。
クラスメソッドとは
クラス自身を扱うことができるメソッドである。クラス内で定義するメソッドはインスタンスメソッドと呼ばれるがこれはインスタンスを扱うことができるメソッドである。つまり平たく言えばクラスメソッドはインスタンスメソッドのクラス版である。
以下にクラスメソッドの例を示す。
class ClassMethod: ''' クラスメソッドを扱うクラス ''' # クラス変数 name = 'Python' # クラスメソッド @classmethod def hello(cls): return 'Hello, ' + cls.name + '!' if __name__ == '__main__': print(ClassMethod.hello())
Hello, Python!
この例を詳細に解説していく。
# クラス変数 name = 'Python'
クラス変数を定義している。クラス変数はクラスを定義するときにも定義ができる。それもclass
ブロック内に書けばOK。インスタンス変数よりも簡単に定義できる。
# クラスメソッド @classmethod def hello(cls): return 'Hello, ' + cls.name + '!'
本題のクラスメソッド。一般的には@classmethod
デコレータを使って第一引数をcls
とするのが慣例である。第一引数cls
は定義するクラス自身、すなわち今回の例で言えばClassMethod
を示している。
これにより、本来であれば呼び出したときに自分自身のクラス変数を使っているとNameError
とかになるが、問題なく呼び出すことが可能となる。
if __name__ == '__main__': print(ClassMethod.hello())
そして最後にクラスメソッドを呼び出し、それを組み込み関数print
で標準出力している。第一引数はクラス(自分)なので渡す必要はない。
これにより、標準出力されたのが以下である。
Hello, Python!
ちなみにこのクラスメソッド、組み込み関数classmethod
を使っても生成できる。したがって以下のような書き方も可能である。
>>> class ClassMethod: ... pass ... >>> ClassMethod.name = 'Python' >>> ClassMethod.hello = classmethod(lambda cls: 'Hello, ' + cls.name + '!') >>> print(ClassMethod.hello()) Hello, Python!
こうして見ると実はPythonってJavaScriptよりもIoのような純粋なプロトタイプベースに近いのではないか?と思ってしまう。
しかしながら残念なことにPythonにはクラスオブジェクトをクローンする機能は標準でついていない。それを実現するためには継承を必要とする。
いちいち継承をするのも面倒くさいので、クラスオブジェクトにclone
メソッドを実装しようと思う。
cloneメソッドの実装
さて、こう言っては何だが実はclone
メソッドを実装するのは非常に簡単である。なぜなら派生クラスを返せば良いからである。
clone
メソッドの実装を以下に示す。
class Object: ''' cloneメソッド付きクラス ''' # cloneメソッド @classmethod def clone(cls): ''' クラスオブジェクトのクローン ''' # クラスメソッド内部に自身の派生クラスを定義する class Clone(cls): pass # 派生クラスを返す return Clone
第一引数cls
にはクラスオブジェクトが渡されるのでcls
を継承してClone
クラスオブジェクトを生成し、それを返している。
ではclone
メソッドを使ってみよう。
>>> Object_1 = Object.clone() >>> Object_1.hello = classmethod(lambda cls: 'Hello, ' + cls.name + '!') >>> Object_1.name = 'Python' >>> print(Object_1.hello()) Hello, Python!
これは本当にクラスなのか?と疑ってしまうかもしれないが、インスタンスを生成できる。よってこれはクラスである。
>>> instance = Object_1()
>>> print(instance.hello())
Hello, Python!
ちなみにclone
メソッドを使ったときにクラスを継承するが、当然clone
メソッドも継承されるので、クローンしたクラスオブジェクトも呼び出すことが可能である。
>>> Object_2 = Object_1.clone()
>>> print(Object_2.hello())
Hello, Python!
cloneメソッドに初期化機能を追加する
しかし、clone
メソッドだけではクラスオブジェクトをクローンした後も新しく属性を定義しなければならない。せめてコンストラクタ関数のようなことはできないものか。
ということでコンストラクタ関数を受け入れてくれるようにObject
クラスに初期化機能を追加してみる。
class Object: ''' cloneメソッド付きクラス ''' # cloneメソッド @classmethod def clone(cls, ctor=None, *args): ''' クラスオブジェクトのクローン(初期化機能付き) ''' # クラスメソッド内部に自身の派生クラスを定義する class Clone(cls): pass # コンストラクタ関数を適用 if ctor is not None: ctor(Clone, *args) # 派生クラスを返す return Clone
まず変更したのはclone
メソッドの引数である。
def clone(cls, ctor=None, *args):
クラスを示すcls
以外にもctor
とargs
がある。
ctor
が適用するコンストラクタ関数で、デフォルトだとNone
になっているデフォルト引数である。そしてargs
はコンストラクタ関数の引数を示し、いくらでも渡すことができる可変長引数である。
次にコンストラクタ関数適用部分を見てみる。
# コンストラクタ関数を適用 if ctor is not None: ctor(Clone, *args)
ctor
がNone
でなかったとき、つまりコンストラクタ関数があったときにこの処理は実行される。そして残りの引数は*args
となっているが、これは可変長引数にパラメータを渡したときタプル型となっているのだがそれを分解して、固定長の引数として使う。
ではこれをもとにコンストラクタ関数を作ってクローンするクラスオブジェクトに適用する。
>>> def constructor(cls, name, year): ... cls.name = name ... cls.year = year ... cls.fst_release = classmethod(lambda cls: f'The first release of {cls.name} was in {cls.year}.') ... >>> Object_3 = Object.clone(constructor, 'Python', 1991) >>> print(Object_3.fst_release()) The first release of Python was in 1991.
Python、自由度が高いなぁ。PEPではこのような手法は推奨されていないと思うけど。
追記(2021/10/3): 続編書きました
*1:ちなみに__dict__と__weakref__も基底クラスにだけ存在する属性である