0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python文法講義~特殊メソッド~

Posted at

まえがき

今回は特殊メソッドについて扱う。これも、[2]のような辞書を読み解かなければ詳細な解説を得られないことが多いと感じるため、ここで整理する。次に解説するデータクラスも同様だが、オブジェクト指向プログラミングのためには、理解は欠かせない。

主な特殊メソッド

Pythonでは、あらかじめ特定の役割を与えられたメソッドとして、特殊メソッドが用意されている。

分類 主なメソッド
基本 __init__(初期化)、__new__(生成)、__hash__(ハッシュ値)、__del__(破棄)、__format__(整形)
型変換 __str__(文字列)、__int__(整数)、__float__(浮動小数点数)、__complex__(複素数)、__bool__(真偽値)、__index__(インデックス)
数値演算 __add__(加算)、__sub__(減算)、__mul__(積算)、__truediv__(除算)、__floordiv__(整数除算)、__mod__(剰余)、__divmod__(商剰余)、__pow__(乗算)、__neg__(単項マイナス)、__pos__(単項プラス)
ビット演算 __lshift__(左シフト)、__rshift__(右シフト)、__and__(論理積)、__or__(論理和)、__xor__(排他的論理和)、__invert__(否定)
比較 __lt__(<)、`__le__`(<=)、`__eq__`(==)、`__ne__`(!=)、`__gt__`(>)、`__ge__`(>=)
属性 __getattr__(取得)、__getattribute__(取得)、__setattr__(設定)、__delattr__(削除)、__dir__(一覧)
呼び出し __call__(関数コール)
関数 __abs__(絶対値)、__round__(丸め)、__trunc__(0方向丸め)、__floor__(切り捨て)、__ceil__(切り上げ)
コンテナー __len__(長さ)、__getitem__(取得)、__setitem__(設定)、__delitem__(削除)、__missing__(キーなし)、__iter__(イテレーター)、__reversed__(逆ソート)、__contains__(存在有無)
ディスクリプター __get__(取得)、__set__(設定)、__delete__(削除)
コンテキスト __enter__(生成)、__exit__(破棄)
継承 __init_subclass__(継承)、__instancecheck__isinstance)、__subclasscheck__issubclass

特殊メソッドを利用することで、例えばインスタンス同士を加算する記述において、obj1 + obj2 のような記述も可能になる。obj1.add(obj2) のようなメソッドでも代用できるが、特殊メソッドを利用することで、より自然で直観的なコードを記述できる。
ただし、その方で実装されていない操作が呼び出された場合は、TyprError例外を送出する。

オブジェクトの文字列表現を取得

__str__メソッドは、可能であればすべてのクラスで実装するべき。オブジェクトの適切な文字列表現(__str__メソッド)を用意しておくことで、ロギング/単体テストなどの局面でも

print(obj)

とするだけで、オブジェクトの概要を確認できる。(print関数にオブジェクトを渡した場合、内部的には __str__ が呼ばれる)
以下は、Personクラスに対して、__str__ メソッドを渡す例。

class Person:
    def __init__(self, firstname: str, lastname: str) -> None:
        self.__firstname = firstname
        self.__lastname = lastname

    # インスタンスの文字列表現を生成
    def __str__(self) -> str:
        return f'{self.lastname} {self.firstname}'

    # プロパティを定義
    @property 
    def firstname(self) -> str:
        return self.__firstname

    @property 
    def lastname(self) -> str:
        return self.__lastname

if __name__ == '__main__':
    p = Person('太郎', '山田')
    print(p)    # 山田 太郎

__str__ メソッドを実行する際は、そのクラスを特徴づける情報(インスタンス変数)を選別して文字化するのがポイント。全てのインスタンス変数を書きだすのが目的ではない。
また、__str__ メソッドで利用したインスタンス変数は、個別のゲッターでも取得できるようにするべき(そうしなければ、利用者が個別の情報を取り出すために、__str__ メソッドの戻り値を解析しなければならなくなる)。

解析可能な表現を取得する

__str__ メソッドによく似たメソッドとして、__repr__ メソッドもある。
→ eval関数が呼び出されたとき、元のオブジェクトを復元できるような文字列を返すことが期待される(Personであれば、例えば「Person('太郎', '山田')」のような文字列)。

