Pythonまとめ(3.クラスとデコレータ)
独自クラス
基本的な構文
親クラスの取得
super()関数で親クラスのメソッドを参照できる。コンストラクタで親クラスのコンストラクタを呼び出す例を示す。
可視性
Pythonでは各メソッドや変数の可視性を指定できない。代わりにアンダースコア_から始まる名前にすることでその変数やメソッドは外部から利用することを想定していないことを示す。通常はアンダースコアで始まる関数や変数を外部から参照するべきでない。
マングリングと可視性
アンダースコア2つ__で始まる変数名はマングリング(名前修飾)が行われる。classname.__attributeはclassname._classname__atributeのように_クラス名が付与される。その結果として外部からのアクセスが意図して実装しない限りできなくなる。
ただし、マングリングは継承クラスとの名前の重複を防ぐための仕組みであり、外部からの隠蔽を目的とする機構ではない。また、厳密にはマングリングはアンダースコア2つ以上で始まり、アンダースコア1つ以下で終わる名前に適用される。なので__name__はマングリングの対象とはならない。
名前の重複
Readerクラスと、Readerクラスを継承したCSVReaderクラスを以下の通り作成した。これはマングリングを使用していない。
ここでCSVReader.read()を呼び出すとRecursionErrorが発生する。
出力から以下のような動作をしていることが分かる。
- CSVReader->read が呼び出される
- CSVReader->readdata が呼び出される
- Reader->readが呼び出される
- CSVReader->readdata が呼び出される
- Reader->readが呼び出される
- 以下繰り返し
これはReader.read()が呼び出すself._readdata()が、Readerクラスの_readdataメソッドではなく、CSVReaderクラスで定義した同名の_readdataメソッドを指すからである。マングリングを使用することでこのような名前重複を防ぐことができる。
プロパティ
プロパティはメンバ変数のようにアクセスできる関数である。getterやsetter付きのメンバ変数と考えてもいいだろう。@property
デコレータをつけたメソッドがそのプロパティのgetterとなり、@propertyname.setter
デコレータをつけたメソッドがそのプロパティのsetterとなる。
プロパティへのアクセスは()をつけずに、インスタンス変数と同じようにアクセスする。
@propertyデコレータはpropertyクラスのオブジェクトを作成している。以下のコードは上と同等の実装である。
プロパティのユースケースとしては以下のようなものが考えられる。
- 値を設定するときにチェックを行う
- 他の属性値の影響で値が変わる
他の属性値の影響を受けるプロパティの例を示す。
特殊メソッド
Pythonでは特殊メソッド(スペシャルメソッド)を実装することでオブジェクトの動作を定義できる。例えば数値的な要素を持つクラスであれば__add__メソッドを実装することで演算子+による加算が行えるようになる。
インスタンスの生成・削除に関する特殊メソッド
用途 | メソッド | 呼び出し例 | 説明 |
---|---|---|---|
インスタンスの生成 | object.__new__( cls, ... ) | obj = Class() | 新しいインスタンスを作成するときに使う。通常は作成するクラスのインスタンスを返すが、異なるクラスのインスタンスを返しても良い。__new__が異なるクラスのインスタンスを返した場合__init__は呼び出されない。 実装しない場合、親クラスの__new__が呼び出される。 通常は__init__でオブジェクトを初期化するため、__new__はあまりカスタマイズしない。 |
インスタンスの初期化 | object.__init__( self, ... ) | obj = Class() | __new__によって作成されたオブジェクトの初期化を行う。 実装しない場合、親クラスの__init__が呼び出される。 |
インスタンスの削除 | object.__del__( self ) | del obj | インスタンスが破棄されるときに呼び出される。ファイナライザやデストラクタとも呼ばれる。 補足:呼び出し例のdel xは正確にはxの参照カウントを減らすものであり、参照カウントが0になったときにx.__del__()が呼び出される。 |
__new__メソッドの実装の違いにより、__init__メソッドが呼び出される場合とそうでない場合の例を示す。Sample1クラスの__init__メソッドは数値リストを返しているため、__init__メソッドが呼び出されない。
文字列・バイト列に変換する特殊メソッド
用途 | メソッド | 呼び出し例 | 説明 |
---|---|---|---|
公式の文字列に変換 | object.__repr__( cls, ... ) | obj | オブジェクトの「公式の」文字列を計算する。この文字列は同じオブジェクトを再生成できるような表現が望ましい。できないなら"<何らかの情報>"の形式の文字列を返すべきである。 これはデバッグでも用いられるため、曖昧でない十分な情報を含むことが重要である。 |
非公式の文字列に変換 | object.__str__( self, ... ) | str(obj) | オブジェクトの「非公式の」文字列を計算する。__repr__とは異なり、ユーザフレンドリーな(簡潔な)文字列を使用して良い。デフォルトでは__repr__を呼び出す。 |
バイト列に変換 | object.__bytes__( self ) | bytes(obj) | オブジェクトのバイト列表現を計算する。 |
フォーマットされた文字列に変換 | object.__format__( self, format ) | f'{obj:+,}' | フォーマット文字列などで指定されたフォーマットどおりの文字列を計算する。 |
公式(repr)と非公式(str)の違いはdatetimeの実装を見るとわかりやすい。datetimeにおいてreprが返す文字列は、実行可能なPythonコードとなっている。このコードを実行すると同値のオブジェクトが得られる。このような条件さえ整えば同じオブジェクトを再生成できるような値が望ましいのがreprである。
比較に関するメソッド
用途 | メソッド | 呼び出し例 | 説明 |
---|---|---|---|
より小さい | object.__lt__( self, other ) | obj < other | |
小さいか等しい | object.__le__( self, other ) | obj <= other | |
等しい | object.__eq__( self, other ) | obj == other | |
等しくない | object.__ne__( self, other ) | obj != other | |
大きい | object.__gt__( self, other ) | obj > other | |
大きいか等しい | object.__ge__( self, other ) | obj >= other | |
ハッシュ | object.__hash__( self ) | hash( obj ) | dictなどでも使われる |
論理 | object.__bool__( self ) | bool( obj ) | オブジェクトそのものの真偽判定 |
obj > other
を実行したときに、もしotherがobjのサブクラスの場合はotherのメソッドが優先される。つまりobj.__gt__(other)
ではなく、その対となるother.__le__(obj)
が実行される。
数値演算に関するメソッド
self + otherのような二項演算子でも特殊メソッドobject.__add__(self, other)
が呼び出される。これを実装することで数値のように動作するクラスを作成できる。すべての数値演算に関するメソッドはPython3 Documentation 数値型をエミュレートするを参照すること
属性に関するメソッド
オブジェクトに関連付けられ、通常ドットを使用してobject.nameの形でアクセスできるものを属性またはアトリビュートとよぶ。属性へアクセスすると内部的には以下のメソッドが呼び出されている。
用途 | メソッド | 呼び出し例 | 説明 |
---|---|---|---|
属性への代入 | object.__setattr__( self, name, value ) | obj.x = 5 | |
属性へのアクセス | object.__getattribute__( self, name ) | obj.x | 属性にアクセスするときに呼び出される。 |
属性の削除 | object.__delattr__( self, name ) | del obj.x |
これらのメソッドをカスタマイズすることで挙動を変更できる。メソッドの呼び出し状況を確認するために標準出力するように変更してみる。
__setattr__メソッドは定義されていない属性であれば自動で追加する動作がデフォルトである。なので最後のi.name = "ItemName"
はエラーにならない。一方で__getattribute__メソッドは定義されていない属性だとAttributeErrorが発生する。
もしクラスに__getattr__メソッドが定義されている場合、存在しない属性にアクセスした場合(主に__getattribute__でAttributeErrorが発生した場合)に__getattr__メソッドが呼び出される。そこでデフォルト値を定義するなどの挙動を制御できる。なお、__getattribute__は、アクセスしようとした属性の値を返すか、AttributeErrorを送出しなければならない。
属性へのアクセスは組み込み関数getattr、setattrでも行える。getattrでは属性が存在しないときのデフォルト値を指定できるメリットがある。
遅延評価
__getattribute__などを利用して属性の遅延評価が行える。遅延評価を簡単に言うなら、属性の値が必要になってから属性値を計算することで、インスタンス生成時にかかる時間・処理コストや、値を保持するために必要なメモリなどを抑えるというものである。
たとえば初期化に時間がかかるアトリビュート:nameがあるとしよう。普通の実装では、nameを使用するかによらずインスタンスを生成するときにコンストラクタでnameを初期化するために時間がかかる。以下のプログラムでは擬似的にtime.sleepで時間がかかるようにしている。
遅延評価を行うサンプルを示す。次に示す実装では初めてnameを取得しようとしたときにnameの初期化が行われ、時間がかかる。もしnameが使われなければ時間がかかることはない。また、一度初期化が行われれば、次から時間がかかることもない。
次にメモリ使用量へ影響する例を見てみる。10万個の数値データを持つクラスを生成してみた。
Line # Mem usage Increment Occurrences Line Contents
=============================================================
16 45.0 MiB 45.0 MiB 1 @profile
17 def main():
18 49.6 MiB 4.6 MiB 1 item = Item()
19 49.6 MiB 0.0 MiB 1 item.codes
20 49.6 MiB 0.0 MiB 1 itemDelayed = ItemDelayed()
21 53.4 MiB 3.8 MiB 1 itemDelayed.codes
これはmemory_profilerライブラリを使用して、行ごとにメモリ使用量の増減を出力したものである。Itemクラスではインスタンス生成時にメモリが増加しているのに対し、ItemDelayedクラスではcodesへアクセスが発生したときにメモリが増加していることが分かる。
デコレータ
デコレータは複数の関数またはクラスに対して共通の加工を行う機能である。
デコレータ利用の背景と動作イメージ
2つの数字を加算する関数add_numberがある。add_numberは変更することなく、この処理の開始前後で標準出力にメッセージを出力する処理を行いたい。
これは内部でadd_numberを呼び出す別の関数を作成すれば簡単に実現できる。
このように、定型の加工を既存の関数に行いたいというケースがある。それを実現する方法の1つがデコレータである。
まずは、この「関数の開始前後で標準出力にメッセージを出力する加工」をプログラムで行ってみる。つまり「add_number関数を引数として受け取りadd_number_debug関数を返す」関数を作成する。次のmodifier関数は加工する関数を引数targetにとり、その前後に出力を追加した関数wrapper_functionを定義し、それを返している。
ここで作成したmodifierは引数が2つに固定されており、出力内容もadd_numberという関数に固定されている。それを任意の引数の個数、関数名に対応するように改善した関数を加工する関数printmethodは以下のようになる。
これで任意の関数に開始前後で標準出力にメッセージを出力する加工が実装できた。さて、ここで加工したい関数に対して@printmethodを付与して実行してみる。
concat_stringにprintmethodによる加工が適用されることが分かる。つまり、デコレータとはデコレーションしてくれる関数を指定するものである。
引数をとるデコレータ
動作を調整するパラメータを引数にとるデコレータも作成できる。この場合は二重のwrapper_functionを作成する。
補足
まずrecordtime(datetime.timezone(datetime.timedelta(hours=+9), 'JST'))が実行される。このrecordtime関数の中身を、引数tzinfoの値を書き換えたものは以下のようになる。
これは「JSTで前後に時刻を表示するデコレータ」であることが分かる。つまりrecordtime関数はパラメータが設定済みのデコレータ用関数を返す関数といえる。こうなってしまえばあとは普通のデコレータと同じである。