[8일차] 객체지향 프로그래밍(다형성), 예외, 매직메서드

2026. 5. 29. 15:52KDT/1. Python

다중 상속(7일차)에 이어서

# MRO (Method Resolution Order)

  • 다중 상속 시 메서드나 속성을 찾는 순서를 정의하는 규칙
  • 클래스 간의 메서드 충돌을 해결
# stack에 쌓이는 것을 "print ___ 끝" 으로 확인할 수 있음
class Base:
    def hello(self):
        print("Base")
        
class Clean(Base):
    def hello(self):
        print("Clean")
        super().hello()
        print("Clean 끝")
        

class Pack(Base):
    def hello(self):
        print("Pack")
        super().hello()
        print("Pack 끝")
        

class Product(Clean, Pack):
    def hello(self):
        print("Product")
        super().hello()
        print("Product 끝")

p = Product()
p.hello()

print(Product.mro())

# 3. 다형성 Polymorphism

  • 같은 이름의 메서드나 함수를 호출하더라도, 객체의 종류에 따라 서로 다른 방식으로 동작하는 특성
  • 정적 타입 언어처럼 명시적인 인터페이스 구현을 요구하지 않고, 동일한 메서드를 가지고 있으면 타입과 관계없이 사용할 수 있는 "덕 타이핑"을 기반으로 다형성을 자연스럽게 지원

# 덕 타이핑 (duct typing) : 객체의 실제 클래스가 무엇인지 중요하지 않고, 필요한 메서드나 기능을 가지고 있으면 같은 방식으로 사용하는 것

class CardPayment:
    def pay(self, amount):
        print(f"카드로 {amount}원 결제합니다")

class CashPayment:
    def pay(self, amount):
        print(f"현금으로 {amount}원 결제합니다")

class KakaoPay:
    def pay(self, amount):
        print(f"카카오페이로 {amount}원 결제합니다")

def process_payment(payment, amount):   # 변수 payment, amount의 타입을 신경쓸 필요가 없음 (일단 받으니까)
    payment.pay(amount)

card = CardPayment()
cash = CashPayment()
kakao = KakaoPay()

process_payment(card, 10000)
process_payment(cash, 5000)
process_payment(kakao, 2000)

# 공통 함수인 process_payment를 사용해서 pay()를 사용
# process_payment의 payment에 int를 넣게 되면 int형변수.pay()가 될텐데, 그럼 말도안되니까 에러 발생
# (payment에는 어떤 "객체"가 들어와야 정상 작동 가능)
# (card, cash, kakako는 객체니까 정상 작동)

# 4. 추상화 Abstraction

  • 복잡한 내부 구현은 숨기고, 외부에서는 필요한 핵심 기능만 간단하게 사용할 수 있도록 하는 개념
  • 파이썬에서는 일반 클래스 설계뿐 아니라 abc 모듈의 ABC와 @abstractmethod를 사용해 추상 클래스 정의하고,
    반드시 구현해야 하는 메서드를 강제함으로써 일관된 구조 유지 가능
  • 즉, 추상화는 "어떻게 동작하는지" 보다 "무엇을 할 수 있는지"에 초점
from abc import ABC, abstractmethod # ABC 클래스와 abstractmethod 함수를 가져다 쓰겠다 (=메모리에 올리겠다)

# 추상 클래스
class Payment(ABC):
    
    @abstractmethod     # abstractmethod를 사용하기 위해 ABC를 상속 받은 것임
    def pay(self, amount):
        pass  # 반드시 구현해야 하는 메서드

# 카드 결제
class CardPayment(Payment):
    def pay(self, amount):
        print(f"카드로 {amount}원 결제합니다")

# 현금 결제
class CashPayment(Payment):
    def pay(self, amount):
        print(f"현금으로 {amount}원 결제합니다")

# 카카오페이
class KakaoPay(Payment):
    def pay(self, amount):
        print(f"카카오페이로 {amount}원 결제합니다")

# 공통 처리 함수
def process_payment(payment: Payment, amount):
    payment.pay(amount)