例えば、上のPersonクラスに __repr__ メソッドを実装すると以下

class Person:
    def __init__(self, firstname: str, lastname: str) -> None:
        self.__firstname = firstname
        self.__lastname = lastname

    # インスタンスの文字列表現を生成
    def __repr__(self) -> str:
        return f''Person('{self.firstname}', '{self.lastname}')''

    # プロパティを定義
    @property 
    def firstname(self) -> str:
        return self.__firstname

    @property 
    def lastname(self) -> str:
        return self.__lastname

if __name__ == '__main__':
    p = Person('太郎', '山田')
    print(p)    # 山田 太郎

オブジェクト同士が等しいかどうかを判定

__eq__ メソッド:オブジェクトの同値性(=オブジェクト同士が意味的に同じ値を持つこと)を判定するためのメソッド。== 判定がなされた場合に呼び出される。
objectクラスが基底で用意している__eq__メソッドは、同一性(オブジェクト同士が同じ参照先を持つこと)を確認するに過ぎない。
→ 意味ある値としての透過性を判定したい場合は、個別のクラスで__eq__メソッドをオーバーライドしなければならない

class Person:
    def __init__(self, firstname: str, lastname:str) -> None:
        self.firstname = firstname
        self.lastname = lastname

    # 氏/名がともに等しければ同値とする
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Person):
            return self.firstname == other.firstname and \
                   self.lastname == other.lastname
        return False

if __name__ == '__main__':
    p1 = Person('太郎', '山田')
    p2 = Person('次郎', '鈴木')
    p3 = Person('太郎', '山田')
    print(p1 == p2)    # False
    print(p1 == p3)    # True

同値性の判定は以下の段階を踏む。
if isinstance(other, Person)::比較の対象(引数 other )がPerson型であるかどうかを判定。型が異なるのであれば等しくないのは明らかなので、Falseを返す。
return self.firstname == other.firstname and self.lastname == other.lastname:型が一致している場合には、firstname/lastnameそれぞれの値を比較し、双方とも等しい場合に、__ eq__ メソッド全体を True と判断する。
ここでは、すべてインスタンス変数を判定の対象としているので、return self.firstname == other.firstname and self.lastname == other.lastnameは、以下のように書き換えても同じ意味。

return self.__dict__ == other.__dict__

cf.) __dict__
__dict__:その方で定義されたインスタンス変数をdict型で取得。__dict__ を利用することで、例えば、

p.firstname

は、

p.__dict__['firstname']

と書き換えても同義。ただし、__dict__は、
__slot__ を利用している場合は参照できない
__ 月の変数も見えてしまう
・そもそも一般的な . でのアクセスの方が完結
などの問題がある。特別な意図がない限り、クラス外部からのアクセスに利用すべきではない。

派生クラスでの同値判定

Personクラスと、その派生クラスであるBusinessPersonクラスを比較する例を考える。BusinessPersonクラスには、firstname/lastnameにtitle(職位)を追加する。

class Person:
    def __init__(self, firstname: str, lastname:str) -> None:
        self.firstname = firstname
        self.lastname = lastname

    # 氏/名がともに等しければ同値とする
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Person):
            return self.firstname == other.firstname and \
                   self.lastname == other.lastname
        return False

class BusinessPerson(Person):
    def __init__(self, firstname: str, lastname: str, title: str) -> None:
        super().__init__(firstname, lastname)
        # titleを追加
        self.title = title

if __name__ == '__main__':
    p = Person('太郎', '山田')
    bp = BusinessPerson('太郎', '山田', '部長')
    print(p == bp)    # True
    print(bp == p)    # True

この場合、Person/BusinessPerson間の比較は、Personの __eq__ メソッドに従うのでシンプル。新たに追加された title は同値判定に関与しないので、いずれも True になる。
→ ただし、Personで __dict__ に基づく判定を行っている場合はその限りではない。Person/BusinessPerson間で保持する情報が異なるため。

では、BusinessPersonクラスに __eq__メソッドを実装するとどうなるか?

class Person:
    def __init__(self, firstname: str, lastname:str) -> None:
        self.firstname = firstname
        self.lastname = lastname

    # 氏/名がともに等しければ同値とする
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Person):
            return self.firstname == other.firstname and \
                   self.lastname == other.lastname
        return False

