매직 메서드(magic method) 는 파이썬 특정 문법이나 연산이 실행될 때 자동으로 호출되는 특별한 메서드이다. __len__, __str__, __getitem__ 처럼 이름의 앞뒤에 밑줄 두 개가 붙어 있어 dunder method 라고도 부른다.
매직 메서드를 클래스에 구현하면 len(obj) , print(obj) , obj[0] , obj + other , with obj: 같은 파이썬의 기본 문법과 사용자 정의 객체를 자연스럽게 연결할 수 있다.
매직 메서드는 개발자가 직접 호출하기보다 파이썬 문법에 의해 자동으로 호출되는 메서드이다.
| 문법 | 호출되는 매직 메서드 |
|---|---|
| print(obj) | obj.__str__() |
| repr(obj) | obj.__repr__() |
| len(obj) | obj.__len__() |
| obj[index] | obj.__getitem__(index) |
| obj[index] = value | obj.__setitem__(index, value) |
| del obj[index] | obj.__delitem__(index) |
| obj() | obj.__call__() |
| for item in obj | obj.__iter__(), obj.__next__() |
| with obj: | obj.__enter__(), obj.__exit__() |
| obj1 + obj2 | obj1.__add__(obj2) |
매직 메서드는 파이썬 문법을 사용자 정의 클래스와 연결하는 약속이다.
객체를 출력할 때 가장 자주 사용하는 매직 메서드는 __str__() 과 __repr__()이다.
__str__() : 사용자에게 보여줄 문자열__str__() 은 객체를 사람이 읽기 쉬운 문자열로 표현할 때 사용한다. print(obj)또는 str(obj) 를 호출하면 자동으로 실행된다.
class Fruit:
def __init__(self, name, quantity):
self.name = name
self.quantity = quantity
def __str__(self):
return f"{self.name} {self.quantity}개"
apple = Fruit("사과", 10)
print(apple)
print(str(apple))
출력 결과 :
사과 10개
사과 10개
__str()__ 을 정의하지 않으면 객체를 출력했을 때 기본적으로 클래스 이름과 메모리 주소 형태가 출력된다.
__repr__() : 개발자를 위한 공식 표현__repr__() 은 객첼ㄹ 개발자 관점에서 명확하게 표현하는 문자열을 반환한다. repr(obj) 를 호출할 때 실행되며, 가능하다면 객체를 다시 생성할 수 있는 형태의 문자열을 반환하는 것이 권장된다.
class Fruit:
def __init__(self, name, quantity):
self.name = name
self.quantity = quantity
def __str__(self):
return f"{self.name} {self.quantity}개"
def __repr__(self):
return f"Fruit'{self.name}', quantity={self.quantity})"
apple = Fruit("사과", 10)
print(str(apple))
print(repr(apple))
출력 결과 :
사과 10개
Fruit(name='사과', quantity=10)
일반적으로 __str__() 은 사용자 친화적인 출력, __repr__() 은 디ㅓ깅에 유용한 출력을 담당한다.
반복 가능한 객체를 이해하려면 이터러블(iterable), 이터레이터(iterator), 제너레이터(generator)를 구분해야 한다.
| 개념 | 설명 |
|---|---|
| 이터러블 | iter()를 적용할 수 있는 객체이다. 예: 리스트, 튜플, 문자열 |
| 이터레이터 | iter()와 next()를 모두 구현해 값을 하나씩 꺼내는 객체이다. |
| 제너레이터 | yield를 사용해 간단히 만드는 이터레이터이다. |
__iter__() 와 __next__() 로 이터레이터 만들기for 문은 내부적으로 iter() 를 호출해 이터레이터를 얻고, 반복할 때마다 next() 를 호출한다.
class Counter:
def __init__(self, max_count):
self.max_count = max_count
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current < self.max_count:
self.current += 1
return self.current
raise StopIteration
counter = Counter(3)
for num in counter:
print(num)
출력 결과 :
1
2
3
__next__() 는 다음 값을 반환하고, 더 이상 반환할 값이 없으면 StopIteration 예외를 발생시켜 반복을 종료한다.
제너레이터는 yield 키워드를 사용해 값을 한 번에 하나씩 생성하는 함수이다. 일반 함수처럼 보이지만 yield 를 만다면 값을 반환하고 실행 상태를 유지한다. 다음 호출 시 이전 위치에서 이어서 실행된다.
def counter(max_count):
current = 0
while courrent < max_count:
current += 1
yield current
for num in counter(3)
print(num)
출력 결과 :
1
2
3
yield 의 핵심은 다음과 같다.
| 역할 | 설명 |
|---|---|
| 값을 반환한다 | yield 값의 값을 호출한 쪽에 전달한다. |
| 함수 상태를 저장한다 | 지역 변수와 실행 위치를 기억한다. |
| 이어서 실행한다 | 다음 next() 호출 때 멈춘 위치부터 실행한다. |
제너레이터는 자동으로 __iter__()와 __next__()를 제공하므로 직접 이터레이터 클래스를 만들지 않아도 된다.
next() 로 호출하기def counter(max_count):
current = 0
while current < max_count:
current += 1
yield current
gen = counter(2)
print(next(gen))
print(next(gen))
출력 결과 :
1
2
제너레이터는 모든 데이터를 한 번에 만들지 않고 필요할 때 하나씩 생성하므로 메모리 효율이 좋다.
사용자 정의 객체도 __getitem__(), __setitem__() , __delitem__() 을 구현하면 리스트나 딕셔너리처럼 사용할 수 있다.
| 문법 | 호출되는 메서드 |
|---|---|
| obj[index] | __getitem__(self, index) |
| obj[index] = value | __setitem__(self, index, value) |
| del obj[index] | __delitem__(self, 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])
ml[1] = 99
print(ml.data)
del m1[0]
print(ml.data)
출력 결과 :
10
[10, 99, 30]
[99, 30]
이렇게 구현하면 MyList 객체가 내부적으로 리스트를 감싸고 있어도 외부에서는 ml[0] 처럼 자연스럽게 사용할 수 있다.
obj[1:3] 처럼 슬라이싱 하면 __getitem__() 의 index 에 slice 객체가 전달된다.
class MyList:
def __init__(self, data):
self.data = data
def __getitem__(self,index):
return self.data[index]
ml = MyList([10, 20, 30, 40])
print(ml[1])
print(ml[1:3])
출력 결과 :
20
[20, 30]
__call__()을 구현하면 객체를 함수처럼 호출할 수 있다.
class Multiplier:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x * self.n
m = Multiplier(3)
print(m(10))
출력 결과 :
30
m(10) 은 내부적으로 m.__call__(10)을 호출한다. 객체가 상태를 가지고 있으면서 함수처럼 동작해야 할 때 유용한다.
__len__()__len__() 은 객체의 길이를 정의한다. len(obj) 를 호출할 때 자동으로 실행된다
class Basket:
def __init__(self):
self.items = []
def __len__(self):
return len(self.items)
basket = Basket()
basket.items = ["사과", "바나나", "오렌지"]
print(len(basket))
출력 결과 :
3
__bool__()__bool__() 은 객체가 조건문에서 참인지 거지인지 판단할 때 호출된다.
class Basket:
def __init__(self, items):
self.items = items
def __bool__(self):
return len(self.items) > 0
empty_basket = Basket([])
fruit_basket = Basket(["사괴"])
print(bool(empty_basket))
print(bool(fruit_basket))
출력 결과 :
False
True
연산자 오버로딩은 +, ==, < 같은 연산자를 사용자 정의 객체에 맞게 동작하도록 만드는 것이다.
__add__()__add__() 는 obj1 + obj2 가 실행될 때 호출된다.
class Basket:
def __init__(self, items):
self.items = items
def __add__(self, other):
return Basket(self.items + other.items)
def __repr__(self):
print f"Basket({self.items})"
basket1 = Basket(["사과", "바나나"])
basket2 = Basket(["오렌지"])
result = basket1 + basket2
print(result)
출력 결과 :
Basket(['사과', '바나나', '오렌지'])
__eq__()__eq__()는 obj1 == obj2 가 실행될 때 호출된다.
class Fruit:
def __init__(self, name, quantity):
self.name = name
self.quantity = quantity
def __eq__(self, other):
return self.name == other.name and self.quantity == other.quantity
apple1 = Fruit("사과", 10)
apple2 = Fruit("사과", 10)
apple3 = Fruit("사과", 5)
print(apple1==apple2)
print(apple2==apple3)
출력 결과 :
True
False
비교 대상이 같은 타입인지 확인하려면 다음처럼 방어 코드를 넣을 수 있다.
class Fruit:
def __init__(self, name, quantity):
self.name = name
self.quantity = quantity
def __eq__(self, other):
if not isinstance(other, Fruit):
return NotImplemented
else:
return self.name==other.name and self.quantity==other.quantity
apple = Fruit("사과", 10)
print(apple == "사과")
출력 결과 :
False
NotImplemented 는 "이 비교는 내가 처리할 수 없다."는 의미이다. 파이썬은 반대쪽 객체의 비교 메서드를 시도하거나 적잘한 기본 결과를 사용한다.
with 문과 컨텍스트 매니저with 문은 파일, 네티워크 연결, DB 연결처럼 사용 후 반드시 정리해야 하는 자원을 안전하게 관리하기 위한 문법이다.
| 메서드 | 호출 시점 |
|---|---|
| enter() | with 블록에 진입할 때 |
| exit() | with 블록이 끝날 때 |
class MyResource:
def __init__(self):
print("자원 열기")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("자원 정리")
with MyResource() as resurce:
print("자원 사용 중")
출력 결과 :
자원 열기
자원 사용 중
자원 정리
__enter__() 에서 반환한 값은 as 뒤의 변수에 들어간다. __exit__() 은 예외 발생 여부와 관계없이 항상 호출된다.
__exit__() 은 실행된다.class MyResource:
def __enter__(self):
print("자원 열기")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("자원 정리")
print("예외 타입", exc_type)
try:
with MyResource():
print("작업 중")
1 / 0
except ZeroDivisionError:
print("바깥에서 예외 처리")
출력 결과 :
자원 열기
작업 중
자원 정리
예외 타입 : <class 'ZeroDivisionError'>
바깥에서 예외 처리
__exit__()은 예외 정보를 인자로 받는다.
| 매개변수 | 의미 |
|---|---|
| exc_type | 발생한 예외 타입 |
| exc_value | 발생한 예외 객체 |
| traceback | 예외 발생 위치 정보 |
__exit__() 의 반환값__exit__() 이 True 를 반환하면 예외가 바깥으로 전파되지 않는다. 이를 예외 억제라고 한다.
class IgnoreError:
def __enter__(self):
print("시작")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("종료")
return True
with IgnoreError():
print("작업 중")
1 / 0
print("프로그램 계속 실행")
출력 결과 :
시작
작업 중
종료
프로그램 계속 실행
예외를 무조건 억제하면 문제를 숨길 수 있으므로, 명확한 의도가 있을때만 True 를 반환해야한다.
매직 메서드는 파이썬의 문법과 사용자 정의 객체를 연결하는 핵심 기능이다.
| 목적 | 매직 메서드 |
|---|---|
| 객체 문자열 표현 | __str__(), __repr__() |
| 반복 처리 | __iter__(), __next__() |
| 인덱싱 | __getitem__(), __setitem__(), __delitem__() |
| 함수처럼 호출 | __call__() |
| 길이와 참/거짓 | __len__(), __bool__() |
| 연산자 | __add__(), __eq__() |
| 컨텍스트 관리 | __enter__(), __exit__() |
매직 메서드를 잘 활용하면 사용자 정의 객체도 파이썬의 기본 자료형처럼 자연스럽게 사용할 수 있다. 다만 모든 기능을 매직 메서드로 해결하려 하기보다, 파이썬 문법과 자연스럽게 연결될 때 사용하는 것이 좋다.
본 정리는 강의 내용과 아래 자료를 참고하여 학습 목적으로 작성하였습니다.