# 사용
card = CardPayment()
cash = CashPayment()
kakao = KakaoPay()

process_payment(card, 10000)
process_payment(cash, 5000)
process_payment(kakao, 2000)

# abstractmethod의 역할 : Payment를 상속 받은 클래스들(CardPayment, CashPayment, KakaoPay)은 Payment의 pay 메소드를 무조건 오버라이딩해서 사용해야돼 라는 강제성을 부여
# 오버라이딩 하지 않으면 아래와 같은 에러 발생
# TypeError: Can't instantiate abstract class KakaoPay without an implementation for abstract method 'pay'

# 예외 Exception

  • 프로그램 실행 중 발생할 수 있는 예상치 못한 문제 또는 오류 상황
  • 예외가 발생하면 프로그램은 중단되기 때문에 이를 적절하게 처리하여 중단을 방지하거나 오류에 대한  정보를 사용자에게 제공해야 함

# 예외 계층 구조

BaseException
 ├── Exception        ← 우리가 보통 사용하는 예외
 │    ├── ValueError
 │    ├── TypeError
 │    ├── ZeroDivisionError
 │    └── ...
 ├── KeyboardInterrupt
 └── SystemExit
  • BaseException이 따로 있는 이유 : "진짜 에러"와 "프로그램 제어용 이벤트"를 구분하기 위함

1. Exception

  • 코드 실행 중 발생한 일반적 오류
    • 0으로 나누기
    • 잘못된 타입
    • 값 오류

2. KeyboardInterrupt

  • 사용자가 Ctrl+C 눌렀을 때 발생 (프로그램 강제로 멈추는 입력키)

3. SystemExit

  • 프로그램 종료 요청 -> 내부적으로 exit() 호출 시 발생
    -> KeyboardInterrupt와 SystemExit은 사실 "에러"가 아니라 프로그램을 멈추거나 종료하기 위한 신호

# 예외 처리 기본 구조

try:
    # 예외가 발생할 가능성이 있는 코드
except ExceptionType1:  # 'ExceptionType1'에는 실제 예외 유형이 들어갑니다.
    # ExceptionType1 예외가 발생했을 때 실행될 코드
except ExceptionType2:  # 'ExceptionType2'에는 다른 예외 유형이 들어갑니다.
    # ExceptionType2 예외가 발생했을 때 실행될 코드
# 추가적인 except 블록을 계속 추가할 수 있습니다.
else:
    # try 블록에서 예외가 발생하지 않았을 때 실행될 코드
finally:
    # 예외 발생 여부와 관계없이 항상 실행될 코드
  • else, finally는 optional
try:
    raise KeyboardInterrupt
except Exception:
    print("Exception 잡힘")
except BaseException:
    print("BaseException 잡힘")

# 👉 위험한 방식입니다. except: = BaseException까지 잡습니다.
# except 뒤에 생략하면 BaseException
# 다만 KeyboardInterrupt와 SystemExit도 처리하게 되니까 예외만 처리하고 싶으면 Exception으로 사용할 것

 

1. rasie

  • 의도적으로 예외를 발생시키거나 (throw)
  • 현재 발생한 예외를 다시 전달 (re-raise)하기위한 키워드
  • 이를 통해 잘못된 입력이나 비정상적인 상태를 명확하게 알리고, 호출한 쪽에서 적절히 처리하도록 흐름 제어

2. 파이썬 주요 예외 타입 정리

try:
    print(10 / 3)
    print(5 / 0)
    print(4 / 2)
except Exception:       # Exception 대신 ZeroDivisionError 사용해도 굳
    print('예외 발생!')
    
print('프로그램을 종료합니다')

# 0으로 나누는 바보가 어딨어? 가 아니라 변수로 사용할 때 어디선가 0값이 들어오게 될 수 있음
try:
    data = [10, 20, 30, 40, 50]
    # print(data[5])
    # print(int('안녕'))
    print(5 / 0)
except IndexError:
    print('인덱스 오류')
except ValueError:
    print('입력 오류')
except ZeroDivisionError:
    print('0으로 나눌 수 없음')
