12. 상속과 매직 메서드
- 상속으로 기존 클래스를 확장하고, 메서드를 오버라이드한다.
super()로 부모의 기능을 재사용한다.__str__·__eq__·__len__같은 매직 메서드로 객체를 자연스럽게 다룬다.- 캡슐화(
_,__)의 관습을 이해한다.
상속: 기존 클래스를 물려받기
동물원 프로그램을 만든다고 합시다. 강아지·고양이·새… 모두 "이름이 있고, 먹는다"는 공통점이 있습니다. 클래스마다 name과 eat()을 똑같이 반복해 쓰면 낭비입니다(초급편의 DRY 원칙을 떠올리세요).
상속(inheritance)은 공통 부분을 부모 클래스에 한 번 정의하고, 자식 클래스가 그것을 물려받는 방식입니다.
class Animal: # 부모 클래스 (기반/베이스 클래스) def __init__(self, name): self.name = name def eat(self): return f"{self.name}이(가) 먹는다" def speak(self): return "..." class Dog(Animal): # Animal을 상속 (괄호 안에 부모) def speak(self): # speak만 새로 정의 (오버라이드) return "멍멍!" class Cat(Animal): def speak(self): return "야옹!" choco = Dog("초코") print(choco.eat()) # 초코이(가) 먹는다 ← Animal에서 물려받음 print(choco.speak()) # 멍멍! ← Dog가 새로 정의
class Dog(Animal):의 괄호가 "Animal을 상속한다"는 뜻입니다. Dog는 eat()을 직접 정의하지 않았는데도 쓸 수 있습니다 — 부모에게 물려받았기 때문입니다.
classDiagram
class Animal {
+name
+eat()
+speak()
}
class Dog {
+speak()
}
class Cat {
+speak()
}
Animal <|-- Dog
Animal <|-- Cat
오버라이드(override)
자식이 부모와 같은 이름의 메서드를 다시 정의하면, 자식 객체는 자기 버전을 씁니다. 위에서 Dog.speak()가 Animal.speak()를 덮어쓴(override) 것입니다. eat()은 오버라이드하지 않았으니 부모 것을 그대로 씁니다.
super(): 부모의 기능 재사용
자식에서 부모의 메서드를 완전히 대체하지 않고, 확장하고 싶을 때가 있습니다. 이때 super()로 부모 메서드를 불러 씁니다. 특히 __init__에서 자주 쓰입니다.
class Puppy(Dog): def __init__(self, name, age): super().__init__(name) # 부모(Dog→Animal)의 __init__ 호출: name 처리 self.age = age # 자식만의 추가 속성 def speak(self): return super().speak() + " (애기 목소리)" # 부모 결과에 덧붙임 p = Puppy("뽀삐", 1) print(p.name, p.age) # 뽀삐 1 print(p.speak()) # 멍멍! (애기 목소리)
super().__init__(name)은 "부모의 __init__을 실행해 name 처리를 맡기고", 그다음 자식만의 self.age를 추가합니다. 부모가 하던 일을 다시 적지 않아도 됩니다.
super()는 "부모 클래스"를 가리키는 말입니다. super().메서드()는 "부모의 그 메서드를 실행하라". __init__에서 super().__init__(...)을 호출하는 패턴은 매우 흔하니 익혀두세요.⭐ 매직 메서드: 객체를 파이썬답게
11장에서 __init__을 봤습니다. 이처럼 앞뒤 밑줄 두 개가 붙은 매직 메서드(magic method, 던더 메서드)를 정의하면, 우리 객체가 Python의 기본 문법(print, ==, len, + 등)과 자연스럽게 어울리게 됩니다.
__str__: print 했을 때 보이는 모습
매직 메서드 없이 객체를 출력하면 알아보기 힘든 메모리 주소가 나옵니다.
class Point: def __init__(self, x, y): self.x = x self.y = y p = Point(3, 5) print(p) # <__main__.Point object at 0x7f...> ← 무의미
__str__을 정의하면 print()나 str()이 그 결과를 씁니다.
class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" p = Point(3, 5) print(p) # (3, 5) ← 읽기 좋게!
__repr__: 개발자용 표현
__str__이 "사용자에게 보여줄 모습"이라면, __repr__은 "개발자가 디버깅할 때 보는 정확한 표현"입니다. 리스트 안에 객체가 있거나 REPL에서 그냥 객체를 칠 때 __repr__이 쓰입니다.
class Point: def __init__(self, x, y): self.x = x; self.y = y def __str__(self): return f"({self.x}, {self.y})" def __repr__(self): return f"Point({self.x}, {self.y})" p = Point(3, 5) print(p) # (3, 5) ← __str__ print([p]) # [Point(3, 5)] ← 리스트 안에서는 __repr__
__repr__을 정의하세요. __str__이 없으면 Python이 __repr__을 대신 씁니다. 관습적으로 __repr__은 Point(3, 5)처럼 "그 코드를 실행하면 같은 객체가 만들어질 법한" 형태로 적습니다.__eq__: == 비교
매직 메서드 없이는 ==가 "같은 객체인가(메모리상 동일한가)"만 봅니다. __eq__를 정의하면 "내용이 같은가"를 우리가 정의할 수 있습니다.
class Money: def __init__(self, amount): self.amount = amount def __eq__(self, other): return self.amount == other.amount print(Money(100) == Money(100)) # True (__eq__ 없으면 False였음) print(Money(100) == Money(200)) # False
__eq__(self, other)에서 두 매개변수가 무엇인지 짚고 넘어갑시다. a == b를 실행하면 Python은 a.__eq__(b)를 부릅니다. 즉 == 왼쪽이 self, 오른쪽이 other(비교 상대)가 됩니다. Money(100) == Money(200)이라면 self는 왼쪽 100원, other는 오른쪽 200원이고, self.amount == other.amount로 두 금액을 비교하는 것이죠. other라는 이름은 관습일 뿐, "비교 대상으로 넘어온 다른 객체"를 뜻합니다.
자주 쓰는 매직 메서드
| 매직 메서드 | 언제 자동 호출 | 예 |
|---|---|---|
__init__ | 객체 생성 | Point(3, 5) |
__str__ | print(), str() | print(p) |
__repr__ | REPL, 리스트 안, 디버깅 | [p] |
__eq__ | == 비교 | a == b |
__len__ | len() | len(cart) |
__add__ | + 연산 | a + b |
class Cart: def __init__(self): self.items = [] def add(self, item): self.items.append(item) def __len__(self): # len(cart)이 이걸 호출 return len(self.items) cart = Cart() cart.add("사과") cart.add("배") print(len(cart)) # 2 ← __len__ 덕분
print·==·len·+)에 자연스럽게 끼워 넣는 연결 고리다. 최소한 __init__과 __repr__은 습관적으로 정의하면 좋다.캡슐화: 안전하게 감추기
객체의 어떤 데이터는 외부에서 함부로 바꾸면 안 됩니다(예: 잔액을 검증 없이 음수로 만들기). Python은 관습으로 이를 표현합니다.
| 표기 | 의미 | 강제력 |
|---|---|---|
self.balance | 공개(public). 자유롭게 접근 | — |
self._balance | "내부용이니 직접 만지지 마세요"라는 약속 | 관습뿐(접근은 됨) |
self.__balance | 이름이 변형되어 외부 접근이 어려워짐 | 약한 강제 |
class Account: def __init__(self): self._balance = 0 # 밑줄 하나: "건드리지 마세요" def deposit(self, amount): if amount > 0: # 검증 후에만 변경 self._balance += amount def get_balance(self): return self._balance a = Account() a.deposit(500) print(a.get_balance()) # 500
isinstance: 객체의 정체 확인
어떤 객체가 특정 클래스의 인스턴스인지 isinstance()로 확인합니다. 상속 관계도 인식합니다.
choco = Dog("초코") print(isinstance(choco, Dog)) # True print(isinstance(choco, Animal)) # True ← Dog는 Animal이기도 함 print(isinstance(choco, Cat)) # False
이 장에서 배운 것
- 상속(
class 자식(부모):)으로 공통 기능을 물려받아 중복을 없앤다. 같은 이름 메서드를 다시 정의하면 오버라이드. super()로 부모의 메서드(특히__init__)를 재사용하며 확장한다.- 매직 메서드(
__str__·__repr__·__eq__·__len__등)로 객체를 Python 기본 문법에 자연스럽게 연결한다. - 캡슐화:
_이름은 "내부용"이라는 관습적 신호다. 데이터를 메서드로 감싸 안전하게 다룬다. isinstance()로 객체가 특정 클래스(또는 그 부모)의 인스턴스인지 확인한다.
🧪 실습 문제
문제 1. Shape 부모 클래스에 name 속성과 describe() 메서드("나는 {name}이다" 반환)를 두세요. 이를 상속한 Circle 클래스를 만들고, Circle("원") 객체로 describe()를 호출해 출력하세요.
문제 2. 문제 1의 Circle에 radius를 추가하려 합니다. __init__에서 super()를 써서 name은 부모가 처리하게 하고 radius만 추가하세요. (name은 "원"으로 고정해도 됩니다.)
문제 3. Temperature 클래스에 celsius 속성을 두고, __str__을 정의해 print() 했을 때 "25°C" 형태가 나오게 하세요.
문제 4. 다음 코드의 출력은? __eq__에 주목하세요.
class Box: def __init__(self, size): self.size = size def __eq__(self, other): return self.size == other.size print(Box(5) == Box(5)) print(Box(5) == Box(9))
문제 5. Playlist 클래스를 만드세요. 곡을 리스트로 저장하고, add(song) 메서드와 __len__(곡 수 반환), __str__("플레이리스트 (N곡)" 형태)을 정의하세요. 곡 2개를 넣고 print()와 len() 결과를 확인하세요.
<details>
<summary>✅ 정답·해설 보기</summary>
1.
class Shape: def __init__(self, name): self.name = name def describe(self): return f"나는 {self.name}이다" class Circle(Shape): pass print(Circle("원").describe()) # 나는 원이다
(pass는 "내용 없음"을 뜻합니다. 부모를 그대로 물려받기만 할 때 씁니다.)
2.
class Circle(Shape): def __init__(self, radius): super().__init__("원") # 부모가 name 처리 self.radius = radius c = Circle(10) print(c.describe(), "/ 반지름", c.radius) # 나는 원이다 / 반지름 10
3.
class Temperature: def __init__(self, celsius): self.celsius = celsius def __str__(self): return f"{self.celsius}°C" print(Temperature(25)) # 25°C
4. True, False. __eq__가 size를 비교하므로 크기가 같으면 True.
5.
class Playlist: def __init__(self): self.songs = [] def add(self, song): self.songs.append(song) def __len__(self): return len(self.songs) def __str__(self): return f"플레이리스트 ({len(self.songs)}곡)" pl = Playlist() pl.add("곡A"); pl.add("곡B") print(pl) # 플레이리스트 (2곡) print(len(pl)) # 2
</details>
◀️ 이전 장: 11. 클래스와 객체 | ▶️ 다음 장: 13. 컴프리헨션