본문으로 건너뛰기
Advertisement

매직 메서드 (Dunder Methods)

매직 메서드(Magic Methods) 또는 **던더 메서드(Dunder Methods)**는 이름 앞뒤에 이중 밑줄(__)이 붙은 특별한 메서드입니다. Python 인터프리터가 특정 연산을 수행할 때 자동으로 호출합니다. 이를 통해 사용자 정의 클래스가 Python의 내장 타입처럼 동작하게 만들 수 있습니다.


매직 메서드 개요

# 매직 메서드가 호출되는 순간들
x = [1, 2, 3]
len(x) # x.__len__() 호출
str(x) # x.__str__() 호출
x[0] # x.__getitem__(0) 호출
x[0] = 10 # x.__setitem__(0, 10) 호출
1 in x # x.__contains__(1) 호출
x + [4, 5] # x.__add__([4, 5]) 호출
x == [1, 2, 3] # x.__eq__([1, 2, 3]) 호출

__str__ vs __repr__

두 메서드 모두 객체를 문자열로 표현하지만 목적이 다릅니다.

  • __str__: 사용자를 위한 가독성 있는 표현 (print, str() 사용)
  • __repr__: 개발자를 위한 명확한 표현 (repr(), REPL에서 출력)
from datetime import datetime


class Product:
def __init__(self, name: str, price: float, quantity: int):
self.name = name
self.price = price
self.quantity = quantity
self.created_at = datetime.now()

def __str__(self) -> str:
"""사용자 친화적 표현"""
return f"{self.name}{self.price:,.0f}원 (재고: {self.quantity}개)"

def __repr__(self) -> str:
"""개발자를 위한 명확한 표현 — eval()로 재생성 가능하면 이상적"""
return (f"Product(name={self.name!r}, price={self.price}, "
f"quantity={self.quantity})")


p = Product("Python 책", 35000, 50)
print(str(p)) # Python 책 — 35,000원 (재고: 50개)
print(repr(p)) # Product(name='Python 책', price=35000, quantity=50)
print(p) # Python 책 — 35,000원 (재고: 50개) (print는 __str__ 사용)

# __str__이 없으면 __repr__ 사용
class OnlyRepr:
def __repr__(self):
return "OnlyRepr()"

o = OnlyRepr()
print(o) # OnlyRepr() (__str__이 없으면 __repr__ 대신 사용)
print(repr(o)) # OnlyRepr()

__len__: len() 지원

class Playlist:
def __init__(self, name: str):
self.name = name
self._songs: list[str] = []

def add(self, song: str) -> None:
self._songs.append(song)

def __len__(self) -> int:
return len(self._songs)

def __bool__(self) -> bool:
"""bool() 또는 if playlist: 에서 사용"""
return len(self._songs) > 0

def __str__(self) -> str:
return f"Playlist({self.name!r}, {len(self)} songs)"


playlist = Playlist("내 플레이리스트")
print(len(playlist)) # 0
print(bool(playlist)) # False

playlist.add("Bohemian Rhapsody")
playlist.add("Hotel California")
print(len(playlist)) # 2
print(bool(playlist)) # True

if playlist:
print(f"{playlist}이(가) 준비되었습니다!")

비교 연산자 오버로딩

from functools import total_ordering


@total_ordering # __eq__와 하나의 비교 메서드만 정의하면 나머지 자동 생성
class Version:
"""소프트웨어 버전 비교 클래스"""

def __init__(self, major: int, minor: int, patch: int = 0):
self.major = major
self.minor = minor
self.patch = patch

def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)

def __lt__(self, other: "Version") -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)

# @total_ordering이 아래를 자동 생성:
# __le__, __gt__, __ge__

def __str__(self) -> str:
return f"v{self.major}.{self.minor}.{self.patch}"

def __repr__(self) -> str:
return f"Version({self.major}, {self.minor}, {self.patch})"

def __hash__(self) -> int:
"""__eq__를 정의하면 __hash__도 정의해야 집합/딕셔너리에서 사용 가능"""
return hash((self.major, self.minor, self.patch))


v1 = Version(1, 0, 0)
v2 = Version(1, 2, 3)
v3 = Version(2, 0, 0)
v4 = Version(1, 0, 0)

print(v1 == v4) # True
print(v1 < v2) # True
print(v2 > v1) # True (total_ordering 자동 생성)
print(v3 >= v2) # True (total_ordering 자동 생성)

versions = [v3, v1, v2]
print(sorted(versions)) # [v1.0.0, v1.2.3, v2.0.0]

# 집합에서 사용 (hashable)
version_set = {v1, v2, v4}
print(version_set) # {v1.0.0, v1.2.3} (v1 == v4이므로 중복 제거)

산술 연산자 오버로딩

class Vector2D:
"""2차원 벡터 클래스"""

def __init__(self, x: float, y: float):
self.x = x
self.y = y

def __add__(self, other: "Vector2D") -> "Vector2D":
"""v1 + v2"""
return Vector2D(self.x + other.x, self.y + other.y)

