12. 상속과 매직 메서드

🎯 이 장의 목표
  • 상속으로 기존 클래스를 확장하고, 메서드를 오버라이드한다.
  • super()로 부모의 기능을 재사용한다.
  • __str__·__eq__·__len__ 같은 매직 메서드로 객체를 자연스럽게 다룬다.
  • 캡슐화(_, __)의 관습을 이해한다.

상속: 기존 클래스를 물려받기

동물원 프로그램을 만든다고 합시다. 강아지·고양이·새… 모두 "이름이 있고, 먹는다"는 공통점이 있습니다. 클래스마다 nameeat()을 똑같이 반복해 쓰면 낭비입니다(초급편의 DRY 원칙을 떠올리세요).

상속(inheritance)은 공통 부분을 부모 클래스에 한 번 정의하고, 자식 클래스가 그것을 물려받는 방식입니다.

PYTHON
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을 상속한다"는 뜻입니다. Dogeat()을 직접 정의하지 않았는데도 쓸 수 있습니다 — 부모에게 물려받았기 때문입니다.

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__에서 자주 쓰입니다.

PYTHON
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 했을 때 보이는 모습

매직 메서드 없이 객체를 출력하면 알아보기 힘든 메모리 주소가 나옵니다.

PYTHON
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()이 그 결과를 씁니다.

PYTHON
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__이 쓰입니다.

PYTHON
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__를 정의하면 "내용이 같은가"를 우리가 정의할 수 있습니다.

PYTHON
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
PYTHON
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__ 덕분
📌 핵심
핵심: 매직 메서드는 우리 객체를 Python의 내장 문법(print·==·len·+)에 자연스럽게 끼워 넣는 연결 고리다. 최소한 __init____repr__은 습관적으로 정의하면 좋다.

캡슐화: 안전하게 감추기

객체의 어떤 데이터는 외부에서 함부로 바꾸면 안 됩니다(예: 잔액을 검증 없이 음수로 만들기). Python은 관습으로 이를 표현합니다.

표기의미강제력
self.balance공개(public). 자유롭게 접근
self._balance"내부용이니 직접 만지지 마세요"라는 약속관습뿐(접근은 됨)
self.__balance이름이 변형되어 외부 접근이 어려워짐약한 강제
PYTHON
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
💡 팁
Python은 다른 언어처럼 "진짜 못 건드리게" 막지 않습니다. 밑줄은 "신사협정"입니다 — "이건 내부 구현이니 직접 쓰지 말고 메서드를 통하세요"라는 신호죠. 이렇게 데이터를 메서드 뒤로 감싸 안전하게 다루는 것을 캡슐화(encapsulation)라고 합니다.

isinstance: 객체의 정체 확인

어떤 객체가 특정 클래스의 인스턴스인지 isinstance()로 확인합니다. 상속 관계도 인식합니다.

PYTHON
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의 Circleradius를 추가하려 합니다. __init__에서 super()를 써서 name은 부모가 처리하게 하고 radius만 추가하세요. (name"원"으로 고정해도 됩니다.)

문제 3. Temperature 클래스에 celsius 속성을 두고, __str__을 정의해 print() 했을 때 "25°C" 형태가 나오게 하세요.

문제 4. 다음 코드의 출력은? __eq__에 주목하세요.

PYTHON
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.

PYTHON
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.

PYTHON
class Circle(Shape):
    def __init__(self, radius):
        super().__init__("원")    # 부모가 name 처리
        self.radius = radius

c = Circle(10)
print(c.describe(), "/ 반지름", c.radius)   # 나는 원이다 / 반지름 10

3.

PYTHON
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.

PYTHON
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. 컴프리헨션