class BusinessPerson(Person):
    def __init__(self, firstname: str, lastname: str, title: str) -> None:
        super().__init__(firstname, lastname)
        # titleを追加
        self.title = title

    def __eq__(self, other: object) -> bool:
        if isinstance(other, BusinessPerson):
            # Person型の判定に加えて、titleも判定
            return super().__eq__(other) and \
                   self.title == other.title
        return False

if __name__ == '__main__':
    p = Person('太郎', '山田')
    bp = BusinessPerson('太郎', '山田', '部長')
    print(p == bp)    # False
    print(bp == p)    # False

・左辺にPerson型を持つ print(p == bp) は、p.__eq__(bp) と等価、すなわち title を加味しないので、True
・左辺にBusinessPerson型を持つ print(bp == p) は、p.__eq__(p) と等価、すなわち title を加味するので、False
になるように思える。
しかし、実際はいずれも False になる。
→ Pythonでは、== 演算子のオペランドが継承関係にある場合、左辺右辺いずれに位置するにも関わらず、派生クラスの __eq__ メソッドによって同値判定が行われる。

このような性質がなければ、「'a == b'」だが「b != a」のような矛盾が生まれてしまう。

__hash__:オブジェクトのハッシュ値を取得する

__hash__メソッド:オブジェクトのハッシュ値(:オブジェクトのデータをもとに生成されたint値、dict/setなどのハッシュ表で正しく値を管理するための情報)を返す。
→ 同値のオブジェクトは同じハッシュ値を返すことが期待される。

一方、異なるオブジェクトに対して、必ずしも異なるハッシュ値を返さなくてもかまわない。即ち、__hash__目楚度が固定値を返しても誤りではない。しかし、ハッシュの性質上、検索効率を落とすので、ハッシュ値は適度に分散させるべき。

具体例を考える。

class Person:
    def __init__(self, firstname: str, lastname:str) -> None:
        self.firstname = firstname
        self.lastname = lastname

    # 氏/名がともに等しければ同値とする
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Person):
            return self.firstname == other.firstname and \
                   self.lastname == other.lastname
        return False

    # ハッシュ値を演算
    def __hash__(self) -> int:
        return hash((self.firstname, self.lastname))

if __name__ == '__main__':
    p = Person('太郎', '山田')
    dic = {p: ''}
    print(dic[p])    # 男

# ハッシュ値を演算 の部分で、オブジェクトの同値判定に関わる情報をタプルとしてまとめ、hash関数に纏めている。
→ これでオブジェクト全体としてのハッシュ値を求められる。
実際、dict型のキーとしてPersonクラスを利用できることが確認できる。

hashableであることの条件

ただし、厳密には上記のコードは不完全。hashableであるためには、
__hash__メソッドを持つこと
・生存期間中、ハッシュ値は変動してはならない(不変でなければならない)
から。
しかし、Personクラスのハッシュ値を構成する firstname/lastnameは変更可能。
以下のように書き換えると、KeyErrorを示す。

class Person:
    def __init__(self, firstname: str, lastname:str) -> None:
        self.firstname = firstname
        self.lastname = lastname

    # 氏/名がともに等しければ同値とする
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Person):
            return self.firstname == other.firstname and \
                   self.lastname == other.lastname
        return False

    # ハッシュ値を演算
    def __hash__(self) -> int:
        return hash((self.firstname, self.lastname))

if __name__ == '__main__':
    p = Person('太郎', '山田')
    dic = {p: 100}
    p.firstname = '次郎'
    print(dic[p])    # KeyError

このような問題を避けるために、ハッシュ値を算出するためのインスタンス変数はすべて不変(読み取り専用)とすべき。

class Person:
    __slots__ = ('__firstname', '__lastname')

    def __init__(self, firstname: str, lastname: str) -> None:
        # 二重下線は名前マングリングされて _Person__firstname などになる
        self.__firstname = firstname
        self.__lastname = lastname

    # 読み取り専用プロパティ
    @property
    def firstname(self) -> str:
        return self.__firstname

    @property
    def lastname(self) -> str:
        return self.__lastname

    # 等値性:氏名が一致すれば等しい
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Person):
            return (self.firstname, self.lastname) == (other.firstname, other.lastname)
        return False

    # ハッシュも氏名に基づく(== と整合)
    def __hash__(self) -> int:
        return hash((self.firstname, self.lastname))


