🐈

Act 07. Pythonのクラスとオブジェクト指向プログラミング

2024/10/27に公開1

はじめに

Act 01. AIで外国為替を自動売買するまでの道のりをベースに学習を進めて行く。

普段はインフラ屋だからPythonが楽しくて楽しくて。
今日もきっちり学んでしっかりと記事を書いていこうー

今まで業務でdefを使うことはあったがclassを使うことはあまりなかった。
Pythonではclassというものが存在し、それを使うことでいくつか利点があるらしい。

ChatGTPさんに聞いたところ、以下のような回答を貰った。

Pythonにおけるclass(クラス)を使う利点はいくつかあります。以下に主要なポイントを挙げます。

  1. カプセル化: クラスを使うことで、データ(属性)とそれに対する操作(メソッド)を一つの構造体にまとめることができます。これにより、データを隠蔽し、外部からの不正アクセスを防ぎやすくなります。

  2. 再利用性: クラスを定義すると、そのクラスを基に新しいオブジェクトを簡単に作成できます。これにより、コードの再利用が可能になり、開発効率が向上します。

  3. 継承: 既存のクラスを基に新しいクラスを作成することで、コードの重複を避け、共通の機能を持つクラスを簡単に作成できます。これにより、プログラムの構造を整理しやすくなります。

  4. ポリモーフィズム: 異なるクラスのオブジェクトが同じインターフェースを持つことができ、異なるクラスのオブジェクトに対して同じ操作を適用できるようになります。これにより、柔軟性の高いコードを書くことができます。

  5. 抽象化: クラスを使うことで、実装の詳細を隠し、ユーザーが使いやすいインターフェースを提供できます。これにより、コードの理解や保守が容易になります。

これらの利点により、クラスは大規模なプログラムや複雑なシステムを構築する際に非常に有用な手段となります。

なるほど。何となくイメージはつく。
今回は実際に上記の利点を感じてみようと思う。

1. カプセル化

まずはカプセル化について。
以下のような銀行口座クラスを作ってみた。

__balance__(アンダースコア2つ)でPrivateの変数として定義している。
そのため外部から変数を直接操作することが出来ない。

Act07.py
class BankAccount:
    def __init__(self, balance=0):
        self.__balance = balance  # 残高(Private属性)

    def deposit(self, amount):
        """
        預け入れ
        """
        if amount > 0:
            self.__balance += amount

    def withdraw(self, amount):
        """
        引き出し
        """
        if 0 < amount <= self.__balance:
            self.__balance -= amount

    def get_balance(self):
        """
        残高照会
        """
        return self.__balance

account_1 = BankAccount(0)
print(f"残高: {account_1.get_balance()}")
account_1.__balance = 10000
print(f"残高: {account_1.get_balance()}")

試しに実行してみると出力は以下の通り。操作出来てない。

残高: 0
残高: 0

ではどのように残高を操作するかというと、それ専用の関数を作成しそれを実行する。

Act07.py
account_1 = BankAccount(0)
print(f"残高: {account_1.get_balance()}")
account_1.deposit(9999)
print(f"残高: {account_1.get_balance()}")

出力は以下の通り。残高が増加していることが確認できた。

残高: 0
残高: 9999

今回はPrivate属性を試してみたが、Public属性も試してみる。
__balanceからbalanceに変更して同じコードを実行してみる。

Act07.py
class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance  # 残高(Public属性)

    def deposit(self, amount):
        """
        預け入れ
        """
        if amount > 0:
            self.balance += amount

    def withdraw(self, amount):
        """
        引き出し
        """
        if 0 < amount <= self.balance:
            self.balance -= amount

    def get_balance(self):
        """
        残高照会
        """
        return self.balance

account_1 = BankAccount(0)
print(f"残高: {account_1.get_balance()}")
account_1.balance = 10000
print(f"残高: {account_1.get_balance()}")

出力は以下の通り。
balanceはPublic属性であるため、外部(クラス外)から変更が行える。

残高: 0
残高: 10000

こんな感じで変数に対する操作を制御したい場合にカプセル化という概念は大事なんだね。
開発者が複数存在する場合は、意図しない操作を防ぐために便利そうだなと感じた。

2. 再利用性

