2026. 5. 29. 15:52ㆍKDT/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의 의미
- 값을 반환
- 함수 상태 저장
- 다음 호출 시 이어서 실행
- 제너레이터는 자동으로 __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 많이 사용
'KDT > 1. Python' 카테고리의 다른 글
| [10일차] CSV (작성중) (0) | 2026.06.04 |
|---|---|
| [9일차-1] 모듈 (0) | 2026.06.01 |
| [7일차] 사용자정의함수, 객체지향프로그래밍(캡슐화, 상속) (0) | 2026.05.28 |
| [6일차] 제어문(반복문), 사용자 정의 함수(함수 생성, 전역변수, 지역변수) (0) | 2026.05.27 |
| [5일차] 컬렉션 타입(튜플, 세트, 딕셔너리), 제어문(조건문, 반복문) (0) | 2026.05.26 |