except Exception:       
    print('예외 발생!')
else:
    print('에러가 발생하지 않은 정상적인 프로그램')
finally:
    print('에러에 관계없이 무조건 실행되는 문장')

print('프로그램을 종료합니다')

- 맨 위로 (IndexError 위로) 가게 되면 Exception에서 처리되기 때문에 원하는 대로 처리 불가능

- (위에서 아래로 순차적으로 진행)

- elif 처럼 하나 처리 되면 try문 빠져 나감


# 예외 객체 내부 구조

Exception 객체
├── args        → 에러 메시지
├── __str__()   → 사용자용 메시지
├── __repr__()  → 개발자용 표현
├── __class__   → 에러 타입
└── traceback   → 에러 발생 위치 정보
try:
    int("abc")
except Exception as e:      # e라는 변수 사용
    print("에러 메시지:", e)
    print("에러 타입:", type(e))
    print("args:", e.args)
    print(e.__str__())
    print(e.__repr__())
    print(e.__class__)
    print(e.__traceback__)

# 👉 as를 사용하는 이유는 발생한 예외 객체를 변수로 받아서, 그 안에 담긴 정보(에러 메시지, 타입 등)를 활용하기 위해서입니다.
def func1():
    n = int(input('짝수를 입력하세요: '))
    if n % 2 == 1:
        raise Exception('홀수를 입력했어요!!')
    print(n)

# func1()

try:
    func1()
except Exception as e:
    print('예외가 발생: ', e)
# 1,2,3,4번 어디에서든 해도 처리가 됨
# -> 함수의 처리 특징인 stack에서 처리되니까 func1,2,3 모두 거쳐가기 때문임
def func1():
    func2()
    # 2번
    # try:
    #     func2()
    # except TypeError:
    #     print('타입이 올바르지 않습니다')

def func2():
    func3()
    # 3번
    # try:
    #     func3()
    # except TypeError:
    #     print('타입이 올바르지 않습니다')

def func3():
    # 4번
    try:
        print('%d' % '문자열')      # 에러 : %d는 정수형인데 문쟈열을 넣음
    except TypeError:
        print('타입이 올바르지 않습니다')

func1()

# 1번
# try:
#     func1()
# except TypeError:
#     print('타입이 올바르지 않습니다')

 


# 매직 메서드 magic method

  • 특정 문법이나 연산 실행 시 자동으로 호출되는 특별한 메서드
  • 양쪽에 밑줄 2개(__) 붙는 형태
  • 이 메서드들을 클래스에 구현하면 len(obj), print(obj), obj[0], obj+obj 같은 파이썬의 기본 문법과 자연스럽게 연결되어 객체의 동작을 직접 정의 가능
  • 즉, 객체를 리스트처럼 사용하거나, 연산을 수행하거나, 출력 형태를 바꾸는 등 파이썬의 문법과 객체를 이어주는 핵심 기능

# 1. 객체 출력

1. __str__() 

  • 객체를 사람이 읽기 쉬운 문자열로 표현하기 위해 사용하는 매직 매서드
  • print(obj)나 str(obj)가 호출될 때 자동 실행
  • 이 메서드를 클래스에 정의하면 객체 출력 시, 기본 주소값 대신 원하는 형태로 출력가능
class Fruit:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    def __str__(self):      # str()과 연결되어있는 매직메서드
                            # 루트 클래스(object)의 str()에 오버라이딩
                            # 이 객체에 대해서만 오버라이딩된 것 (다른 곳에선 사용 가능)
        return f"{self.name} {self.quantity}개"

apple = Fruit("사과", 10)

print(apple)        # __str__ 호출
print(str(apple))   # __str__ 호출
더보기

def __str__(self):

    return f"{self.name} {self.quantity}개"

 

두 줄을 주석 처리하고 print(apple) 실행하면 <main.Fruit object at 0x0000025CAAC25D00>라고 주소 출력됨

 