if __name__ == '__main__':
    p = Person('太郎', '山田')
    dic = {p: 100}

    # 読み取り専用なので代入はエラー(AttributeError)
    try:
        p.firstname = '次郎'
    except AttributeError as e:
        print(type(e).__name__, e)    # AttributeError …

    # p は変化していないので辞書参照は 100 のまま
    print(dic[p])    # 100

このようにすれば、値の書き換えやメンバーの追加/削除を禁止できる(Pythonにおける隠蔽の非厳密性から、正確には不変でないが、一般的な用途であれば十分)。
→ しかし、特別な機能を求めないのならば、NamedTupleで充分であるし、そもそもデータクラスを利用した方がシンプル。

オブジェクトを四則演算する

演算子のオーバーロード:Pythonでは、+> のような演算子をクラス独自に再定義できる(datetime型の加減などがよい例)。
→ 四則演算や比較などの演算機能をシンプルかつ直観的に記述できる。

まずは数値演算に関わるメソッドについて扱う。
以下は、演算子とその対応表。

演算子 対応するメソッド
+ __add__
- __sub__
* __mul__
** __pow__
/ __truediv__
// __floordiv__
% __mod__

以下に具体例を示す。これは座標(Chondinate)クラスに対して、+ 演算子をオーバーロードする例。

from __future__ import anntations

class Coordinate
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # Coordinate同士の加算
    def __add__(self, other: Coordinate) -> Coordinate:
        return Coordinate(
            self.x + other.x,
            self.y + other.y
        )

    def __str__(self) -> str:
        return f'({self.x}, {self.y})'

if __name__ == '__main__':
    # Coordinate同士を計算
    c1 = Coordinate(10.5, 20.5)
    c2 = Coordinate(15.5, 25.5)
    print(c1 + c2)    # (26.0, 46.0)

+ 演算子によってオペランドに影響が出ないように、あくまで加算結果は「新規のインスタンス」として返すようにしている。
また、Coordinateオブジェクト同士の加算だけでなく、Coordinate型 + float型のような演算も可能。

from __future__ import anntations

class Coordinate
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # Coordinate同士の加算
    def __add__(self, other: Coordinate | float) -> Coordinate:
        # オペランドの型によって処理を分類
        if isinstance(other, Coordinate):
            # Coordinate同士の加算
            return Coordinate(
                self.x + other.x,
                self.y + other.y
            )
        elif:
        # それ以外の型は不可
        rasie TypeError('type must be Coordinate or float')

    def __str__(self) -> str:
        return f'({self.x}, {self.y})'

if __name__ == '__main__':
    # Coordinate同士を計算
    c1 = Coordinate(10.5, 20.5)
    c2 = Coordinate(15.5, 25.5)
    print(c1 + 10.5)    # (21.0, 20.5)
    print(c1 + c2)    # (26.0, 46.0)
    print(c1 + 'hoge')    # TypeError: type must be Coordinate or float

複合演算子の例

'+=' や '-=' 演算子のような複合演算子をオーバーロードすることも可能。

演算子 対応するメソッド
+= __iadd__
-= __isub__
*= __imul__
**= __ipow__
/= __itruediv__
//= __ifloordiv__
%= __imod__

これらのオーバーロードが存在しない場合、__add__, __sub__ など、本来の +, - 演算子で処理されるため、常に __add__ / __iadd__ 双方を実装しなければならないというわけではない。

from __future__ import annotations

class Coordinate:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # Coordinate同士の加算 (c1 += c2)
    def __iadd__(self, other: Coordinate) -> Coordinate:
        if isinstance(other, Coordinate):
            self.x += other.x
            self.y += other.y
        return self

    def __str__(self) -> str:
        return f'({self.x}, {self.y})'

if __name__ == '__main__':
    # Coordinate同士を計算
    c1 = Coordinate(10.5, 20.5)
    c2 = Coordinate(15.5, 25.5)
    c1 += c2
    print(c1)    # (26.0, 46.0)

今回は、自分自身への置き換えであるから、引数 self そのものを書き換え、戻り値も self としている。

右オペランドの演算ルールを実装

__rXXX__ メソッド:演算子の左オペランドがその演算子を実装していない場合に実行される

