32. 継承とポリモーフィズムでクラスを拡張する
第30章では、現実世界の概念をモデル化するために自分でクラスを作る方法を学びました。BankAccount や Student のようなクラスを作り、データと振る舞いをひとまとめにしました。しかし、既存のクラスに似ているものの、いくつかの違いや追加がある新しいクラスを作る必要が出てきたらどうでしょうか?
継承(inheritance) は、既存のクラスを基に新しいクラスを作るための Python の仕組みです。コードをコピー&ペーストする代わりに、サブクラス(subclass) を作れば、親クラス(parent class)(ベースクラス(base class) や スーパークラス(superclass) とも呼ばれます)からすべての属性とメソッドを自動的に受け継ぎ、必要なものを追加したり修正したりできます。
この章では、継承によって関連するクラスの階層を構築できること、継承した振る舞いをどのようにカスタマイズするか、そして共通のインターフェースを共有しているときに ポリモーフィズム(polymorphism) によって異なるクラスを入れ替え可能に扱えることを見ていきます。
32.1) 既存クラスからサブクラスを作成する
32.1.1) 継承の基本構文
継承により、既存のクラス(親クラス(parent class) または ベースクラス(base class))を基に、新しいクラス(サブクラス(subclass) または 子クラス(child class))を作れます。サブクラスは親クラスからすべてのメソッドと属性を自動的に継承します。
サブクラスを作るときは、クラス名の後ろの丸括弧に親クラスを指定します。
# 親クラス
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
# Animal を継承するサブクラス
class Dog(Animal):
pass # まだ追加コードは不要
# Dog のインスタンスを作成
buddy = Dog("Buddy")
print(buddy.speak()) # Output: Buddy makes a sound
print(buddy.name) # Output: BuddyDog には独自のコードがなく(pass だけでも)、Animal からすべてを継承します。Dog クラスには、親から __init__ メソッドと speak メソッドが自動的に備わります。
32.1.2) なぜ継承が重要なのか
継承は、よくあるプログラミングの問題であるコードの重複を解決します。たとえば、さまざまな種類の従業員を管理するシステムを作っているとしましょう。
# 継承なし - 重複がたくさん
class FullTimeEmployee:
def __init__(self, name, employee_id, salary):
self.name = name
self.employee_id = employee_id
self.salary = salary
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"
class PartTimeEmployee:
def __init__(self, name, employee_id, hourly_rate):
self.name = name
self.employee_id = employee_id
self.hourly_rate = hourly_rate
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"name、employee_id、get_info() が重複しているのが分かります。継承を使えば、この重複をなくせます。
# 継承あり - 共有コードを親クラスにまとめる
class Employee:
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"
class FullTimeEmployee(Employee):
def __init__(self, name, employee_id, salary):
Employee.__init__(self, name, employee_id) # 親の __init__ を呼び出す
# Note: We'll learn a better way to do this with super() in Section 32.3
self.salary = salary
class PartTimeEmployee(Employee):
def __init__(self, name, employee_id, hourly_rate):
Employee.__init__(self, name, employee_id) # 親の __init__ を呼び出す
self.hourly_rate = hourly_rate
# 両方のサブクラスが get_info() を継承する
alice = FullTimeEmployee("Alice", "E001", 75000)
bob = PartTimeEmployee("Bob", "E002", 25)
print(alice.get_info()) # Output: Alice (ID: E001)
print(bob.get_info()) # Output: Bob (ID: E002)これで、共通の属性とメソッドは Employee に置かれ、各サブクラスは固有の部分だけを定義すればよくなります。
32.1.3) サブクラスに新しいメソッドを追加する
サブクラスは、親にない独自のメソッドを追加できます。
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def get_description(self):
return f"{self.brand} {self.model}"
class Car(Vehicle):
def __init__(self, brand, model, num_doors):
Vehicle.__init__(self, brand, model)
self.num_doors = num_doors
def honk(self): # Car 固有の新しいメソッド
return "Beep beep!"
class Motorcycle(Vehicle):
def __init__(self, brand, model, has_sidecar):
Vehicle.__init__(self, brand, model)
self.has_sidecar = has_sidecar
def rev_engine(self): # Motorcycle 固有の新しいメソッド
return "Vroom vroom!"
# 各サブクラスは親のメソッドに加え、自分のメソッドも持つ
my_car = Car("Toyota", "Camry", 4)
print(my_car.get_description()) # Output: Toyota Camry
print(my_car.honk()) # Output: Beep beep!
my_bike = Motorcycle("Harley", "Sportster", False)
print(my_bike.get_description()) # Output: Harley Sportster
print(my_bike.rev_engine()) # Output: Vroom vroom!Car クラスには get_description()(継承)と honk()(独自)が両方あります。Motorcycle クラスには get_description()(継承)と rev_engine()(独自)があります。
32.1.4) サブクラスに新しい属性を追加する
サブクラスは独自のインスタンス属性も追加できます。通常はサブクラスの __init__ メソッドで行います。
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
class SavingsAccount(BankAccount):
def __init__(self, account_number, balance, interest_rate):
BankAccount.__init__(self, account_number, balance)
self.interest_rate = interest_rate # 新しい属性
def apply_interest(self): # 新しい属性を使う新しいメソッド
interest = self.balance * self.interest_rate
self.balance += interest
return interest
# SavingsAccount は BankAccount の全属性に加えて interest_rate も持つ
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500) # 継承したメソッド
print(f"Balance: ${savings.balance}") # Output: Balance: $1500
interest_earned = savings.apply_interest()
print(f"Interest earned: ${interest_earned:.2f}") # Output: Interest earned: $45.00
print(f"New balance: ${savings.balance:.2f}") # Output: New balance: $1545.00SavingsAccount には BankAccount からの account_number と balance に加えて、独自の interest_rate 属性があります。
32.1.5) 複数レベルの継承
クラスは、他のクラスを継承しているクラスから継承することもでき、継承階層を作れます。
class LivingThing:
def __init__(self, name):
self.name = name
def is_alive(self):
return True
class Animal(LivingThing):
def __init__(self, name, species):
LivingThing.__init__(self, name)
self.species = species
def move(self):
return f"{self.name} is moving"
class Dog(Animal):
def __init__(self, name, breed):
Animal.__init__(self, name, "Dog")
self.breed = breed
def bark(self):
return f"{self.name} says: Woof!"
# Dog は Animal を継承し、Animal は LivingThing を継承する
max_dog = Dog("Max", "Golden Retriever")
# 3階層すべてのメソッドが動く
print(max_dog.is_alive()) # Output: True (from LivingThing)
print(max_dog.move()) # Output: Max is moving (from Animal)
print(max_dog.bark()) # Output: Max says: Woof! (from Dog)
# 3階層すべての属性が存在する
print(max_dog.name) # Output: Max (from LivingThing)
print(max_dog.species) # Output: Dog (from Animal)
print(max_dog.breed) # Output: Golden Retriever (from Dog)Dog は Animal を継承し、Animal は LivingThing を継承しています。つまり Dog は、両方の親クラスからのメソッドと属性にアクセスできます。
32.2) サブクラスでメソッドをオーバーライドする
32.2.1) メソッドのオーバーライドとは
サブクラスが、継承したメソッドの動きを変更する必要がある場合があります。メソッドのオーバーライド(method overriding) とは、親クラスにあるメソッドと同じ名前のメソッドをサブクラスで定義することです。サブクラスのインスタンスでそのメソッドを呼ぶと、Python は親のものではなくサブクラス版を使います。
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self): # 親の speak メソッドをオーバーライドする
return f"{self.name} says: Woof!"
class Cat(Animal):
def speak(self): # 別の振る舞いでオーバーライドする
return f"{self.name} says: Meow!"
# 各クラスがそれぞれの speak() を持つ
generic_animal = Animal("Generic")
print(generic_animal.speak()) # Output: Generic makes a sound
buddy = Dog("Buddy")
print(buddy.speak()) # Output: Buddy says: Woof!
whiskers = Cat("Whiskers")
print(whiskers.speak()) # Output: Whiskers says: Meow!buddy.speak() を呼ぶと、buddy は Dog のインスタンスなので、Python はまず Dog クラスの中に speak メソッドがあるか探します。Dog は独自の speak メソッドを定義しているため、Python はそのバージョンを使います。もし Dog に speak がなければ、その次に親クラス Animal を探し、代わりにそちらのバージョンを使います。
この探索順序(インスタンスのクラスから始めて、次に親クラスへ進む)が、メソッドのオーバーライドが働く仕組みであり、サブクラスが継承した振る舞いをカスタマイズする方法です。
32.2.2) なぜメソッドをオーバーライドするのか
メソッドのオーバーライドにより、一般的な振る舞いを特化させたバージョンを作れます。図形の階層を考えてみましょう。
class Shape:
def __init__(self, name):
self.name = name
def area(self):
return 0 # デフォルト実装
def describe(self):
return f"{self.name} with area {self.area()}"
class Rectangle(Shape):
def __init__(self, width, height):
Shape.__init__(self, "Rectangle")
self.width = width
self.height = height
def area(self): # 長方形向けの計算でオーバーライドする
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
Shape.__init__(self, "Circle")
self.radius = radius
def area(self): # 円向けの計算でオーバーライドする
return 3.14159 * self.radius ** 2
# 図形ごとに面積の計算が異なる
rect = Rectangle(5, 3)
print(rect.describe()) # Output: Rectangle with area 15
circle = Circle(4)
print(circle.describe()) # Output: Circle with area 50.26544describe() メソッドは両方のサブクラスに継承されますが、各サブクラスがそれぞれ独自の area() 実装を提供するため、正しく動作します。
rect.describe() を呼ぶと、継承された describe() メソッドが実行されますが、self は Rectangle のインスタンスを参照します。そのため、describe() が self.area() を呼ぶと、Python はまず Rectangle クラスの area() を探し、オーバーライドされたバージョンを見つけます。
32.2.3) __init__ のオーバーライドと親の初期化呼び出し
__init__ をオーバーライドする場合、通常は親の __init__ を呼んで、親側の初期化が確実に行われるようにする必要があります。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f"I'm {self.name}, {self.age} years old"
class Student(Person):
def __init__(self, name, age, student_id, major):
# 親の __init__ を呼び出して name と age を設定する
Person.__init__(self, name, age)
# 次に student 固有の属性を設定する
self.student_id = student_id
self.major = major
alice = Student("Alice", 20, "S12345", "Computer Science")
print(alice.name) # Output: Alice
print(alice.student_id) # Output: S12345Student.__init__ はまず Person.__init__(self, name, age) を呼んで親クラスの属性を初期化し、その後に独自の属性を追加している点に注目してください。
32.3) super() を使って親の振る舞いにアクセスする
32.3.1) super() の役割
前のセクションでは、親メソッドを ParentClass.method(self, ...) のように明示的に呼び出しました。Python には、よりきれいな方法として super() 関数があります。super() は、一時的なオブジェクトを返し、親クラス名を明示せずに親クラスのメソッドを呼べるようにします。
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Animal.__init__(self, name) よりも簡潔
self.breed = breed
def speak(self):
parent_sound = super().speak() # 親の speak() を呼ぶ
return f"{parent_sound} - specifically, Woof!"
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!super() を使う利点はいくつかあります。
- 親クラス名を明示する必要がない
- 多重継承でも正しく動作する(後で扱います)
- 親クラスを変更しても保守しやすい
32.3.2) __init__ で super() を使う
super() の最も一般的な使い方は、親の __init__ を呼び出すことです。
class Employee:
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
self.is_active = True
def deactivate(self):
self.is_active = False
class Manager(Employee):
def __init__(self, name, employee_id, department):
super().__init__(name, employee_id) # 親の属性を初期化する
self.department = department
self.team = [] # Manager 固有の属性
def add_team_member(self, employee):
self.team.append(employee)
# Manager は Employee の全属性に加えて独自の属性も持つ
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name) # Output: Sarah
print(sarah.is_active) # Output: True
print(sarah.department) # Output: Engineeringsuper().__init__(name, employee_id) を呼び出すことで、Manager クラスは Employee の初期化ロジック(is_active を True に設定することも含む)がすべて実行されることを保証します。
32.3.3) super() で親メソッドを拡張する
super() を使えば、親のメソッドを完全に置き換えるのではなく、拡張できます。
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
self.transaction_count = 0
def deposit(self, amount):
self.balance += amount
self.transaction_count += 1
return self.balance
class CheckingAccount(BankAccount):
def __init__(self, account_number, balance, overdraft_limit):
super().__init__(account_number, balance)
self.overdraft_limit = overdraft_limit
def deposit(self, amount):
was_overdrawn = self.balance < 0
# 親の deposit を呼んで基本ロジックを処理する
new_balance = super().deposit(amount)
# 当座預金固有の振る舞いを追加する
if was_overdrawn and new_balance >= 0:
print("Account is no longer overdrawn")
return new_balance
checking = CheckingAccount("C001", -50, 100)
checking.deposit(75)
# Output: Account is no longer overdrawn
print(f"Balance: ${checking.balance}") # Output: Balance: $25
print(f"Transactions: {checking.transaction_count}") # Output: Transactions: 1CheckingAccount.deposit() は super().deposit(amount) を呼んで基本的な入金ロジック(残高と取引回数の更新)を処理し、その後に独自の当座貸越状況のチェックを追加しています。
32.3.4) super() と親の直接呼び出しはいつ使い分けるか
ほとんどの場合は super() を使います。
class Vehicle:
def __init__(self, brand):
self.brand = brand
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand) # 推奨
self.model = model多重継承の場面で特定の親を呼ぶ必要があるとき(後で扱います)、またはどの親を呼ぶかを明示したいときは、親を直接呼び出します。
class Car(Vehicle):
def __init__(self, brand, model):
Vehicle.__init__(self, brand) # 明示的だが柔軟性は低い
self.model = model単一継承(親が1つ)の場合、super() がほぼ常により良い選択です。
32.3.5) 他のメソッドでの super()
super() は __init__ だけでなく、どのメソッドにも使えます。
class TextProcessor:
def process(self, text):
# 基本処理: 前後の空白を取り除く
return text.strip()
class UppercaseProcessor(TextProcessor):
def process(self, text):
# まず親の処理を行う
processed = super().process(text)
# 次に大文字変換を追加する
return processed.upper()
class PrefixProcessor(UppercaseProcessor):
def __init__(self, prefix):
self.prefix = prefix
def process(self, text):
# まず親の処理を行う(親がさらに親も呼ぶ)
processed = super().process(text)
# 次にプレフィックスを追加する
return f"{self.prefix}: {processed}"
processor = PrefixProcessor("ALERT")
result = processor.process(" system error ")
print(result) # Output: ALERT: SYSTEM ERROR32.4) ポリモーフィズム: 互換性のあるクラスを扱う
32.4.1) ポリモーフィズムとは
ポリモーフィズム(ギリシャ語で「多くの形」)とは、同じメソッドを提供していれば、異なるクラスのオブジェクトを同じように扱える能力のことです。
Python では、複数のクラスが同じ名前のメソッドを持っていれば、オブジェクトの正確なクラスを知らなくてもそのメソッドを呼び出せます。
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Woof!"
class Cat:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Meow!"
class Bird:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Tweet!"
# speak() メソッドを持つ任意のオブジェクトで動作する関数
def make_animal_speak(animal):
print(animal.speak())
# 異なるクラスでも動く
buddy = Dog("Buddy")
whiskers = Cat("Whiskers")
tweety = Bird("Tweety")
make_animal_speak(buddy) # Output: Buddy says: Woof!
make_animal_speak(whiskers) # Output: Whiskers says: Meow!
make_animal_speak(tweety) # Output: Tweety says: Tweet!make_animal_speak() 関数は、animal 引数が何のクラスかを気にしません。必要なのは、そのオブジェクトが speak() メソッドを持つことだけです。これがポリモーフィズムです。
32.4.2) 継承と組み合わせたポリモーフィズム
ポリモーフィズムは、サブクラスが親のメソッドをオーバーライドする継承と組み合わせると特に強力です。
class PaymentMethod:
def process_payment(self, amount):
return f"Processing ${amount:.2f}"
class CreditCard(PaymentMethod):
def __init__(self, card_number):
self.card_number = card_number
def process_payment(self, amount):
return f"Charging ${amount:.2f} to credit card ending in {self.card_number[-4:]}"
class PayPal(PaymentMethod):
def __init__(self, email):
self.email = email
def process_payment(self, amount):
return f"Sending ${amount:.2f} via PayPal to {self.email}"
class BankTransfer(PaymentMethod):
def __init__(self, account_number):
self.account_number = account_number
def process_payment(self, amount):
return f"Transferring ${amount:.2f} to account {self.account_number}"
# 任意の PaymentMethod で動作する関数
def complete_purchase(payment_method, amount):
print(payment_method.process_payment(amount))
print("Purchase complete!")
# どの支払い方法でも同じ関数で動く
credit = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bank = BankTransfer("9876543210")
complete_purchase(credit, 99.99)
# Output: Charging $99.99 to credit card ending in 3456
# Output: Purchase complete!
complete_purchase(paypal, 49.50)
# Output: Sending $49.50 via PayPal to user@example.com
# Output: Purchase complete!
complete_purchase(bank, 199.00)
# Output: Transferring $199.00 to account 9876543210
# Output: Purchase complete!complete_purchase() 関数は、任意の PaymentMethod サブクラスで動作します。各サブクラスは process_payment() を独自に実装しますが、この関数はどの具体的なクラスを扱っているかを知る必要がありません。
32.4.3) ダックタイピング(duck typing):「アヒルのように歩けば...」
Python のポリモーフィズムは、クラス同士が継承で関係していることを必須としません。これを ダックタイピング(duck typing) と呼びます。「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」という考え方です。つまり Python が重視するのは、オブジェクトが「何であるか」(クラス)ではなく「何ができるか」(メソッド)です。必要なメソッドを持っていれば、そのクラス階層に関係なく利用できます。
class FileWriter:
def __init__(self, filename):
self.filename = filename
def write(self, data):
print(f"Writing to {self.filename}: {data}")
class DatabaseWriter:
def __init__(self, table_name):
self.table_name = table_name
def write(self, data):
print(f"Inserting into {self.table_name}: {data}")
class ConsoleWriter:
def write(self, data):
print(f"Console output: {data}")
# write() メソッドを持つ任意のオブジェクトで動作する関数
def save_data(writer, data):
writer.write(data)
# 3つのクラスは無関係でも、どれも動く
file_writer = FileWriter("data.txt")
db_writer = DatabaseWriter("users")
console_writer = ConsoleWriter()
save_data(file_writer, "User data")
# Output: Writing to data.txt: User data
save_data(db_writer, "User data")
# Output: Inserting into users: User data
save_data(console_writer, "User data")
# Output: Console output: User dataこれらのクラスはいずれも共通の親クラスを継承していませんが、すべて write() メソッドを持つため save_data() で扱えます。これがダックタイピングであり、関数が気にするのはクラスではなくインターフェース(利用できるメソッド)だけです。
32.4.4) 実用例: プラグインシステム
ポリモーフィズムにより、柔軟で拡張しやすいシステムを作れます。以下は、データ処理のためのシンプルなプラグインシステムです。
class DataProcessor:
def process(self, data):
return data # ベース実装は何もしない
class UppercaseProcessor(DataProcessor):
def process(self, data):
return data.upper()
class ReverseProcessor(DataProcessor):
def process(self, data):
return data[::-1]
class RemoveSpacesProcessor(DataProcessor):
def process(self, data):
return data.replace(" ", "")
class DataPipeline:
def __init__(self):
self.processors = []
def add_processor(self, processor):
self.processors.append(processor)
def run(self, data):
result = data
for processor in self.processors:
result = processor.process(result) # ポリモーフィックな呼び出し
return result
# 処理パイプラインを構築する
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
# パイプラインを通してデータを処理する
input_data = "Hello World"
output = pipeline.run(input_data)
print(f"Input: {input_data}") # Output: Input: Hello World
print(f"Output: {output}") # Output: Output: DLROWOLLEHDataPipeline は、どの具体的なプロセッサを含んでいるかを知る必要がなく、各要素の process() を呼ぶだけです。パイプラインのコードを変えずに、新しい種類のプロセッサを簡単に追加できます。
32.5) 型とクラス関係をチェックする (isinstance, issubclass)
32.5.1) isinstance() でインスタンスの型をチェックする
オブジェクトが特定のクラスのインスタンスかどうかを確認したい場合があります。isinstance() 関数はこれを行います。
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
buddy = Dog()
whiskers = Cat()
# オブジェクトがクラスのインスタンスかどうかをチェックする
print(isinstance(buddy, Dog)) # Output: True
print(isinstance(buddy, Animal)) # Output: True (Dog inherits from Animal)
print(isinstance(buddy, Cat)) # Output: False
print(isinstance(whiskers, Cat)) # Output: True
print(isinstance(whiskers, Animal)) # Output: True
print(isinstance(whiskers, Dog)) # Output: Falsebuddy は Dog のインスタンスなのに、isinstance(buddy, Animal) が True を返す点に注目してください。これは Dog が Animal を継承しているため、Dog のインスタンスは Animal のインスタンスとも見なされるからです。
32.5.2) なぜ isinstance() は継承を考慮するのか
isinstance() 関数は、継承チェーン全体をチェックします。
class Vehicle:
pass
class Car(Vehicle):
pass
class ElectricCar(Car):
pass
tesla = ElectricCar()
# 継承のすべての階層をチェックする
print(isinstance(tesla, ElectricCar)) # Output: True
print(isinstance(tesla, Car)) # Output: True
print(isinstance(tesla, Vehicle)) # Output: True
print(isinstance(tesla, str)) # Output: Falsetesla オブジェクトは ElectricCar のインスタンスですが、継承のため Car と Vehicle のインスタンスでもあります。
32.5.3) 複数の型をまとめてチェックする
タプルを渡すことで、オブジェクトが複数クラスのいずれかのインスタンスかどうかをチェックできます。
class Dog:
pass
class Cat:
pass
class Bird:
pass
def is_pet(animal):
return isinstance(animal, (Dog, Cat, Bird))
buddy = Dog()
whiskers = Cat()
tweety = Bird()
rock = "just a rock"
print(is_pet(buddy)) # Output: True
print(is_pet(whiskers)) # Output: True
print(is_pet(tweety)) # Output: True
print(is_pet(rock)) # Output: Falseこれは isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird) と書くよりも簡潔です。
32.5.4) issubclass() でクラス関係をチェックする
issubclass() 関数は、あるクラスが別のクラスのサブクラスかどうかをチェックします。
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
class Poodle(Dog):
pass
# クラス関係をチェックする
print(issubclass(Dog, Animal)) # Output: True
print(issubclass(Cat, Animal)) # Output: True
print(issubclass(Poodle, Dog)) # Output: True
print(issubclass(Poodle, Animal)) # Output: True (indirect inheritance)
print(issubclass(Dog, Cat)) # Output: False
# クラスは自分自身のサブクラスと見なされる
print(issubclass(Dog, Dog)) # Output: Trueissubclass() はインスタンスではなくクラスに対して動作する点に注意してください。インスタンスには isinstance()、クラスには issubclass() を使います。
32.5.5) 型チェックの実用的なユースケース
型チェックは、型によって異なる振る舞いが必要な場合に役立ちます。
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
class Contractor:
def __init__(self, name, hourly_rate, hours):
self.name = name
self.hourly_rate = hourly_rate
self.hours = hours
def calculate_payment(worker):
# 継承がある場合に isinstance() を使うときは、親クラスより先にサブクラスをチェックする
if isinstance(worker, Manager): # まず Manager をチェック
return worker.salary + worker.bonus
elif isinstance(worker, Employee): # 次に Employee(親クラス)をチェック
return worker.salary
elif isinstance(worker, Contractor):
return worker.hourly_rate * worker.hours
else:
return 0
alice = Employee("Alice", 50000)
bob = Manager("Bob", 70000, 10000)
charlie = Contractor("Charlie", 50, 160)
print(f"Alice's payment: ${calculate_payment(alice)}") # Output: Alice's payment: $50000
print(f"Bob's payment: ${calculate_payment(bob)}") # Output: Bob's payment: $80000
print(f"Charlie's payment: ${calculate_payment(charlie)}")# Output: Charlie's payment: $8000しかし、多くの場合、ポリモーフィズム(各クラスが共通のメソッドを実装すること)のほうが型チェックより良い選択です。
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def calculate_payment(self):
return self.salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def calculate_payment(self):
return self.salary + self.bonus
class Contractor:
def __init__(self, name, hourly_rate, hours):
self.name = name
self.hourly_rate = hourly_rate
self.hours = hours
def calculate_payment(self):
return self.hourly_rate * self.hours
# 型チェックは不要 - ポリモーフィズムが処理する
workers = [
Employee("Alice", 50000),
Manager("Bob", 70000, 10000),
Contractor("Charlie", 50, 160)
]
for worker in workers:
payment = worker.calculate_payment() # ポリモーフィックな呼び出し
print(f"{worker.name}'s payment: ${payment}")Output:
Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000このポリモーフィックなアプローチは、より柔軟で、新しい worker 型を拡張しやすいです。新しい worker クラスを追加しても呼び出し側のコードを修正する必要はなく、calculate_payment() メソッドを持つようにするだけで済みます。
継承とポリモーフィズムは、コードを整理し、柔軟で拡張可能なシステムを作るための強力な道具です。サブクラスを作れば、既存コードを再利用しつつ、振る舞いを追加・変更できます。メソッドをオーバーライドすれば、サブクラスの動きをカスタマイズできます。そしてポリモーフィズムを使えば、共通インターフェースを通じて多くの異なるクラスで動作するコードを書けます。
重要なのは、これらの機能をよく考えて使うことです。
- 「is-a」関係が本当にあるときに継承を使う(
DogはAnimalである) - クラスが行うことを完全に変えるのではなく、振る舞いを特化させるためにメソッドをオーバーライドする
- 親の振る舞いを完全に置き換えるのではなく、
super()で拡張する - 可能なら型チェックよりもポリモーフィズム(共通メソッド)を優先する
より大きなプログラムを作っていく中で、これらのオブジェクト指向の技法は、理解しやすく、保守しやすく、拡張しやすいコードを作る助けになります。