🐍

精読「独習Python」(オブジェクト指向構文 応用)

2025/01/03に公開



独習Python
「独習Python」は、初心者から中級者までを対象に、Pythonの基礎から応用までを体系的に学べる入門書で、豊富な例題と練習問題を通じて実践的なスキルを身につけられる一冊です。

関連記事

例外処理

例外クラスの型

例外をクラス型で指定する方法は次のようになる

try:
    # エラーが発生するコード
    x = 1 / 0  # ZeroDivisionErrorが発生
except ZeroDivisionError as e:
    print(f"ゼロで割り算しました: {e}")
except Exception as e:
    print(f"その他のエラーが発生: {e}")

終了処理を定義するーfinally節

Pythonのtry...except構文におけるfinally節は、例外の有無にかかわらず必ず実行され、主にリソースの後始末(クリーンアップ)に使用される。finally節は1つしか指定でき、try節でリソースを解放し忘れないようにするために重要。例えば、ファイル操作などでfile.close()finallyで呼び出すことで、例外発生時でもリソースを確実に解放できる。

また、with...as構文を使うと、リソースの解放が自動的に行われるため、finallyよりシンプルに記述できる。with節内で発生した例外は、コンテキストマネージャーの__exit__メソッドで処理され、return Trueを使うとその後の処理が中断され、Falseにすると例外が再送出される。

finally節の実行順序は、基本的にtry→(except)→finallyfinallyは、try節でreturnbreakcontinueが発生しても必ず実行され、その後の処理に影響を与える。特に、finally節内でreturnがあった場合、finallyreturnが優先される。

例外をスローするーraise命令

raise命令は、Pythonで例外を発生させるための命令。これにより、プログラムの実行中に意図的にエラーを発生させ、エラーハンドリングの仕組みを通じて適切に処理することができる。

try:
    # 何らかの処理
    raise ValueError("エラーが発生しました")
except ValueError as e:
    print("エラー:", e)
    raise  # 現在の例外を再送出

独自の例外クラス

ythonでは、独自の例外クラスを作成して、アプリケーション固有のエラーハンドリングを行うことができる。独自の例外クラスは、標準の例外クラスを継承することで作成する。これにより、標準的なエラーハンドリング機構を利用しつつ、特定のエラーを明示的に区別することができる

class InvalidUserInputError(Exception):
    def __init__(self, message="無効な入力がありました"):
        # Exceptionの__init__メソッドを呼び出し
        super().__init__(message)
        self.message = message

# 使用例
def get_user_input(value):
    if value < 0:
        raise InvalidUserInputError("入力値は負の数ではありません。")
    return value

try:
    get_user_input(-1)
except InvalidUserInputError as e:
    print(f"エラー: {e}")

特殊メソッド

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

__str__ メソッドはエンドユーザー向けのわかりやすい文字列を返し、print(obj) で呼ばれる。オブジェクトの特徴的な情報を簡潔に表現する。

# Personクラスを定義
class Person:
    # コンストラクタ: インスタンスを初期化
    def __init__(self, firstname, lastname):
        self.firstname = firstname  # 姓をインスタンス変数に格納
        self.lastname = lastname    # 名をインスタンス変数に格納
    
    # __str__メソッドを定義: オブジェクトの文字列表現を返す
    def __str__(self):
        return f"{self.firstname} {self.lastname}"  # フルネームを返す

# Personクラスのインスタンスを作成
person = Person("太郎", "山田")

# print関数でpersonオブジェクトを出力
print(person)  # 結果: 太郎 山田

__repr__ メソッドは開発者向けで、オブジェクトを再生成可能な文字列を返し、eval() などで使用されます。

class Person:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname
    
    def __repr__(self):
        return f"Person('{self.firstname}', '{self.lastname}')"

person = Person("太郎", "山田")
print(repr(person))  # 結果: Person('太郎', '山田')

repr(person) で、オブジェクトの再生成が可能な文字列を表示する。eval(repr(person)) を使えば元のオブジェクトを再生成できる。