演算子 対応するメソッド
__radd__ 加算
__rsub__ 減算
__rmul__ 積算
__rpow__ 乗算
__rpow__ 除算
__rfloordiv__ 整数除算
__rmod__ 剰余

例えば、「left-right」であれば、

left.__sub__(right)

が呼び出され、未実装であった場合は、

right.__rsub__(left)

が呼び出される。

以下で具体的に、Left/Rightクラスを実装して挙動を確認する。Leftクラスには、__sub__ メソッドを、Rightクラスには __rsub__メソッドをそれぞれ実装している。

from __future__ import annotations

class Left:
    def __init__(self, value: int) -> None:
        self.value = value

    # 「-」演算子のオーバーロード
    def __sub__(self, other: Left) -> Left:
        print('Left#__sub__')
        return Left(self.value - other.value)

class Right:
    def __init__(self, value: int) -> None:
        self.value = value

    # 「-」演算子のオーバーロード(右オペランド)
    def __rsub__(self, other: Left) -> Left:
        print('Right#__rsub__')
        return Left(other.value - self.value)

if __name__ == '__main__':
    lf = Left(10)
    rt = Right(5)
    result = lf - rt
    print(result.value)
実行結果
Left#__sub__
5

上記の状況では、左オペランドLeftの __sub__ メソッドを呼び出す。
ここで、

    # 「-」演算子のオーバーロード
    def __sub__(self, other: Left) -> Left:
        print('Left#__sub__')
        return Left(self.value - other.value)

をコメントアウトすると、左オペランドが - 演算子をサポートしないため、右オペランドRightの __rsub__ メソッドが呼び出される。

実行結果
Right#__rsub__
5

また、class Right:を、class Right(Left): と書き換えると、右オペランドが左オペランドの派生クラスである場合。この場合は左オペランドの __sub__ メソッドの有無にかかわらずに、右オペランドの __rsub__ メソッドが呼び出される。

オブジェクト同士を比較する

Python では >, <, == などの演算子をオーバーロードすることで、オブジェクト同士の大小比較ができる。

演算子 対応するメソッド
== __eq__
!= __ne__
< __lt__
<= __le__
> __gt__
>= __ge__

以下は具体例。Coordinateクラスを用いて、大小比較をまとめて実装する。

from __future__ import annotations

class Coordinate:
    def __init__(self, x: floot, y: floot) -> None:
        self.x = x
        self.y = y

    # 「<」ルール
    def __lt__(self, pther: Coordinate) -> bool:
        return self.x ** 2 + self.y ** 2 \
               < other.x ** 2 + other.y ** 2

    # 「<=」ルール
    def __le__(self, other: Coordinate):
        return self.x ** 2 + self.y ** 2 \
               <= other.x ** 2 + other.y ** 2

    # 「>」ルール
    def __gt__(self, other: Coordinate) -> bool:
        return not self.__le__(other)

    # 「>=」ルール
    def __ge__(self, other: Coordinate) -> bool:
        return not self.__lt__(other)

    def __str__(self) -> str:
        return f'({self.x}, {self.y})'

if __name__ == '__main__':
    c1 = Coordinate(1, 2)
    c2 = Coordinate(15, 25)
    c3 = Coordinate(2, 1)

    print(c1 < c2)   # True
    print(c1 <= c3)  # True
    print(c1 >= c3)  # True
    print(c1 > c2)   # False

データ型を変換する

インスタンスを特定の型に変換する時も、特殊メソッドを定義できる。

演算子
__str__ 文字列型
__int__ 整数型
__float__ 浮動小数点型
__complex__ 複素数型
__bool__ 論理型
__bytes__ バイト型
__index__ インデックス値

以下に具体例を示す。

import math

class Coordinate:
    def __init__(self, x: floot, y: floot) -> None:
        self.x = x
        self.y = y

    def __init__(self) -> int:
        return int(self.__float__())

    def __float__(self) -> float:
        return math.sqrt(self.x ** 2 + self.y ** 2)

if __name__ == '__main__':
    c = Coordinate(1, 2)
    print(float(c))    # 2.2360679・・・
    print(int(c))      # 2

オブジェクトの真偽を判定する

bool演算コンテキスト:オブジェクトが真偽値として判断されるような状況
if 条件式として渡されたオブジェクトはbool演算コンテキストでは、True/False値として解釈される。

