상속(Inheritance)

2025. 3. 26. 04:04Programming Languages/Python

상속(Inheritance)

1. 상속이란?

상속은 한 클래스가 다른 클래스의 속성과 메서드를 물려받는 것을 말합니다. 이를 통해 코드 재사용성을 높이고 계층적인 관계를 구현할 수 있습니다.

2. 상속의 이론적 개념

2.1 상속의 핵심 개념

  • IS-A 관계: 상속은 "IS-A" 관계를 표현합니다. 예를 들어 "고양이는 동물이다(Cat IS-A Animal)". 이 관계는 자식 클래스가 부모 클래스의 특수한 형태임을 의미합니다.
  • 코드 재사용성: 상속의 주요 이점 중 하나는 기존 코드를 재사용하여 개발 시간을 단축하고 코드 중복을 줄이는 것입니다.
  • 확장성: 기존 클래스의 기능을 변경하지 않고 새로운 기능을 추가할 수 있습니다.

2.2 상속의 종류

  • 단일 상속(Single Inheritance): 한 클래스가 하나의 부모 클래스만 상속받는 형태입니다.
  • 다중 상속(Multiple Inheritance): 한 클래스가 여러 부모 클래스를 상속받는 형태로, 파이썬에서 지원합니다.
  • 다단계 상속(Multilevel Inheritance): A -> B -> C와 같이 상속이 연속적으로 이루어지는 형태입니다.

2.3 메서드 해결 순서(MRO, Method Resolution Order)

  • 파이썬에서 다중 상속 시 메서드 호출 순서를 결정하는 알고리즘입니다.
  • C3 선형화 알고리즘을 사용하여 메서드 호출 순서를 결정합니다.
  • ClassName.__mro__ 또는 ClassName.mro()로 확인할 수 있습니다.
# MRO 예제
class A:
    def method(self):
        return "A의 메서드"

class B(A):
    def method(self):
        return "B의 메서드"

class C(A):
    def method(self):
        return "C의 메서드"

class D(B, C):
    pass

print(D.mro())  # 메서드 해결 순서 출력
# 출력: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

d = D()
print(d.method())  # 출력: B의 메서드 (B가 MRO에서 C보다 앞에 있기 때문)

2.4 상속과 객체지향 설계 원칙

  • 리스코프 치환 원칙(Liskov Substitution Principle): 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대체하여 사용할 수 있어야 합니다.
  • 개방-폐쇄 원칙(Open-Closed Principle): 클래스는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다. 상속은 이 원칙을 구현하는 방법 중 하나입니다.

2.5 상속의 장단점

  • 장점:
    • 코드 재사용성 향상
    • 계층적 관계 표현 가능
    • 확장성 제공
  • 단점:
    • 과도한 상속은 코드를 복잡하게 만들 수 있음
    • 부모 클래스 변경 시 자식 클래스에 영향을 줄 수 있음 (결합도 증가)
    • 다중 상속 시 다이아몬드 문제 발생 가능

3. 상속 구현하기

# 부모 클래스 (기본 클래스)
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "동물이 소리를 냅니다."
    
# 자식 클래스 (파생 클래스)
class Cat(Animal):  # Animal 클래스를 상속
    def speak(self):  # 메서드 오버라이딩
        return f"{self.name}가 야옹하고 울어요."

# 또 다른 자식 클래스
class Dog(Animal):  # Animal 클래스를 상속
    def speak(self):  # 메서드 오버라이딩
        return f"{self.name}가 멍멍하고 짖어요."

# 인스턴스 생성 및 메서드 호출
animal = Animal("동물")
cat = Cat("나비")
dog = Dog("멍멍이")

print(animal.speak())  # 출력: 동물이 소리를 냅니다.
print(cat.speak())     # 출력: 나비가 야옹하고 울어요.
print(dog.speak())     # 출력: 멍멍이가 멍멍하고 짖어요.

4. super() 함수

super() 함수는 부모 클래스의 메서드를 호출할 때 사용합니다. 특히 자식 클래스에서 부모 클래스의 메서드를 확장할 때 유용합니다.

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "동물이 소리를 냅니다."

class Dog(Animal):
    def __init__(self, name, breed):
        # 부모 클래스의 __init__ 메서드 호출
        super().__init__(name)
        self.breed = breed
    
    def speak(self):
        # 부모 클래스의 speak 메서드를 호출하고 추가 기능 구현
        parent_message = super().speak()
        return f"{parent_message} 그리고 {self.name}는 {self.breed} 종이며 멍멍하고 짖습니다."

# 인스턴스 생성
my_dog = Dog("멍멍이", "진돗개")
print(my_dog.speak())
# 출력: 동물이 소리를 냅니다. 그리고 멍멍이는 진돗개 종이며 멍멍하고 짖습니다.

5. 다중 상속

파이썬은 여러 클래스로부터 상속받는 다중 상속을 지원합니다.

class Flyable:
    def fly(self):
        return "날 수 있습니다!"

class Swimmable:
    def swim(self):
        return "수영할 수 있습니다!"

class Duck(Animal, Flyable, Swimmable):
    def speak(self):
        return f"{self.name}가 꽥꽥 소리를 냅니다."

# 인스턴스 생성
duck = Duck("Donald")
print(duck.speak())  # 출력: Donald가 꽥꽥 소리를 냅니다.
print(duck.fly())    # 출력: 날 수 있습니다!
print(duck.swim())   # 출력: 수영할 수 있습니다!

6. 상속 관련 유용한 함수와 속성

6.1 isinstance() 함수

객체가 특정 클래스의 인스턴스인지 또는 해당 클래스의 하위 클래스의 인스턴스인지 확인합니다.

