Python入門 第15回:オブジェクト指向

Python入門 第15回:オブジェクト指向 Python

はじめに

前回は、Pythonのクラスについて学習しました。

クラスを使うと、データと処理をひとつにまとめて管理できます。

class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):
        print(f"{self.name}がワンワンと鳴きました")
dog = Dog("ポチ")
dog.bark()

実行結果は次のとおりです。

ポチがワンワンと鳴きました

このように、クラスを設計してプログラムを組み立てる考え方を、一般的にオブジェクト指向と呼びます。

オブジェクト指向には、代表的な考え方として次の3つがあります。

  • 継承
  • カプセル化
  • ポリモーフィズム

言葉だけを見ると少し難しそうですが、初心者の段階ではすべてを完璧に理解する必要はありません。

まずは、それぞれがどのような考え方なのかを簡単なコードで確認していきましょう。

オブジェクト指向とは

オブジェクト指向とは、プログラムを「物」や「役割」の集まりとして考える設計方法です。

例えば、ゲームを作る場合を考えてみましょう。

ゲームには、次のような登場人物が存在します。

  • プレイヤー
  • モンスター
  • アイテム
  • 武器
  • 魔法

これらをそれぞれクラスとして定義し、クラスからインスタンスを作成します。

class Player:
    def __init__(self, name, hp):
        self.name = name
        self.hp = hp

    def show_status(self):
        print(f"名前:{self.name}")
        print(f"HP:{self.hp}")
player = Player("勇者", 100)
player.show_status()

実行結果は次のとおりです。

名前:勇者
HP:100

Playerクラスには、プレイヤーが持つ名前やHPといったデータと、状態を表示する処理がまとめられています。

このように、関連するデータと処理をクラスにまとめることで、プログラムの構造を整理しやすくなります。


継承とは

継承とは、あるクラスの機能を引き継いで、新しいクラスを作る仕組みです。

継承元となるクラスを、次のように呼びます。

  • 親クラス
  • 基底クラス
  • スーパークラス

継承して作られたクラスは、次のように呼ばれます。

  • 子クラス
  • 派生クラス
  • サブクラス

例えば、動物を表すAnimalクラスを作成してみましょう。

class Animal:
    def eat(self):
        print("食べ物を食べます")

このAnimalクラスを継承して、犬を表すDogクラスを作成します。

class Dog(Animal):
    def bark(self):
        print("ワンワン")

クラス名の後ろに丸括弧を書き、その中に継承したいクラス名を指定します。

class 子クラス名(親クラス名):
    処理

それでは、Dogクラスからインスタンスを作成してみましょう。

dog = Dog()

dog.eat()
dog.bark()

実行結果は次のとおりです。

食べ物を食べます
ワンワン

Dogクラスには、eat()メソッドを直接定義していません。

しかし、DogクラスはAnimalクラスを継承しているため、親クラスのeat()メソッドを使用できます。

一方、bark()メソッドはDogクラスで新しく追加した機能です。

つまり、継承を使うと、親クラスの機能を再利用しながら、子クラス独自の機能を追加できます。


継承を使わない場合

継承を使わずに犬や猫のクラスを作成すると、同じ処理を何度も記述することがあります。

class Dog:
    def eat(self):
        print("食べ物を食べます")

    def bark(self):
        print("ワンワン")


class Cat:
    def eat(self):
        print("食べ物を食べます")

    def meow(self):
        print("ニャー")

DogクラスとCatクラスの両方に、同じeat()メソッドが書かれています。

そこで、共通する処理をAnimalクラスにまとめます。

class Animal:
    def eat(self):
        print("食べ物を食べます")


class Dog(Animal):
    def bark(self):
        print("ワンワン")


class Cat(Animal):
    def meow(self):
        print("ニャー")

これにより、共通する処理を何度も書かなくて済みます。

dog = Dog()
cat = Cat()

dog.eat()
dog.bark()

cat.eat()
cat.meow()

実行結果は次のとおりです。

食べ物を食べます
ワンワン
食べ物を食べます
ニャー

super()を使って親クラスの処理を呼び出す

子クラスから親クラスのメソッドを呼び出す場合は、super()を使用します。

特によく使われるのが、親クラスの__init__()を呼び出す場面です。

class Animal:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        print(f"名前は{self.name}です")

このクラスを継承して、犬を表すDogクラスを作成します。

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def show_breed(self):
        print(f"犬種は{self.breed}です")

super().__init__(name)によって、親クラスの__init__()が呼び出されます。