一般的なオブジェクトの真偽判定ルールは以下。
__bool__メソッドが True を返せば、オブジェクトは True
__bool__ が存在しない場合、 __len__ メソッドが非ゼロ値を変えせば、オブジェクトは True
__bool____len__ も存在しない場合、オブジェクトは常に True

以下に具体例を示す。Coordinateクラスに __bool/__len__メソッドを定義。
__bool__ メソッドは、Coordinateが原点 (0, 0)を表す場合に False、それ以外の場合は True を返し、__len__ メソッドは原点から座標までの距離を整数値で返すものとする。

import math

class Coordinate:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # 真偽判定のメソッド
    def __bool__(self) -> bool:
        print('__bool__'):
        return self.x != 0 or self.y != 0

    # オブジェクトの長さを求めるためのメソッド
    def __len__(self) -> int:
        print('__len__')
        return int(math.sqrt(self.x ** 2 + self.y ** 2))

if __name__ == '__main__':
    c = Coordinate(0, 0)
    if c:
        print('cはTrueです。')
    else:
        print('cはFlaseです。')
実行結果
__bool__
cはFalseです。

ここで、__bool__ は戻り値としてTrue/False を、__len__ は整数値を返すようにする。ここで、

# 真偽判定のメソッド
def __bool__(self) -> bool:
    print('__bool__'):
    return self.x != 0 or self.y != 0

をコメントアウトした場合は、結果が変化し、__len__ メソッドが呼び出されていることも確認できる。

実行結果
__len__
cはFalseです。

この例のように、__bool__/__len__ メソッドの真偽判定ルールが意味的に一致しているならば、最初から __len__ メソッドだけを実行してもかまわない。また、判定ルールを明示するならば、__bool__ メソッドから __len__ メソッドを明示的に呼び出してもよい。

また、以下の両方をコメントアウトした場合は、結果は無条件に True 扱いとなる。

    # 真偽判定のメソッド
    def __bool__(self) -> bool:
        print('__bool__'):
        return self.x != 0 or self.y != 0

    # オブジェクトの長さを求めるためのメソッド
    def __len__(self) -> int:
        print('__len__')
        return int(math.sqrt(self.x ** 2 + self.y ** 2))

属性の取得/設定の挙動をカスタマイズする

以下のメソッドを利用することで、属性の取得/参照時に共通の処理を加えることが可能になる。

メソッド 呼び出しのタイミング
__getattribute__(self, name) 属性を取得する時(常時)
__getattr__(self, name) 属性を取得する時(存在しない時)
__setattr__(self, name, value) 属性を設定する時
__delattr__(self, name) del命令で属性を削除する時
__dir__(self) インスタンスがdir関数で呼び出されたとき

属性の取得/設定

以下に具体例を示す。MyInfoクラスは、任意の情報を配下の辞書に格納する。

from typing import Any

class MyInfo:
    # 属性格納のための__data(辞書)を準備
    def __init__(self) -> None:
        super().__setter__('__data', {})

    # 指定された属性を__dataから取得
    def __getattr__(self, name: str) -> Any:
        try:
            return super().__getattribute__('__data')[name]
        except KeyError as ex:
            return None

    # 指定された属性を__dataに格納
    def __setattr__(self, name: str, value: Any) -> None:
        super().__getattribute__('__data')[name] = value

if name == '__main__':
    i = MyInfo()
    i.score = 58
    i.hobby = '卓球'
    print(i.hobby)       
    print(i.__dict__)    # {'__data': {'score': 58, 'hobby': '卓球'}}

順に解説する。
属性の格納先を準備する

    # 属性格納のための__data(辞書)を準備
    def __init__(self) -> None:
        super().__setter__('__data', {})

MyInfoクラスでは、任意の属性をdict型の変数 __dict で管理している。初期化メソッドを用いて、まずは空の辞書を用意。

__data への代入に self.__data = {} としていないのは、__setter__ メソッドが呼び出されてしまうから。
→ この場合、__setattr__ は辞書 __dataの配下に値を追加しようとするが、まだ __data が存在しないので、AttributeErrorを吐く。
→ そこで、基底クラス( super )の __setattr__メソッドを呼び出して、現在のクラスの __setattr__ メソッドを経由するのを避けている。

属性を辞書から取り出す

    # 指定された属性を__dataから取得
    def __getattr__(self, name: str) -> Any:
        try:
            return super().__getattribute__('__data')[name]
        except KeyError as ex:
            return None