1. __repr__()

  • 객체를 개발자 관점에서 정확하게 표현하는 문자열 반환하는 매직 메서드
  • 주로 디버깅이나 로그에서 사용
  • repr(obj)를 호출하거나 인터프리터에서 객체를 직접 평가할 때 자동 호출
  • 가능하다면 객체를 다시 생성할 수 있는 형태의 문자열 반환하는 것이 권장
  • __str__()이 사람이 읽기 좋은 출력이라면
    __repr__()은 객체의 구조와 상태를 명확하게 보여주는 공식적 표현
class Fruit:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    def __repr__(self):
        return f"Fruit(name='{self.name}', quantity={self.quantity})"
        # 관례 : 출력 시 객체인 것을 한 눈에 알 수 있음

apple = Fruit("사과", 10)

print(repr(apple))  # __repr__ 호출
print(apple)        # __str__ 없으면 __repr__ 호출 (str우선)

# 제너레이터 generator

  • yield 키워들르 사용해 값을 한 번에 하나씩 생성해 반환하는 반복 객체
  • 함수 형태로 간단하게 이터레이터를 만들 수 있는 파이썬 기능
  • 일반 함수처럼 보이지만 yield를 만나면 값을 반환하면서도 실행 상태를 유지하고
    다음 호출 시 이어서 실행되기 때문에 전체 데이터를 한 번에 만들지 않고 필요할 때마다 생성할 수 있어 메모리 효율이 매우 높음
    (iterable과 iterator할 때 이후 제너레이터 학습 예정이라고 했음
    range() 함수 : 제너레이터로 만들어짐)
# for문이 어떻게 도는지 설명 코드
class Counter:
    def __init__(self, max):
        self.max = max
        self.current = 0

    def __iter__(self):
        return self  # 자기 자신을 iterator로 사용

    def __next__(self):
        if self.current < self.max:
            self.current += 1
            return self.current
        else:
            raise StopIteration     # 에러 발생

counter = Counter(3)

# iter(), next(), StopIteration
for num in counter:
    print(num)

👉 for문이 내부적으로 자동으로 iter() 호출

👉 이터레이터는 이터러블로부터 실제로 값을 하나씩 꺼내는 객체. __iter__()와 __next__() 메서드를 모두 구현

    __iter__()는 반복을 시작하게

    __next__()는 값을 하나씩 꺼내고

    더 이상 return 없으면 StopIteration 예외를 발생시켜 반복 종료

 

# 제너레이터 설명 코드
def counter(max):
    current = 0
    while current < max:
        current += 1
        yield current

for num in counter(3):
    print(num)

# current가 0으로 계속 초기화되지 않도록 yield로 값을 리턴시키면서 기억하도록 함
# 위의 경우가 아니게 되면(=yield를 사용하지 않으면) counter는 3을 계속 매개변수로 전달하고 current += 1 한 것을 기억하지 못하기 때문에 0으로 계속 초기화되고 1,2,3이 찍히지 않게 됨

 

# yield의 의미

  1. 값을 반환
  2. 함수 상태 저장
  3. 다음 호출 시 이어서 실행
  • 제너레이터는 자동으로 __iter__ + __next__를 생성

# 3. 인덱싱⭐

1. __getitem__()

  • 객체에서 인덱스나 키를 이용해 값을 조회할 때 호출되는 매직 메서드
  • obj[index] 또는 obj[key] 형태로 접근하면 자동 실행
print(apple[0])		# getter
class MyList:
    def __init__(self):
        self.data1 = []
        self.data2 = []

    def __getitem__(self, index):
        return self.data1[index]

ml = MyList()
ml.data1 = [10, 20, 30]
ml.data2 = [50, 60]

print(ml[0])    # TypeError: 'MyList' object is not subscriptable

# getitem 없는 경우 : ml 자체는 리스트가 아니고 클래스 안의 속성 값이 리스트인 것이기 때문에 에러 발생

 

2. __setitem__()

  • 객체에서 인덱스나 키로 값을 할당할 때 호출되는 매직 메서드
  • obj[index] = value 형태의 코드 실행 시 자동 호출
  • 이 메서드를 구현하면 사용자 정의 객체에 값을 저장하거나 수정하는 동작을 제어 가능
  • 전달된 인덱스와 값을 이용해 내부 데이터를 업데이트하도록 구현