次に再利用性。
これはかなり簡単な話な気がする。(認識があっていれば)

さっき定義したBankAccountクラスを使う。
※BankAccountクラスの中身は同じ

Act07.py
account_1 = BankAccount(10000)
account_2 = BankAccount(20000000)
print(f"Account1の残高: {account_1.get_balance()}")
print(f"Account2の残高: {account_2.get_balance()}")

出力は以下の通り。
同じクラスを使って複数の口座を管理することが出来ている。これが再利用性ってことであってるよね?

Account1の残高: 10000
Account2の残高: 20000000

3. 継承

次に継承。
これはよく聞く便利な機能。(実際に使ったことはない。)

今回もBankAccountクラスを使用する。

楽天銀行とローソン銀行のいずれも、残高照会、預け入れ、引き出しの機能があるため、それに関するメソッドを定義しているBankAccountのクラスを継承して楽天銀行とローソン銀行のクラスを作ろうと思う。

楽天銀行には楽天ポイントを、ローソン銀行にはローソンATMで引き出したときにクーポンがもらえるか否かのフラグを追加した。

Act07.py
class BankAccount:
    def __init__(self, balance=0):
        self.__balance = balance  # 残高(プライベート変数)

    def deposit(self, amount):
        """
        預け入れ
        """
        if amount > 0:
            self.__balance += amount

    def withdraw(self, amount):
        """
        引き出し
        """
        if 0 < amount <= self.__balance:
            self.__balance -= amount

    def get_balance(self):
        """
        残高照会
        """
        return self.__balance

class RakutenBankAccount(BankAccount):
    """
    楽天銀行
    """

    def __init__(self, balance=0):
        super().__init__(balance)
        self.__point = 1000

    def get_point(self):
        """
        楽天ポイント照会
        """
        return self.__point

class LawsonBankAccount(BankAccount):
    """
    ローソン銀行
    """

    def __init__(self, balance=0):
        super().__init__(balance)
        self.atm_coupon = True

rakuten_account = RakutenBankAccount(10000)
lawson_account = LawsonBankAccount(20000000)
print(f"楽天銀行アカウントの残高: {rakuten_account.get_balance()}")
print(f"楽天ポイント: {rakuten_account.get_point()}")
print(f"ローソン銀行アカウントの残高: {lawson_account.get_balance()}")
print(f"ローソン銀行ATMのクーポン発行フラグ: {lawson_account.atm_coupon}")

出力は以下の通り。
lawson_accountでは.get_point()を呼び出すことが出来ない。
そんなメソッドが存在しないよって怒られる。

楽天銀行アカウントの残高: 10000
楽天ポイント: 1000
ローソン銀行アカウントの残高: 20000000
ローソン銀行ATMのクーポン発行フラグ: True

ベースとなるクラスを定義し、それを継承することで不要な記述を減らすことが出来るね。便利。

4. ポリモーフィズム

次にポリモーフィズムについて。
これに関しては聞いたことはあるけど何かは全く知らない。

まず結論から、ポリモーフィズムとは異なるクラスのオブジェクトが同じインターフェースを持ち、同じメソッドを呼び出すことで異なる動作をすることができるという考え方である。
カプセル化や継承などの機能とは違って、概念のようなものだね。

ゲームを例に記載する。
とりあえず以下のようなコードを作ってみた。

Act07.py
class Character:
    def attack(self):
        raise NotImplementedError("Subclasses must implement this method")

class Warrior(Character):
    def attack(self):
        return "Warrior attacks with a sword!"

class Mage(Character):
    def attack(self):
        return "Mage casts a fireball!"

def perform_attack(character):
    print(character.attack())

# 使用例
warrior = Warrior()
mage = Mage()

perform_attack(warrior)
perform_attack(mage)

これを実行すると出力は以下の通りになる。
それぞれのクラスのキャラクターが攻撃するメソッドを呼び出している。

戦士はソードで攻撃、魔法使いはファイアーボールで攻撃。うんうん、ここまでは問題ない。

Warrior attacks with a sword!
Mage casts a fireball!

ではどの部分がポリモーフィズムなのか。
本来、戦士クラスはattackメソッドではなくslashメソッドの方が戦士っぽい。
魔法使いもattackメソッドではなくmagicメソッドの方が魔法使いっぽい

