상속의 is-a 관계, MRO와 C3 선형화, 믹스인 패턴, 추상 클래스로 인터페이스를 강제하는 다형성 설계를 마스터한다.
🎯 이 강의에서 배우는 것
상속(inheritance)은 OOP의 강력한 코드 재사용 도구입니다. 하지만 잘못 쓰면 오히려 유지보수가 어려워집니다. 언제 상속을 쓰고 언제 컴포지션을 써야 하는지, MRO(메서드 해석 순서)로 다중 상속을 안전하게 다루는 법, 그리고 다형성과 추상 클래스로 유연한 설계를 만드는 방법을 배웁니다.
🔗 상속의 기초 — is-a 관계
class Animal:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def breathe(self) -> str:
return f"{self.name}이(가) 숨을 쉽니다"
def speak(self) -> str:
raise NotImplementedError("서브클래스에서 구현해야 합니다")
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name!r}, age={self.age})"
class Dog(Animal):
def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age) # 부모 __init__ 호출 — 필수!
self.breed = breed
def speak(self) -> str: # 오버라이딩
return f"{self.name}: 왈왈!"
def fetch(self, item: str) -> str:
return f"{self.name}이(가) {item}을(를) 가져왔습니다"
class Cat(Animal):
def __init__(self, name: str, age: int, indoor: bool = True):
super().__init__(name, age)
self.indoor = indoor
def speak(self) -> str:
return f"{self.name}: 야옹~"
def purr(self) -> str:
return f"{self.name}이(가) 그루밍합니다"
dog = Dog("뽀삐", 3, "골든리트리버")
cat = Cat("나비", 5)
print(dog.breathe()) # 상속받은 메서드
print(dog.speak()) # 오버라이딩된 메서드
print(dog.fetch("공"))
# isinstance: 상속 관계 포함
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True — 부모 클래스도 포함
print(isinstance(dog, Cat)) # False
# issubclass
print(issubclass(Dog, Animal)) # True
print(issubclass(Dog, Cat)) # False
🔍 MRO — 메서드 해석 순서와 C3 선형화
# 단순 상속의 MRO
class A:
def method(self):
return "A.method"
class B(A):
def method(self):
return "B.method"
class C(B):
pass # method 없음 → B에서 상속
c = C()
print(c.method()) # B.method
print(C.__mro__) # (<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)
# 탐색 순서: C → B → A → object
# 다중 상속의 MRO (다이아몬드 문제)
class Base:
def hello(self):
return "Base.hello"
class Left(Base):
def hello(self):
return f"Left → {super().hello()}"
class Right(Base):
def hello(self):
return f"Right → {super().hello()}"
class Child(Left, Right): # 다중 상속
def hello(self):
return f"Child → {super().hello()}"
print(Child.__mro__)
# (<class 'Child'>, <class 'Left'>, <class 'Right'>, <class 'Base'>, <class 'object'>)
print(Child().hello())
# Child → Left → Right → Base.hello
# super()는 MRO를 따라 순서대로 다음 클래스를 찾음
# super()의 진짜 의미: "MRO에서 현재 클래스의 다음 클래스"
# 부모 클래스가 아니라 "메서드 탐색 순서에서 다음"
🔀 믹스인 패턴 — 다중 상속의 올바른 활용
class JSONMixin:
"""JSON 직렬화 기능을 제공하는 믹스인"""
def to_json(self) -> str:
import json
return json.dumps(self.__dict__, ensure_ascii=False, default=str)
@classmethod
def from_json(cls, json_str: str):
import json
data = json.loads(json_str)
obj = cls.__new__(cls) # __init__ 없이 인스턴스 생성
obj.__dict__.update(data)
return obj
class ReprMixin:
"""__repr__ 자동 생성 믹스인"""
def __repr__(self) -> str:
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{self.__class__.__name__}({attrs})"
class ValidateMixin:
"""필드 유효성 검사 믹스인"""
def validate(self) -> bool:
for attr, validator in getattr(self, "_validators", {}).items():
if not validator(getattr(self, attr, None)):
raise ValueError(f"{attr} 유효성 검사 실패")
return True
# 믹스인 조합
class User(JSONMixin, ReprMixin):
def __init__(self, name: str, email: str):
self.name = name
self.email = email
u = User("홍길동", "hong@example.com")
print(repr(u)) # User(name='홍길동', email='hong@example.com')
json_str = u.to_json()
print(json_str) # {"name": "홍길동", "email": "hong@example.com"}
u2 = User.from_json(json_str)
print(u2.name, u2.email)
🦆 다형성 — 덕 타이핑과 추상 클래스
from abc import ABC, abstractmethod
# 추상 클래스: 인터페이스를 강제
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
def describe(self) -> str:
return f"{self.__class__.__name__}: 넓이={self.area():.2f}, 둘레={self.perimeter():.2f}"
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
class Circle(Shape):
PI = 3.14159265358979
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return self.PI * self.radius ** 2
def perimeter(self) -> float:
return 2 * self.PI * self.radius
# 추상 클래스는 직접 인스턴스화 불가
try:
s = Shape() # TypeError!
except TypeError as e:
print(e)
# 다형성: 같은 인터페이스, 다른 구현
shapes = [Rectangle(4, 6), Circle(5), Rectangle(3, 3), Circle(2)]
total_area = sum(s.area() for s in shapes)
for shape in shapes:
print(shape.describe())
print(f"전체 넓이: {total_area:.2f}")
# 덕 타이핑: 타입이 아닌 메서드가 있는지만 확인
class Triangle: # Shape를 상속하지 않아도
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def area(self) -> float:
s = (self.a + self.b + self.c) / 2
return (s * (s-self.a) * (s-self.b) * (s-self.c)) ** 0.5
def perimeter(self) -> float:
return self.a + self.b + self.c
# describe()가 없어도 area()와 perimeter()만 있으면 작동
triangle = Triangle(3, 4, 5)
print(f"삼각형 넓이: {triangle.area():.2f}") # 6.00
⚠️ 자주 하는 실수
- super().__init__() 빠뜨리기: 자식 클래스의
__init__에서super().__init__(...)을 호출하지 않으면 부모 클래스의 초기화가 일어나지 않아 부모의 속성을 사용할 때 AttributeError가 발생합니다. - 모든 공통점에 상속 사용: Car와 Engine이 공통 속성이 있다고 Engine을 상속하면 안 됩니다. Car has-a Engine이므로 컴포지션(속성으로 포함)이 올바릅니다. "is-a"(~는 ~이다) 관계일 때만 상속하세요.
- 다중 상속에서 MRO 무시: 다중 상속 시
__mro__를 확인하지 않으면 어떤 메서드가 호출될지 예측하기 어렵습니다.super()를 일관되게 사용하고 믹스인 패턴으로 복잡도를 낮추세요.
📝 정리 및 다음 강의 예고
- 상속: is-a 관계에서만.
super().__init__()은 항상 명시적으로. - MRO (C3 선형화): 다중 상속 탐색 순서.
Class.__mro__로 확인.super()는 MRO의 다음 클래스. - 믹스인: 기능을 작은 클래스로 분리해 다중 상속으로 조합하는 깔끔한 패턴.
- 추상 클래스(
ABC,@abstractmethod): 인터페이스 강제. 덕 타이핑과 함께 파이썬다운 다형성 구현.
다음 강의: 특수 메서드(dunder) —
__len__,__getitem__,__iter__,__eq__,__hash__등 파이썬 객체 프로토콜을 구현해 내장 함수와 연산자가 동작하게 만듭니다.
관련 주제
- 상속과 메서드 오버라이딩
- MRO(메서드 해석순서)
- 다중상속 다이아몬드 문제
- 믹스인 패턴
- 덕 타이핑
- 추상 클래스(ABC)
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 40강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