__str__ はユーザー向け、__repr__ は開発者向けに使い分ける

オブジェクト動詞が等しいかどうかを判定する

__eq__ メソッドは、オブジェクト同士の同値性を判定するためのメソッドで、== 演算子が使われると呼び出される。デフォルトでは、object クラスが提供する __eq__ は同一性(参照が同じかどうか)を確認するだけ。意味的に同じかどうかを判定するためには、__eq__ をオーバーライドして比較基準を定義する必要がある。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __eq__(self, other):
        # 型が異なる場合は False
        if not isinstance(other, Person):
            return False
        # name と age が一致する場合に True
        return self.name == other.name and self.age == other.age

# 使用例
person1 = Person('太郎', 30)
person2 = Person('太郎', 30)
person3 = Person('次郎', 25)

# 同じ名前と年齢なので同値
print(person1 == person2)  # True

# 名前が異なるので同値ではない
print(person1 == person3)  # False

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

__hash__ メソッドは、オブジェクトのハッシュ値を返すために使われ、dictset での検索に利用される。同値のオブジェクトは同じハッシュ値を返すべきですが、異なるオブジェクトが同じハッシュ値を返しても問題ない。重要なのは、オブジェクトのハッシュ値は変更されないこと。変更可能な変数を使うと、検索時にエラーが発生することがある。そのため、__hash__ を実装する際は、不変の変数を使用するべき。

class Person:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname
    
    def __eq__(self, other):
        return (self.firstname, self.lastname) == (other.firstname, other.lastname)
    
    def __hash__(self):
        return hash((self.firstname, self.lastname))

# ハッシュ表で利用
person1 = Person("太郎", "山田")
person2 = Person("太郎", "山田")
person3 = Person("次郎", "山田")

# 同じハッシュ値を持つか確認
print(hash(person1))  # person1のハッシュ値
print(hash(person2))  # person2のハッシュ値
print(hash(person3))  # person3のハッシュ値

# dictを使った例
person_dict = {person1: "社員A", person3: "社員B"}
print(person_dict[person2])  # person1とperson2は同じハッシュ値なので、"社員A"が返る

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

演算子をオーバーロード(再定義)することで、カスタムクラスのオブジェクトに対して四則演算や比較を直感的に行えるようにできる。これを行うには、__add__(加算)、__sub__(減算)などの特殊メソッドを定義する。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # + 演算子のオーバーロード
    def __add__(self, other):
        if isinstance(other, Point):
            return Point(self.x + other.x, self.y + other.y)
        else:
            raise TypeError("Can only add Point to Point")

    def __str__(self):
        return f"({self.x}, {self.y})"

# 使用例
p1 = Point(1, 2)
p2 = Point(3, 4)

# Pointオブジェクト同士を加算
p3 = p1 + p2

print(p3)  # 結果: (4, 6)

データ型を変換する

__int____float__ メソッドを使って、Coordinate クラスのインスタンスを整数や浮動小数点数に変換する例。

  • __float__ メソッドは、座標の原点からの距離を計算し、浮動小数点数として返す。
  • __int__ メソッドは、__float__ を呼び出して得た距離を整数に変換して返す。

これにより、Coordinate オブジェクトに対して float()int() 関数を使って数値表現を取得できるようになる

import math

class Coordinate:
  def __init__(self, x, y):
    self.x = x
    self.y = y

  def __int__(self):
    return int(self.__float__())

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

# 使用例
c = Coordinate(1, 2)
print(float(c))    # 結果:2.23606797749979
print(int(c))      # 結果:2

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

オブジェクトの真偽判定は以下のルールで行われる

  • __bool__ が定義されていれば、その値が TrueFalse になる。
  • __bool__ がない場合、__len__ が使われ、長さがゼロなら False、それ以外は True
  • 両方がない場合、オブジェクトは常に True

アトリビュートの取得/設定の挙動をカスタマイズする