def __sub__(self, other: "Vector2D") -> "Vector2D":
"""v1 - v2"""
return Vector2D(self.x - other.x, self.y - other.y)

def __mul__(self, scalar: float) -> "Vector2D":
"""v * scalar (벡터 × 스칼라)"""
return Vector2D(self.x * scalar, self.y * scalar)

def __rmul__(self, scalar: float) -> "Vector2D":
"""scalar * v (스칼라 × 벡터 — 교환 법칙)"""
return self.__mul__(scalar)

def __truediv__(self, scalar: float) -> "Vector2D":
"""v / scalar"""
if scalar == 0:
raise ZeroDivisionError("0으로 나눌 수 없습니다.")
return Vector2D(self.x / scalar, self.y / scalar)

def __neg__(self) -> "Vector2D":
"""-v (단항 음수)"""
return Vector2D(-self.x, -self.y)

def __abs__(self) -> float:
"""abs(v) — 벡터의 크기(magnitude)"""
return (self.x ** 2 + self.y ** 2) ** 0.5

def __iadd__(self, other: "Vector2D") -> "Vector2D":
"""v += other (in-place 덧셈)"""
self.x += other.x
self.y += other.y
return self

def dot(self, other: "Vector2D") -> float:
"""내적"""
return self.x * other.x + self.y * other.y

def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector2D):
return NotImplemented
return self.x == other.x and self.y == other.y

def __repr__(self) -> str:
return f"Vector2D({self.x}, {self.y})"

def __str__(self) -> str:
return f"({self.x}, {self.y})"


v1 = Vector2D(1, 2)
v2 = Vector2D(3, 4)

print(v1 + v2) # (4, 6)
print(v2 - v1) # (2, 2)
print(v1 * 3) # (3, 6)
print(3 * v1) # (3, 6) — __rmul__ 호출
print(v2 / 2) # (1.5, 2.0)
print(-v1) # (-1, -2)
print(abs(v2)) # 5.0
print(v1.dot(v2)) # 11

v1 += Vector2D(10, 10)
print(v1) # (11, 12)

__contains__: in 연산자

class IPNetwork:
"""IP 네트워크 범위 클래스"""

def __init__(self, start: str, end: str):
self.start = self._ip_to_int(start)
self.end = self._ip_to_int(end)

@staticmethod
def _ip_to_int(ip: str) -> int:
parts = list(map(int, ip.split(".")))
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]

def __contains__(self, ip: str) -> bool:
"""ip in network"""
ip_int = self._ip_to_int(ip)
return self.start <= ip_int <= self.end

def __len__(self) -> int:
return self.end - self.start + 1


local_net = IPNetwork("192.168.1.1", "192.168.1.254")
print("192.168.1.100" in local_net) # True
print("192.168.1.255" in local_net) # False
print("10.0.0.1" in local_net) # False
print(len(local_net)) # 254

__getitem__, __setitem__, __delitem__: 인덱싱 지원

class Matrix:
"""2D 행렬 클래스"""

def __init__(self, rows: int, cols: int, default: float = 0.0):
self.rows = rows
self.cols = cols
self._data = [[default] * cols for _ in range(rows)]

def __getitem__(self, key: tuple[int, int]) -> float:
"""matrix[row, col]"""
row, col = key
self._check_bounds(row, col)
return self._data[row][col]

def __setitem__(self, key: tuple[int, int], value: float) -> None:
"""matrix[row, col] = value"""
row, col = key
self._check_bounds(row, col)
self._data[row][col] = value

def __delitem__(self, key: tuple[int, int]) -> None:
"""del matrix[row, col] — 0으로 초기화"""
row, col = key
self._check_bounds(row, col)
self._data[row][col] = 0.0

def _check_bounds(self, row: int, col: int) -> None:
if not (0 <= row < self.rows and 0 <= col < self.cols):
raise IndexError(f"인덱스 범위 초과: ({row}, {col})")

def __len__(self) -> int:
return self.rows * self.cols

def __repr__(self) -> str:
rows_str = "\n ".join(str(row) for row in self._data)
return f"Matrix(\n {rows_str}\n)"


m = Matrix(3, 3)
m[0, 0] = 1
m[1, 1] = 5
m[2, 2] = 9

print(m[1, 1]) # 5
print(len(m)) # 9
del m[1, 1]
print(m[1, 1]) # 0.0
print(m)

실전 예제: 카드 덱 구현

from __future__ import annotations
import random
from functools import total_ordering


@total_ordering
class Card:
SUITS = ["♠", "♥", "♦", "♣"]
RANKS = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
RANK_VALUES = {r: i for i, r in enumerate(RANKS, 2)}

def __init__(self, rank: str, suit: str):
if rank not in self.RANKS:
raise ValueError(f"유효하지 않은 랭크: {rank}")
if suit not in self.SUITS:
raise ValueError(f"유효하지 않은 슈트: {suit}")
self.rank = rank
self.suit = suit

def __str__(self) -> str:
return f"{self.suit}{self.rank}"

def __repr__(self) -> str:
return f"Card({self.rank!r}, {self.suit!r})"