cat = Cat("나비")
print(isinstance(cat, Cat))     # 출력: True
print(isinstance(cat, Animal))  # 출력: True (상속 관계이므로)
print(isinstance(cat, Dog))     # 출력: False

6.2 issubclass() 함수

어떤 클래스가 다른 클래스의 하위 클래스인지 확인합니다.

print(issubclass(Cat, Animal))  # 출력: True
print(issubclass(Dog, Animal))  # 출력: True
print(issubclass(Cat, Dog))     # 출력: False

6.3 bases 속성

클래스의 직계 상위 클래스를 튜플로 반환합니다.

print(Cat.__bases__)  # 출력: (<class '__main__.Animal'>,)
print(Duck.__bases__)  # 출력: (<class '__main__.Animal'>, <class '__main__.Flyable'>, <class '__main__.Swimmable'>)

7. 실전 활용 예제

7.1 도형 클래스 계층 구조

import math

# 기본 도형 클래스
class Shape:
    def area(self):
        pass
    
    def perimeter(self):
        pass
    
    def describe(self):
        return f"이 도형의 면적은 {self.area():.2f}, 둘레는 {self.perimeter():.2f}입니다."

# 원 클래스
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return math.pi * self.radius ** 2
    
    def perimeter(self):
        return 2 * math.pi * self.radius

# 사각형 클래스
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

# 정사각형 클래스 (사각형의 특수한 형태)
class Square(Rectangle):
    def __init__(self, side):
        # Rectangle의 __init__을 호출
        super().__init__(side, side)

# 도형 인스턴스 생성 및 사용
circle = Circle(5)
rectangle = Rectangle(4, 6)
square = Square(4)

print(circle.describe())    # 출력: 이 도형의 면적은 78.54, 둘레는 31.42입니다.
print(rectangle.describe()) # 출력: 이 도형의 면적은 24.00, 둘레는 20.00입니다.
print(square.describe())    # 출력: 이 도형의 면적은 16.00, 둘레는 16.00입니다.

7.2 은행 계좌 시스템

# 기본 계좌 클래스
class Account:
    def __init__(self, account_number, owner, balance=0):
        self.account_number = account_number
        self.owner = owner
        self.balance = balance
    
    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            return f"{amount}원이 입금되었습니다. 현재 잔액: {self.balance}원"
        return "유효하지 않은 금액입니다."
    
    def withdraw(self, amount):
        if 0 < amount <= self.balance:
            self.balance -= amount
            return f"{amount}원이 출금되었습니다. 현재 잔액: {self.balance}원"
        return "잔액이 부족하거나 유효하지 않은 금액입니다."
    
    def get_balance(self):
        return f"계좌 잔액: {self.balance}원"

# 저축 계좌 (이자 기능 추가)
class SavingsAccount(Account):
    def __init__(self, account_number, owner, balance=0, interest_rate=0.01):
        super().__init__(account_number, owner, balance)
        self.interest_rate = interest_rate
    
    def add_interest(self):
        interest = self.balance * self.interest_rate
        self.balance += interest
        return f"이자 {interest}원이 추가되었습니다. 현재 잔액: {self.balance}원"

# 체크카드 계좌 (카드 결제 기능 추가)
class CheckingAccount(Account):
    def __init__(self, account_number, owner, balance=0, overdraft_limit=0):
        super().__init__(account_number, owner, balance)
        self.overdraft_limit = overdraft_limit
    
    def withdraw(self, amount):
        # 당좌대출 한도까지 출금 가능하도록 오버라이딩
        if 0 < amount <= (self.balance + self.overdraft_limit):
            self.balance -= amount
            return f"{amount}원이 출금되었습니다. 현재 잔액: {self.balance}원"
        return "출금 한도를 초과했거나 유효하지 않은 금액입니다."
    
    def card_payment(self, amount):
        return self.withdraw(amount)

# 계좌 사용 예제
savings = SavingsAccount("1001", "홍길동", 10000, 0.05)
checking = CheckingAccount("1002", "김철수", 5000, 10000)

print(savings.deposit(5000))       # 출력: 5000원이 입금되었습니다. 현재 잔액: 15000원
print(savings.add_interest())      # 출력: 이자 750.0원이 추가되었습니다. 현재 잔액: 15750.0원

print(checking.deposit(1000))      # 출력: 1000원이 입금되었습니다. 현재 잔액: 6000원
print(checking.card_payment(8000)) # 출력: 8000원이 출금되었습니다. 현재 잔액: -2000원

8. 상속 사용 시 주의점

  1. 깊은 상속 계층 피하기: 너무 깊은 상속 계층은 코드 이해와 유지보수를 어렵게 만듭니다.
  2. 상속보다 구성 고려: 때로는 상속보다 구성(Composition)이 더 적합할 수 있습니다.
  3. 인터페이스 상속 vs 구현 상속: 인터페이스를 상속받아 구현하는 방식과 기능 구현까지 상속받는 방식의 차이를 이해해야 합니다.
  4. 메서드 오버라이딩 시 주의: 부모 클래스의 메서드 동작을 완전히 변경하는 오버라이딩은 리스코프 치환 원칙을 위반할 수 있습니다.
  5. 다중 상속 사용 시 주의: 다중 상속은 복잡성을 증가시킬 수 있으므로 신중하게 사용해야 합니다.

'Programming Languages > Python' 카테고리의 다른 글

다형성 (Polymorphism)  (0) 2025.03.26
캡슐화(Encapsulation)  (0) 2025.03.26
클래스와 객체지향 프로그래밍  (0) 2025.03.26
함수 디버깅과 테스트  (0) 2025.03.26
함수의 효율성과 성능 최적화  (0) 2025.03.26