__getattr メソッド:属性取得の際に呼び出されるメソッド
基底クラスの __getattribute__ メソッドで __data を取得し、name(属性名)をキーに取得。__data にキーが存在しない(KeyError が発生した)場合は None を返す。
また、__data を取得するに際して self.__data[name] としないのは、上と同じ理由。この場合、__data (内部的には _MyInfo__data) へのアクセス時に、__getattr__ メソッドが呼び出されるため、正しい値を取得できない(無限ループをおこす)

属性を辞書に設定する

    # 指定された属性を__dataに格納
    def __setattr__(self, name: str, value: Any) -> None:
        super().__getattribute__('__data')[name] = value

__setattr__ メソッド:属性決定の際に呼び出されるメソッド
基底クラスの__getattr__ メソッドで __data を取得し、値を設定している。

ディスクリプタ―

メソッド 呼び出しのタイミング
__get__(self, obj, type) 属性を取得する時
__set__(self, obj, value) 属性を設定する時
__delattr__(self, obj) 属性を削除する時

__getattr__/__setattr__などのメソッドにも似ているが、ディスクリプタ―を用いることで

・属性操作に関わる挙動を別クラスに切り出せる(= クラスをまたがる属性関連の機能を再利用できる)
・特定の属性に対してのみ適用される挙動を定義できる( __getattr__/__setattr__では、そのクラスの全ての属性に影響)

などのメリットがある。プロパティ/クラス/静的メソッド/superなどの裏側でも走っている。
以下に具体例を示す。LogPropディスクリプターは、値の取得/設定に際して、その内容を出力する。

from typing import Any

# ディスクリプターの定義
class LogProp:
    # 対象の属性名(name)を設定
    def __init__(self, name: str) -> None:
        self.nname = name

    # 属性取得時の処理
    def __get__(self, obj: object, t: type) -> Any:
        print(f'{self.name}: get')
        return obj.__dict__[self.name]

    # 属性設定時の処理
    def __set__(self, obj: object, value: Any) -> None:
        print(f'{self.name}: set {value}')
        obj.__dict__[self.name] = value

class App:
    # ディスクリプターを定義
    title = LogProp('title')

if __name__ == '__main__':
    app = App()
    app.title = '独習Python'
    print(app.title)
実行結果
title: set 独習Python
title: get
独習Python

ディスクリプターの側で定義しているのは、__init__/__get__/__set__
__init__ メソッド:ディスクリプターの対象となる属性名を受け取り、これを保存する。後で値を受け渡しするためのキーとなる情報である。
__get__, __set__メソッド:値受け渡しのための主となるメソッド。引数経由で以下の情報を受け取る。

obj:対象となるインスタンス
type:対象となるクラス(typeオブジェクト)
value:渡された値

上の例であれば、__dict__を介してインスタンスの値を取得/参照している。__get__/__set__ はインスタンスの属性操作を肩代わりするので、何らかの値の取得/保存操作がない場合、属性値の取得/設定は無効になる(ディスクリプターよっては、必ずしも保存先は __dict__ でなくてもかまわない)。

インスタンスを関数的に呼び出す

__call__ メソッド:オブジェクトが関数形式で呼び出された際にコールするメソッド

早速、具体例を示す。Coordinateクラスに _call_ メソッドを追加した例。__call__ メソッドは、引数として座標 x, y を受け取り、現在の座標との距離を返すものとする。

import math

class Coordinate:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    # c(x, y)形式で呼び出し、距離を求める
    def __call__(self, o_x: float, o_y: float) -> float:
        return math.sqrt(
        (o_x - self.x) ** 2 + (o_y - self.y) ** 2)
        )

if __name__ == '__main__':
    c = Coordinate(10, 20)
    print(c(5, 15))    # 7.0710678118654755

他の特殊メソッドと異なり、__call__ メソッドは任意戸数の引数を受け取り、任意の戻り値を返すことが出来る点に注目。
上の例では、比較する座標 (o_x, o_y) を受け取り、座標間の距離を返す。

    print(c(5, 15))    # 7.0710678118654755

でも関数構文で、__call__ メソッドが呼び出せていることが確認できる。これはメタクラスなどでも現れる。

参考文献

[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?