def __eq__(self, other: object) -> bool:
if not isinstance(other, Card):
return NotImplemented
return self.RANK_VALUES[self.rank] == self.RANK_VALUES[other.rank]

def __lt__(self, other: Card) -> bool:
return self.RANK_VALUES[self.rank] < self.RANK_VALUES[other.rank]

def __hash__(self) -> int:
return hash((self.rank, self.suit))


class Deck:
def __init__(self):
self._cards = [Card(r, s) for s in Card.SUITS for r in Card.RANKS]

def __len__(self) -> int:
return len(self._cards)

def __getitem__(self, index: int | slice) -> Card | list[Card]:
return self._cards[index]

def __contains__(self, card: Card) -> bool:
return card in self._cards

def __iter__(self):
return iter(self._cards)

def __repr__(self) -> str:
return f"Deck({len(self)} cards)"

def shuffle(self) -> None:
random.shuffle(self._cards)

def draw(self) -> Card:
if not self._cards:
raise IndexError("덱이 비었습니다.")
return self._cards.pop()

def deal(self, num_hands: int, cards_per_hand: int) -> list[list[Card]]:
hands = [[] for _ in range(num_hands)]
for _ in range(cards_per_hand):
for hand in hands:
hand.append(self.draw())
return hands


deck = Deck()
print(f"카드 수: {len(deck)}")
print(f"처음 5장: {deck[:5]}")

deck.shuffle()
hands = deck.deal(4, 5)
for i, hand in enumerate(hands, 1):
sorted_hand = sorted(hand)
print(f"플레이어 {i}: {' '.join(str(c) for c in sorted_hand)}")

special_card = Card("A", "♠")
print(f"♠A가 덱에 있나요? {special_card in deck}")

컨텍스트 매니저: __enter__ / __exit__

class DatabaseConnection:
"""with 문을 지원하는 DB 연결 클래스"""

def __init__(self, host: str, database: str):
self.host = host
self.database = database
self._connection = None

def __enter__(self) -> "DatabaseConnection":
"""with 블록 진입 시 호출"""
print(f"{self.host}/{self.database}에 연결 중...")
self._connection = f"conn:{self.host}/{self.database}"
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
"""with 블록 종료 시 호출 (예외 발생 여부와 무관)"""
print(f"연결 종료: {self._connection}")
self._connection = None
if exc_type is not None:
print(f"예외 처리: {exc_type.__name__}: {exc_val}")
return False # True 반환 시 예외 억제

def query(self, sql: str) -> str:
if not self._connection:
raise RuntimeError("연결이 없습니다.")
return f"[{self._connection}] 실행: {sql}"


with DatabaseConnection("localhost", "mydb") as db:
result = db.query("SELECT * FROM users")
print(result)
# 블록 종료 시 자동으로 __exit__ 호출 → 연결 종료

@functools.total_ordering으로 비교 최소화

from functools import total_ordering


@total_ordering
class Money:
def __init__(self, amount: float, currency: str = "KRW"):
self.amount = amount
self.currency = currency

def __eq__(self, other: object) -> bool:
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError("다른 통화 비교 불가")
return self.amount == other.amount

def __lt__(self, other: "Money") -> bool:
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError("다른 통화 비교 불가")
return self.amount < other.amount

# total_ordering이 자동으로 생성: __le__, __gt__, __ge__

def __add__(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("다른 통화 덧셈 불가")
return Money(self.amount + other.amount, self.currency)

def __str__(self) -> str:
return f"{self.amount:,.0f} {self.currency}"


m1 = Money(1000)
m2 = Money(2000)
m3 = Money(1000)

print(m1 == m3) # True
print(m1 < m2) # True
print(m2 > m1) # True (자동 생성)
print(m1 <= m3) # True (자동 생성)
print(m1 + m2) # 3,000 KRW

매직 메서드 요약표

메서드호출 시점예시
__init__객체 생성 시obj = MyClass()
__str__str(), print()str(obj)
__repr__repr(), REPLrepr(obj)
__len__len()len(obj)
__bool__bool(), ifbool(obj)
__eq__==obj == other
__lt__<obj < other
__add__+obj + other
__mul__*obj * scalar
__contains__initem in obj
__getitem__[] 읽기obj[key]
__setitem__[] 쓰기obj[key] = val
__iter__for, iter()for x in obj:
__enter__with 진입with obj as x:
__exit__with 종료with 블록 끝
__call__() 호출obj()
__hash__hash(), set, dicthash(obj)

고수 팁

__call__로 인스턴스를 함수처럼 사용

class Multiplier:
def __init__(self, factor: float):
self.factor = factor

def __call__(self, value: float) -> float:
return value * self.factor


double = Multiplier(2)
triple = Multiplier(3)

print(double(5)) # 10.0
print(triple(5)) # 15.0

# 함수를 받는 곳에 인스턴스를 전달
numbers = [1, 2, 3, 4, 5]
print(list(map(double, numbers))) # [2.0, 4.0, 6.0, 8.0, 10.0]
Advertisement