apple[0] = 10

 

3. __delitem__()

  • 객체에서 인덱스나 키를 이용해 값을 삭제할 때  호출되는 매직 메서드
  • del obj[index] 형태의 코드 실행 시 자동 호출
  • 이 메서드 구현하면 사용자 정의 객체에서 특정 데이터를 삭제하는 로직을 정의 가능
  • 전달된 인덱스 기반으로 내부 데이터 제거
class MyList:
    def __init__(self):
        self.data = []

    def __getitem__(self, index):
        return self.data[index]

    def __setitem__(self, index, value):
        self.data[index] = value

    def __delitem__(self, index):
        del self.data[index]
        
ml = MyList()

ml.data = [10, 20, 30]      # 직접 접근 추천하지 않지만 예제니까^^;
print(ml[0])    # __getitem__ → 10

ml[1] = 99  # __setitem__
print(ml.data) # [10, 99, 30]

del ml[0]      # __delitem__
print(ml.data) # [99, 30]

# 4. 객체를 함수, 값처럼 사용

1. __call__()

  • 객체를 함수처럼 호출 가능하도록 만들어주는 매직 메서드
  • obj() 형태로 객체 실행 시 자동 호출
  • 이 메서드 구현하면 클래스 인스턴스가 일반 함수처럼 동작 가능
  • 내부 상태를 활용한 연산이나 처리 로직 간결 표현 가능
class Multiplier:
    def __init__(self, n):
        self.n = n

    def __call__(self, x):
        return x * self.n

m = Multiplier(3)

print(m(10))  # 30
# 함수처럼 매개변수를 넣어 사용 가능

 

2. __len__() ⭐

  • 객체의 길이를 정의하는 매직 메서드
  • len(obj) 호출 시 자동 실행
  • 이 메서드 구현하면 사용자 정의 객체도 리스트나 문자열처럼 길이를 가질 수 있음
  • 내부 데이터 개수를 반환하도록 작성
class Basket:
    def __init__(self):
        self.items = []

    def __len__(self):
        return len(self.items)

b = Basket()
b.items = ["사과", "바나나", "오렌지"]

print(len(b))  # 3

# 5. with 문

  • 파일, 네트워크, DB연결처럼 사용 후 반드시 정리(clean-up) 필요한 자원을 안전하게 관리하기 위한 문법
  • 블록이 시작될 때 필요한 작업을 수행하고 종료될 때 자동으로 정리 작업 실행
  • 이를 통해 try-finally 직접 작성할 필요 없음
  • 예외 발생하더라도 자원이 정상적으로 해제되도록 보장

1. __enter__()

  • with문에 진입할 때 자동 호출하는 매직 메서드
  • 자원을 준비하거나 초기화하는 역할
  • 이 메서드의 반환 값은 as 뒤의 변수에 할당되어 with 블록 내부에서 사용

2. __exit__()

  • with 블록 끝날 때 자동 호출되는 매직 메서드
  • 사용한 자원을 정리(clean-up)하는 역할
  • 예외 발생 여부와 관계없이 반드시 실행
  • 예외가 발생한 경우, 예외 타입, 값, traceback 정보를 인자로 받아 처리 가능
class MyResource:
    def __enter__(self):
        print("자원 열기")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("자원 정리") 
        
with MyResource() as res:
    print("자원 사용 중")
class MyResource:
    def __enter__(self):
        print("자원 열기")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("자원 정리")
        print("예외 타입:", exc_type)

with MyResource():
    print("작업 중")
    1 / 0  # ZeroDivisionError: division by zero 발생
    
# 👉 에러가 나도 exit() 실행됨

* 이후 데이터 처리를 위해서 인덱싱 정확히 알기

* 데이터를 클래스로 관리하게 될텐데, 이 과정에서 getitem 쓰이게 될 것

* 딥러닝 모델 시 len 많이 사용