dog = Dog("ポチ", "柴犬")

dog.introduce()
dog.show_breed()

実行結果は次のとおりです。

名前はポチです
犬種は柴犬です

nameは親クラスで初期化し、breedは子クラスで初期化しています。

super().__init__(name)

この書き方は、クラスの継承を使うときによく登場します。

初心者の段階では、super()は「親クラスの処理を呼び出すためのもの」と覚えておけば十分です。


メソッドのオーバーライド

子クラスでは、親クラスに定義されているメソッドと同じ名前のメソッドを定義できます。

この仕組みをオーバーライドと呼びます。

class Animal:
    def speak(self):
        print("動物が鳴きます")


class Dog(Animal):
    def speak(self):
        print("ワンワン")


class Cat(Animal):
    def speak(self):
        print("ニャー")

DogクラスとCatクラスでは、親クラスのspeak()メソッドをそれぞれ上書きしています。

dog = Dog()
cat = Cat()

dog.speak()
cat.speak()

実行結果は次のとおりです。

ワンワン
ニャー

どちらも同じspeak()というメソッド名ですが、インスタンスの種類によって実行される処理が変わります。

親クラスのメソッドも実行したい場合は、super()を使用できます。

class Animal:
    def speak(self):
        print("動物が鳴きます")


class Dog(Animal):
    def speak(self):
        super().speak()
        print("ワンワン")
dog = Dog()
dog.speak()

実行結果は次のとおりです。

動物が鳴きます
ワンワン

カプセル化とは

カプセル化とは、クラスの内部にあるデータや処理をまとめ、外部からの操作を制限する考え方です。

例えば、銀行口座を表すクラスを作成してみましょう。

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

このクラスでは、インスタンスの外部から残高を自由に変更できます。

account = BankAccount(10000)

account.balance = -50000

print(account.balance)

実行結果は次のとおりです。

-50000

銀行口座の残高が突然マイナス5万円に書き換えられてしまいました。

このように、重要な値を外部から自由に変更できる設計には問題があります。

そこで、残高を直接変更するのではなく、入金や出金のためのメソッドを用意します。

class BankAccount:
    def __init__(self, balance):
        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
        else:
            print("出金できません")
account = BankAccount(10000)

account.deposit(5000)
account.withdraw(3000)

print(account.balance)

実行結果は次のとおりです。

12000

入金や出金の処理をメソッドにまとめることで、不正な値が設定されにくくなります。

これがカプセル化の基本的な考え方です。


アンダースコアから始まる変数

Pythonでは、変数名の先頭にアンダースコアを付けることがあります。

self._balance

先頭にアンダースコアが付いた変数は、「クラスの外部から直接操作しないでください」という意味を表します。

class BankAccount:
    def __init__(self, balance):
        self._balance = balance

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def show_balance(self):
        print(f"残高は{self._balance}円です")
account = BankAccount(10000)

account.deposit(5000)
account.show_balance()

実行結果は次のとおりです。

残高は15000円です

ただし、アンダースコアを付けても、外部からのアクセスが完全に禁止されるわけではありません。

print(account._balance)

このコードは実行できてしまいます。

Pythonの先頭アンダースコアは、アクセスを強制的に禁止する仕組みではなく、プログラマー同士の約束に近いものです。

「内部で使う変数なので、外部から直接変更しない」という意思を示すために使用します。


ダブルアンダースコアを使った変数

変数名の先頭にアンダースコアを2つ付ける方法もあります。

self.__balance

次の例を見てみましょう。

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance

    def show_balance(self):
        print(f"残高は{self.__balance}円です")
account = BankAccount(10000)
account.show_balance()

実行結果は次のとおりです。

残高は10000円です

外部から__balanceにアクセスしてみます。

print(account.__balance)

この場合はエラーが発生します。

AttributeError: 'BankAccount' object has no attribute '__balance'

ただし、ダブルアンダースコアを付けた変数も、完全に外部から見えなくなるわけではありません。

Pythonでは、名前が内部的に変換されます。これを名前マングリングと呼びます。

初心者の段階では、詳しい仕組みまで覚える必要はありません。

次のように整理しておきましょう。

_balance
外部から直接使わないことを示す慣習

__balance
外部から簡単にアクセスされにくくする仕組み

一般的なPythonプログラムでは、まず先頭にアンダースコアを1つ付ける方法がよく使用されます。


propertyを使った値の管理

Pythonでは、propertyを使って、変数の取得や変更にルールを設定できます。

class Product:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