けど、「結局どちらも攻撃に関することだからattackメソッドで統一しようね」っていうのがポリモーフィズムらしい。

確かに、今回の例でattackメソッドの名前を異なるものにしたら以下の部分でエラーが発生する。

Act07.py
def perform_attack(character):
    print(character.attack())

同じメソッド名で異なる処理を実装することで、異なるオブジェクトが同じ操作に応じて異なる動作をすることを可能にするっていうのはこういうことなのか。。

何となく理解した。
raise NotImplementedError("Subclasses must implement this method")の部分で何しているのかは、次の5. 抽象化で説明する。

5. 抽象化

続いて抽象化について。
ポリモーフィズムのコードを使って説明する。

以下のraise ~の部分が抽象化を行っている個所。

Act07.py
class Character:
    def attack(self):
        raise NotImplementedError("Subclasses must implement this method")

では抽象化を行うことでどうなるか。

  • サブクラスでの実装を強制
    親クラスのattackメソッドは、サブクラス(WarriorやMageなど)で具体的に実装されることを期待する。このメソッドが実装されずに呼び出されると、NotImplementedErrorが発生。これにより、意図しない動作を防ぐことが可能になる。

    今回の場合はattackメソッド抽象化しており、継承先のクラスでは定義が必須となる。

  • 明示的な警告
    このエラーは、プログラマに「このメソッドは実装されていないため、サブクラスで具体的に実装する必要がある」という明確なメッセージを提供する。

  • 設計の意図を示す
    抽象メソッドを使うことで、設計上の意図を明確にし、コードの可読性を向上させる。

では実際に、継承したクラスでattackメソッドを定義しなかったらどうなるのか。
Mageクラスでattackメソッドではなくmagicメソッドに変更してみた。

Act07.py
class Mage(Character):
    """
    魔法使いクラス
    """
    def magic(self):
        return "Mage casts a fireball!"

この状態でコードを実行してみる。
すると以下のエラーが発生。

Warrior attacks with a sword!
Traceback (most recent call last):
  File "/home/onishi/python/Act07.py", line 81, in <module>
    perform_attack(mage)
  File "/home/onishi/python/Act07.py", line 74, in perform_attack
    print(character.attack())
          ^^^^^^^^^^^^^^^^^^
  File "/home/onishi/python/Act07.py", line 54, in attack
    raise NotImplementedError("Subclasses must implement this method")
NotImplementedError: Subclasses must implement this method

mage = Mage()の部分ではエラーが発生しない。
perform_attack関数内のprint(character.attack())でエラーが発生する。

なるほど。

mage = Mage()でインスタンス化する時にエラーが発生するようにしてほしいなー。
というときはabcモジュールを使うといい。

@abstractmethodを抽象化したいメソッドの上に記載してあげる。
さらに@abstractmethodを使うクラスはABCクラスを継承する。

Act07.py
from abc import ABC, abstractmethod
class Character(ABC):
    """
    各キャラクーのベースクラス
    """
    @abstractmethod
    def attack(self):
        pass  # サブクラスで実装する必要がある

class Warrior(Character):
    """
    戦士クラス
    """
    def attack(self):
        return "Warrior attacks with a sword!"

class Mage(Character):
    """
    魔法使いクラス
    """
    def magic(self):
        return "Mage casts a fireball!"

def perform_attack(character):
    """
    attackメソッドの呼び出し
    """
    print(character.attack())

# 使用例
warrior = Warrior()
mage = Mage()

perform_attack(warrior)
perform_attack(mage)

この状態で実行してみると以下の遠り。
mage = Mage()の部分でエラーが発生している。いいね!

Traceback (most recent call last):
  File "/home/onishi/python/Act07.py", line 79, in <module>
    mage = Mage()
           ^^^^^^
TypeError: Can't instantiate abstract class Mage without an implementation for abstract method 'attack'

さいごに

抽象化やポリモーフィズムは全く知らなかったのでいい勉強になった。
ただ、一人で開発している時に使う機会はあるのか。。

コードを見直す時に可読性は上がるかもしれないから、もし余裕があれば取り入れて行こうと思う。

ではまた

Discussion