アトリビュートの取得・設定の挙動をカスタマイズするためには、__getattr____getattribute____setattr____set__ などの特殊メソッドを実装する。これにより、オブジェクトがアトリビュートにアクセスする際の動作を制御できる。

  • __getattr__(アトリビュートが存在しないときに呼ばれる)
    __getattr__は、オブジェクトがアクセスするアトリビュートが存在しない場合に呼び出される。このメソッドを使って、動的にアトリビュートの値を生成したり、デフォルト値を返したりできる。

  • __getattribute__(すべてのアトリビュートアクセスで呼ばれる)
    __getattribute__は、すべてのアトリビュートアクセス時に呼び出されるため、アトリビュートが存在するかどうかに関係なく実行される。通常は__getattr__の方が優先されますが、__getattribute__でのアクセスをカスタマイズできる。

  • __setattr__(アトリビュートが設定されるときに呼ばれる)
    __setattr__はアトリビュートに値を設定する際に呼び出される。ここで、設定の前に検証を行ったり、特殊な処理を加えることができる。

  • __set__(プロパティで使用される)
    __set__は、@propertyデコレータやカスタムのプロパティに使用される。これを使うと、アトリビュートが設定される際にカスタマイズしたロジックを実行できる。

class Person:
    def __init__(self, name, age):
        self.name = name  # __setattr__が呼ばれます
        self.age = age    # __setattr__が呼ばれます

    # アトリビュートの設定時にカスタマイズ(例:名前が空文字だった場合にデフォルト値を設定)
    def __setattr__(self, name, value):
        if name == "name" and not value:
            value = "Unknown"  # 名前が空ならデフォルト値
        super().__setattr__(name, value)

    # アトリビュートの取得時にカスタマイズ(例:年齢が負の場合にエラーメッセージを返す)
    def __getattr__(self, name):
        if name == "age":
            raise ValueError("Age is not accessible directly!")
        return super().__getattr__(name)

    # アトリビュートの取得時にカスタマイズ(例:設定した名前を小文字にする)
    def __getattribute__(self, name):
        if name == "name":
            return super().__getattribute__(name).lower()  # 名前を小文字で返す
        return super().__getattribute__(name)

# 使用例
p = Person("Alice", 30)
print(p.name)  # 結果: alice
print(p.age)   # 結果: 30(Error: Age is not accessible directly!は発生しません)

p.name = ""  # 名前が空ならデフォルトの'Unknown'を設定
print(p.name)  # 結果: unknown

ディスクリプター

Pythonのディスクリプターは、属性のアクセス、設定、削除を制御するためのオブジェクト。以下のメソッドを実装することで作成できる

  • __get__: 属性の取得時に呼ばれる
  • __set__: 属性の設定時に呼ばれる
  • __delete__: 属性の削除時に呼ばれる
class Descriptor:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        print(f"Getting {self.name}")
        return instance.__dict__.get(self.name, None)

    def __set__(self, instance, value):
        print(f"Setting {self.name} to {value}")
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        print(f"Deleting {self.name}")
        del instance.__dict__[self.name]

class MyClass:
    attr = Descriptor("attr")

obj = MyClass()
obj.attr = 10  # Setting attr to 10
print(obj.attr)  # Getting attr
del obj.attr  # Deleting attr

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

Pythonでインスタンスを関数的に呼び出すには、__call__メソッドを定義する。このメソッドを実装することで、インスタンスを関数のように呼び出すことができる。

class CallableClass:
    def __init__(self, value):
        self.value = value

    def __call__(self, x):
        return self.value * x

# インスタンスを関数のように呼び出す
instance = CallableClass(5)
result = instance(10)  # 5 * 10 = 50
print(result)  # 50

データクラス

データクラスの基本

簡単にクラスを定義して、主にデータを格納するためのもの。データクラスを使うと、__init____repr__などの特殊メソッドを自動的に生成してくれる。

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

# インスタンスを作成
person = Person(name="Alice", age=30)