@propertyを付けると、メソッドを変数のように使用できます。

product = Product(1000)

print(product.price)

実行結果は次のとおりです。

1000

値を変更する処理も定義できます。

class Product:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value >= 0:
            self._price = value
        else:
            print("価格には0以上の値を指定してください")
product = Product(1000)

product.price = 1500
print(product.price)

product.price = -500
print(product.price)

実行結果は次のとおりです。

1500
価格には0以上の値を指定してください
1500

マイナスの値を設定しようとしても、価格は変更されません。

propertyは便利な機能ですが、初心者の段階では「値の取得や変更にルールを設定できる」と理解しておけば十分です。


ポリモーフィズムとは

ポリモーフィズムとは、同じ操作でも、対象となるオブジェクトによって異なる処理を実行できる仕組みです。

日本語では、多態性多相性と呼ばれることもあります。

例えば、犬と猫のクラスを作成します。

class Dog:
    def speak(self):
        print("ワンワン")


class Cat:
    def speak(self):
        print("ニャー")

どちらのクラスにも、同じ名前のspeak()メソッドがあります。

dog = Dog()
cat = Cat()

dog.speak()
cat.speak()

実行結果は次のとおりです。

ワンワン
ニャー

さらに、次のようにリストへまとめることもできます。

animals = [
    Dog(),
    Cat()
]

for animal in animals:
    animal.speak()

実行結果は次のとおりです。

ワンワン
ニャー

繰り返し処理の中では、animalが犬なのか猫なのかを細かく判定していません。

どちらもspeak()メソッドを持っているため、同じ書き方で呼び出せます。

これがポリモーフィズムの基本的な考え方です。


Pythonでは継承していなくても使える

Pythonでは、必ずしも同じ親クラスを継承している必要はありません。

同じ名前のメソッドを持っていれば、同じように扱えることがあります。

class Dog:
    def speak(self):
        print("ワンワン")


class Robot:
    def speak(self):
        print("こんにちは。私はロボットです")
objects = [
    Dog(),
    Robot()
]

for obj in objects:
    obj.speak()

実行結果は次のとおりです。

ワンワン
こんにちは。私はロボットです

DogRobotは、まったく異なるクラスです。

しかし、どちらもspeak()メソッドを持っているため、同じ処理で呼び出せます。

Pythonでは、このような考え方をダックタイピングと呼ぶことがあります。

「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルとして扱える」という考え方が名前の由来です。

初心者の段階では、「同じメソッドを持っていれば同じように扱える」と理解しておきましょう。


継承とポリモーフィズムを組み合わせる

継承とポリモーフィズムを組み合わせた例を見てみましょう。

まず、親クラスとしてEmployeeクラスを作成します。

class Employee:
    def work(self):
        print("仕事をします")

続いて、Employeeクラスを継承した子クラスを作成します。

class Programmer(Employee):
    def work(self):
        print("プログラムを作成します")


class Designer(Employee):
    def work(self):
        print("デザインを作成します")


class Sales(Employee):
    def work(self):
        print("お客様に商品を提案します")

それぞれの子クラスで、work()メソッドをオーバーライドしています。

employees = [
    Programmer(),
    Designer(),
    Sales()
]

for employee in employees:
    employee.work()

実行結果は次のとおりです。

プログラムを作成します
デザインを作成します
お客様に商品を提案します

すべてEmployeeクラスを継承していますが、実際に行う仕事は異なります。

呼び出す側は、すべて同じように次のコードを実行するだけです。

employee.work()

それぞれのインスタンスに応じて、適切な処理が実行されます。

これが、継承とポリモーフィズムを組み合わせた代表的な例です。


オブジェクト指向を使うメリット

オブジェクト指向を使うと、プログラムを整理しやすくなります。

主なメリットを確認しておきましょう。

同じ処理を再利用できる

継承を使うと、親クラスに定義した共通処理を子クラスで再利用できます。

class Animal:
    def eat(self):
        print("食べ物を食べます")

犬や猫ごとに同じeat()メソッドを書く必要がありません。


変更する場所を減らせる

共通する処理を親クラスにまとめておけば、修正する場所を少なくできます。

例えば、eat()メソッドの表示内容を変更するとします。

class Animal:
    def eat(self):
        print("おいしそうに食べています")

親クラスを修正するだけで、親クラスの処理を利用している子クラスにも変更が反映されます。


データを安全に管理しやすい

カプセル化を意識すると、重要な値を外部から直接変更されにくくできます。

class BankAccount:
    def __init__(self, balance):
        self._balance = balance

    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount

