32. 상속과 다형성으로 클래스 확장하기
30장에서는 현실 세계의 개념을 모델링하기 위해 우리만의 클래스를 만드는 방법을 배웠습니다. 데이터와 동작을 함께 묶는 BankAccount, Student 같은 클래스를 만들었습니다. 그런데 기존 클래스와 비슷하지만, 몇 가지 차이점이나 추가 기능이 있는 새 클래스를 만들어야 한다면 어떻게 해야 할까요?
상속(inheritance)은 기존 클래스를 바탕으로 새로운 클래스를 만드는 Python의 메커니즘입니다. 코드를 복사해서 붙여넣는 대신, 부모 클래스(parent class)(또는 기본 클래스(base class), 슈퍼클래스(superclass)라고도 함)의 모든 속성과 메서드를 자동으로 물려받는 서브클래스(subclass)를 만든 뒤, 필요한 것을 추가하거나 수정할 수 있습니다.
이 장에서는 상속이 관련된 클래스들의 계층 구조를 만드는 데 어떻게 도움이 되는지, 상속받은 동작을 어떻게 커스터마이즈하는지, 그리고 다형성(polymorphism)이 공통 인터페이스를 공유하는 서로 다른 클래스를 어떻게 서로 바꿔서 사용할 수 있게 해주는지를 살펴봅니다.
32.1) 기존 클래스에서 서브클래스 만들기
32.1.1) 상속의 기본 문법
상속을 사용하면 기존 클래스(부모 클래스 또는 베이스 클래스)를 기반으로 새로운 클래스(자식 클래스 또는 서브클래스)를 만들 수 있습니다. 서브클래스는 부모 클래스의 모든 메서드와 속성을 자동으로 상속받습니다.
서브클래스를 만들 때는 클래스 이름 뒤 괄호 안에 부모 클래스를 지정합니다.
# 부모 클래스
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__ 호출
# 참고: 32.3절에서 super()를 사용하는 더 나은 방법을 배웁니다
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")
# 세 단계의 메서드가 모두 동작함
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)
# 세 단계의 속성이 모두 존재함
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()를 호출하면, Python은 buddy가 Dog의 인스턴스이므로 먼저 Dog 클래스에서 speak 메서드를 찾습니다. Dog가 자체적으로 speak 메서드를 정의하고 있으므로, Python은 그 버전을 사용합니다. 만약 Dog에 speak 메서드가 없다면, Python은 부모 클래스인 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
# 각 도형은 area를 다르게 계산함
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):
# name과 age를 설정하기 위해 부모의 __init__ 호출
Person.__init__(self, name, age)
# 그 다음 학생 전용 속성 설정
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()를 쓸 때 vs 부모를 직접 호출할 때
대부분의 경우 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) 다형성이란 무엇인가?
다형성(Polymorphism)(그리스어: "many forms")은 서로 다른 클래스의 객체들이 같은 메서드를 제공할 때, 이들을 동일한 방식으로 다룰 수 있는 능력을 말합니다.
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) 덕 타이핑: “오리처럼 걷고…”
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)
# 서로 관련이 없어도 세 클래스 모두 동작함
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()를 사용하세요 - 가능하면 타입 체크보다 다형성(공통 메서드)을 선호하세요
더 큰 프로그램을 만들수록, 이러한 객체 지향 기법은 더 이해하기 쉽고 유지보수하기 쉬우며 확장하기 쉬운 코드를 만드는 데 도움이 될 것입니다.