# 属性にアクセス
print(person.name)  # Alice
print(person.age)   # 30

dataclassデレコーターの主なオプション

オプション 説明 デフォルト値
init __init__メソッドの自動生成を有効/無効にする True
repr __repr__メソッドの自動生成を有効/無効にする True
eq __eq__メソッドの自動生成を有効/無効にする True
order __lt__, __le__, __gt__, __ge__メソッドの自動生成を有効/無効にする False
unsafe_hash __hash__メソッドの自動生成を有効/無効にする False
frozen 属性を変更不可にする(イミュータブル) False

フィールドのカスタマイズ

dataclassでは、フィールドに対してカスタマイズを行うためにfield()関数を使用する。field()を使うことで、属性のデフォルト値やその他のオプションを制御できる。

@dataclass
class Person:
    name: str
    age: int
    _id: int = field(init=False)  # __init__メソッドには含めない

イミュータブルなクラス

dataclassを使ってイミュータブルなクラスを作成する方法は、frozen=Trueオプションを使用すること。これにより、インスタンスが生成された後にフィールドの値を変更できなくなる。frozen=Trueを指定すると、Pythonは__setattr__メソッドを無効化し、オブジェクトのフィールドに対する変更を防ぐ。

from dataclasses import dataclass

@dataclass(frozen=True)
class Person:
    name: str
    age: int

# インスタンスの生成
p = Person(name="Alice", age=30)

# フィールドへの変更を試みるとエラーが発生します
# p.age = 31  # AttributeError: can't set attribute

メタクラス

メタクラスは、クラスを作成するためのクラス。通常、Pythonではクラスを使ってオブジェクトを作成しますが、メタクラスはそのクラスを作成するために使われる。つまり、クラス自体を操作するためのクラス。

クラスがオブジェクトの設計図であるのに対して、メタクラスはクラスの設計図。

メタクラスの基本

通常のクラスは、class キーワードを使って定義しますが、メタクラスは type クラス(もしくは他のカスタムメタクラス)を使って定義する。メタクラスを利用することで、クラスがどのように生成されるか、またはクラスの振る舞いをカスタマイズできる。

メタクラスの使い方

メタクラスを作成するには、type を継承して、__new__() または __init__() メソッドをオーバーライドします。__new__() はクラスの作成時に呼ばれ、__init__() は作成されたクラスの初期化時に呼ばれる。

メタクラスの例

# メタクラスを定義
class MyMeta(type):
    def __new__(cls, name, bases, dct):
        print(f"Creating class {name}")
        return super().__new__(cls, name, bases, dct)

# メタクラスを使ったクラス定義
class MyClass(metaclass=MyMeta):
    pass

# インスタンス生成
obj = MyClass()

メタクラスの用途

メタクラスは、以下のような用途で使用されます:

  • クラスの作成時に自動的に属性やメソッドを追加する
  • クラスの継承やインスタンス化のルールを変更する
  • クラス定義のバリデーションを行う
  • クラスのインスタンス化時に特別な処理を追加する

例:クラス定義時に自動でメソッドを追加する

class MethodAddingMeta(type):
    def __new__(cls, name, bases, dct):
        # クラスにメソッドを追加
        def greet(self):
            return "Hello from the class!"
        dct['greet'] = greet
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=MethodAddingMeta):
    pass

obj = MyClass()
print(obj.greet())  # "Hello from the class!"

メタクラスの実用的な使用例

メタクラスは、Pythonでフレームワークやライブラリを作成する際によく使われる。例えば、DjangoのORM(Object-Relational Mapping)や、構造化されたデータを扱うライブラリでは、メタクラスを使ってクラス定義の際に自動で設定やチェックを行っている。

まとめ

  • メタクラスは、クラスを生成するためのクラス。
  • メタクラスを使うことで、クラスの生成時にカスタマイズや拡張を行うことができる。
  • type を基にしてカスタムメタクラスを定義し、クラスの動作や構造を制御することができる。

参考


Discussion