値を変更するためのルールをクラス内にまとめられます。


同じ方法で複数のオブジェクトを扱える

ポリモーフィズムを利用すると、異なる種類のインスタンスを同じコードで扱えます。

for employee in employees:
    employee.work()

インスタンスの種類ごとに大量のif文を書く必要がなくなる場合があります。


オブジェクト指向を使うときの注意点

オブジェクト指向は便利ですが、すべての処理をクラスにする必要はありません。

簡単な処理であれば、関数だけで十分な場合もあります。

def add(a, b):
    return a + b

このような簡単な計算を、無理にクラスとして作成すると、かえってコードが複雑になることがあります。

また、継承を何段階も重ねると、処理がどこに定義されているのか分かりにくくなります。

Animal
  ↓
Mammal
  ↓
Pet
  ↓
Dog
  ↓
ShibaDog

継承は便利ですが、使いすぎるとクラス同士の関係が複雑になります。

初心者のうちは、次のような場面で継承を検討するとよいでしょう。

  • 複数のクラスに明確な共通処理がある
  • 親クラスと子クラスに自然な関係がある
  • 子クラスが親クラスの一種として扱える

例えば、「犬は動物の一種」という関係は自然です。

一方、「車はエンジンの一種」ではありません。

この場合は、車がエンジンを持つという関係なので、継承ではなく別の設計方法が適しています。

最初から複雑な設計を目指さず、必要になったときに少しずつクラスを整理していきましょう。


練習問題

ここまでの内容を使って、簡単なプログラムを作成してみましょう。

問題1

次の条件を満たすプログラムを作成してください。

  1. Vehicleクラスを作成する
  2. move()メソッドを定義する
  3. CarクラスとBicycleクラスでVehicleクラスを継承する
  4. それぞれのクラスでmove()メソッドをオーバーライドする

実行結果は、次のようになるようにします。

車が道路を走ります
自転車をこいで進みます

解答例

class Vehicle:
    def move(self):
        print("乗り物が移動します")


class Car(Vehicle):
    def move(self):
        print("車が道路を走ります")


class Bicycle(Vehicle):
    def move(self):
        print("自転車をこいで進みます")
vehicles = [
    Car(),
    Bicycle()
]

for vehicle in vehicles:
    vehicle.move()

実行結果は次のとおりです。

車が道路を走ります
自転車をこいで進みます

問題2

商品を管理するProductクラスを作成してください。

次の条件を満たすようにします。

  1. 商品名と価格を受け取る
  2. 価格は_priceに保存する
  3. show_info()メソッドで商品名と価格を表示する
  4. set_price()メソッドで価格を変更する
  5. マイナスの価格は設定できないようにする

解答例

class Product:
    def __init__(self, name, price):
        self.name = name
        self._price = price

    def show_info(self):
        print(f"商品名:{self.name}")
        print(f"価格:{self._price}円")

    def set_price(self, price):
        if price >= 0:
            self._price = price
        else:
            print("価格には0以上の値を指定してください")
product = Product("キーボード", 5000)

product.show_info()

product.set_price(6000)
product.show_info()

product.set_price(-1000)

実行結果は次のとおりです。

商品名:キーボード
価格:5000円
商品名:キーボード
価格:6000円
価格には0以上の値を指定してください

まとめ

今回は、オブジェクト指向の代表的な考え方について学習しました。

継承

親クラスの機能を引き継いで、新しい子クラスを作る仕組みです。

class Dog(Animal):
    pass

共通する処理を親クラスにまとめることで、コードを再利用できます。

カプセル化

クラスの内部にデータや処理をまとめ、外部からの操作を制限する考え方です。

self._balance

Pythonでは、先頭のアンダースコアを使って、外部から直接操作しない変数であることを示します。

ポリモーフィズム

同じメソッドを呼び出しても、インスタンスの種類によって異なる処理を実行できる仕組みです。

for animal in animals:
    animal.speak()

オブジェクト指向には難しい言葉が多く登場しますが、最初からすべてを理解する必要はありません。

まずは、次の3点を覚えておきましょう。

継承
親クラスの機能を子クラスで再利用する

カプセル化
データをクラスの中で安全に管理する

ポリモーフィズム
同じ操作で異なるオブジェクトを扱う

実際にクラスを作りながら、少しずつ理解を深めていくことが大切です。

今回の内容まで理解できれば、Pythonにおけるクラスとオブジェクト指向の基本は十分に身についています。

コメント

タイトルとURLをコピーしました