포스트

[Python 100일 챌린지] Day 39 - 특수 메서드 (__str__, __repr__ 등)

[Python 100일 챌린지] Day 39 - 특수 메서드 (__str__, __repr__ 등)

print(my_object)를 했을 때 무엇이 출력될까요? my_list[0]처럼 대괄호로 접근하려면 어떻게 해야 할까요? 🤔 ──Python의 특수 메서드(Magic Methods)로 모든 게 가능합니다! 😊

우리가 쓰는 len(), str(), [], + 같은 연산들, 사실 모두 뒤에서 특수 메서드가 작동하는 거였습니다! len(my_list) → 내부에서 my_list.__len__()을 호출 str(my_obj) → 내부에서 my_obj.__str__()을 호출

파일을 열고 닫는 with 문도, 리스트 순회하는 for 루프도, 객체끼리 비교하는 ==, < 연산도, 모두 특수 메서드 덕분입니다!

오늘은 Python의 마법 같은 특수 메서드를 배워서 진짜 Pythonic한 클래스를 만듭니다! 💡

🎯 오늘의 학습 목표

⭐⭐⭐⭐ (45-55분 완독)

📚 사전 지식


🎯 학습 목표 1: 특수 메서드의 개념과 활용 이해하기

특수 메서드의 개념

특수 메서드(Special Methods)는 Python이 내부적으로 호출하는 메서드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Point({self.x}, {self.y})"

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

# 특수 메서드는 Python이 자동으로 호출
p1 = Point(1, 2)      # __init__ 호출
print(p1)             # __str__ 호출
p2 = Point(3, 4)
p3 = p1 + p2          # __add__ 호출
print(p3)             # Point(4, 6)

왜 특수 메서드를 사용하나?

장점:

  1. Python스러운 코드: 내장 타입처럼 동작하는 클래스
  2. 직관적인 인터페이스: +, == 등 연산자 사용 가능
  3. 프로토콜 지원: 이터레이터, 컨텍스트 매니저 등 표준 프로토콜
  4. 성능 최적화: Python 인터프리터가 직접 호출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 특수 메서드 없이
class NumberWithoutSpecial:
    def __init__(self, value):
        self.value = value

    def add(self, other):
        return NumberWithoutSpecial(self.value + other.value)

n1 = NumberWithoutSpecial(10)
n2 = NumberWithoutSpecial(20)
n3 = n1.add(n2)  # 불편함

# 특수 메서드 사용
class Number:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        return Number(self.value + other.value)

    def __str__(self):
        return str(self.value)

n1 = Number(10)
n2 = Number(20)
n3 = n1 + n2     # Python스러움!
print(n3)        # 30

🎯 학습 목표 2: 문자열 표현 메서드 구현하기

__str__ vs __repr__

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year

    def __str__(self):
        """사용자 친화적인 문자열 (읽기 쉬움)"""
        return f"'{self.title}' by {self.author}"

    def __repr__(self):
        """개발자 친화적인 문자열 (재생성 가능)"""
        return f"Book(title='{self.title}', author='{self.author}', year={self.year})"

book = Book("1984", "George Orwell", 1949)

# __str__은 print()에서 사용
print(book)           # '1984' by George Orwell
print(str(book))      # '1984' by George Orwell

# __repr__은 인터프리터에서 사용
print(repr(book))     # Book(title='1984', author='George Orwell', year=1949)
book                  # 대화형 인터프리터: Book(title='1984', author='George Orwell', year=1949)

규칙:

  • __str__: 최종 사용자가 읽기 쉽게
  • __repr__: 개발자가 디버깅하기 쉽게 (가능하면 재생성 가능한 형태)

실전 예제: 사용자 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from datetime import datetime

class User:
    def __init__(self, username, email, joined_at=None):
        self.username = username
        self.email = email
        self.joined_at = joined_at or datetime.now()

    def __str__(self):
        """일반 사용자를 위한 표현"""
        return f"{self.username} ({self.email})"

    def __repr__(self):
        """개발자를 위한 표현 (재생성 가능)"""
        return (f"User(username='{self.username}', "
                f"email='{self.email}', "
                f"joined_at=datetime({self.joined_at.year}, "
                f"{self.joined_at.month}, {self.joined_at.day}))")

# 사용 예제
user = User("alice", "[email protected]")

print(user)           # alice ([email protected])
print(repr(user))     # User(username='alice', email='[email protected]', joined_at=datetime(2025, 4, 8))

# 로깅 시나리오
users = [
    User("alice", "[email protected]"),
    User("bob", "[email protected]")
]

print("사용자 목록:")
for u in users:
    print(f"  - {u}")  # __str__ 사용

print("\n디버그 정보:")
print(users)          # __repr__ 사용 (리스트는 요소의 repr 사용)

__format__ 커스텀 포맷팅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Duration:
    """시간 길이를 표현하는 클래스"""
    def __init__(self, seconds):
        self.seconds = seconds

    def __str__(self):
        return f"{self.seconds} seconds"

    def __format__(self, format_spec):
        """커스텀 포맷팅 지원"""
        if format_spec == 'hms':
            # 시:분:초 형식
            hours = self.seconds // 3600
            minutes = (self.seconds % 3600) // 60
            secs = self.seconds % 60
            return f"{hours:02d}:{minutes:02d}:{secs:02d}"
        elif format_spec == 'verbose':
            # 상세 형식
            hours = self.seconds // 3600
            minutes = (self.seconds % 3600) // 60
            secs = self.seconds % 60
            return f"{hours}시간 {minutes}{secs}"
        else:
            # 기본 형식
            return str(self.seconds)

duration = Duration(3725)  # 1시간 2분 5초

print(duration)              # 3725 seconds
print(f"{duration:hms}")     # 01:02:05
print(f"{duration:verbose}") # 1시간 2분 5초

🎯 학습 목표 3: 호출 가능 객체와 컨텍스트 매니저 만들기

__call__ 메서드

__call__ 메서드를 정의하면 객체를 함수처럼 호출할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Multiplier:
    """수를 곱하는 호출 가능 객체"""
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        """객체를 함수처럼 호출"""
        return x * self.factor

# 사용 예제
double = Multiplier(2)
triple = Multiplier(3)

print(double(5))    # 10 (함수처럼 호출!)
print(triple(5))    # 15

# callable() 함수로 확인
print(callable(double))  # True

실전 예제: 로거 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from datetime import datetime

class Logger:
    """호출 가능한 로거 객체"""
    def __init__(self, prefix="[LOG]"):
        self.prefix = prefix
        self.logs = []

    def __call__(self, message):
        """로그 메시지 기록"""
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        log_entry = f"{self.prefix} [{timestamp}] {message}"
        self.logs.append(log_entry)
        print(log_entry)

    def get_logs(self):
        return self.logs

# 사용 예제
logger = Logger("[INFO]")

logger("애플리케이션 시작")          # 함수처럼 호출!
logger("데이터베이스 연결 성공")
logger("사용자 로그인: alice")

print("\n전체 로그:")
for log in logger.get_logs():
    print(log)

실전 예제: 캐싱 데코레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Cached:
    """함수 결과를 캐싱하는 호출 가능 객체"""
    def __init__(self, func):
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        """캐시를 확인하고 함수 호출"""
        if args in self.cache:
            print(f"💾 캐시 사용: {args}")
            return self.cache[args]

        print(f"🔄 계산 중: {args}")
        result = self.func(*args)
        self.cache[args] = result
        return result

@Cached
def fibonacci(n):
    """피보나치 수열"""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(5))
print(fibonacci(5))  # 캐시에서 가져옴
print(fibonacci(6))  # 일부는 캐시 사용

컨텍스트 매니저 (__enter__, __exit__)

컨텍스트 매니저with 문과 함께 사용되는 객체입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class FileManager:
    """파일 관리 컨텍스트 매니저"""
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        """with 블록 진입 시 호출"""
        print(f"📂 파일 열기: {self.filename}")
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        """with 블록 종료 시 호출"""
        if self.file:
            self.file.close()
            print(f"📁 파일 닫기: {self.filename}")

        # 예외 처리
        if exc_type is not None:
            print(f"❌ 오류 발생: {exc_type.__name__}: {exc_val}")

        # False 반환 시 예외를 다시 발생시킴
        return False

# 사용 예제
with FileManager("test.txt", "w") as f:
    f.write("Hello, Context Manager!")
    # 파일이 자동으로 닫힘

실전 예제: 데이터베이스 연결 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import time

class DatabaseConnection:
    """데이터베이스 연결 컨텍스트 매니저"""
    def __init__(self, host, database):
        self.host = host
        self.database = database
        self.connection = None

    def __enter__(self):
        """연결 시작"""
        print(f"🔌 DB 연결 중: {self.host}/{self.database}")
        time.sleep(0.1)  # 연결 시뮬레이션
        self.connection = f"Connection<{self.host}/{self.database}>"
        print("✅ 연결 성공")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """연결 종료"""
        print("🔌 DB 연결 해제 중...")
        time.sleep(0.1)  # 연결 해제 시뮬레이션
        self.connection = None
        print("✅ 연결 해제 완료")
        return False

    def execute(self, query):
        """쿼리 실행"""
        if not self.connection:
            raise RuntimeError("연결되지 않음")
        print(f"📝 쿼리 실행: {query}")
        return f"Result of '{query}'"

# 사용 예제
with DatabaseConnection("localhost", "mydb") as db:
    result = db.execute("SELECT * FROM users")
    print(f"결과: {result}")
# 자동으로 연결 해제됨

실전 예제: 타이머 컨텍스트 매니저

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import time

class Timer:
    """코드 실행 시간 측정 컨텍스트 매니저"""
    def __init__(self, name="코드 블록"):
        self.name = name
        self.start_time = None
        self.elapsed = None

    def __enter__(self):
        """타이머 시작"""
        print(f"⏱️  {self.name} 시작...")
        self.start_time = time.time()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """타이머 종료"""
        self.elapsed = time.time() - self.start_time
        print(f"⏱️  {self.name} 완료: {self.elapsed:.4f}")
        return False

# 사용 예제
with Timer("데이터 처리"):
    time.sleep(0.5)
    data = [i ** 2 for i in range(1000000)]

with Timer("파일 작업"):
    with open("test.txt", "w") as f:
        for i in range(10000):
            f.write(f"Line {i}\n")

🎯 학습 목표 4: 이터레이터와 컨테이너 프로토콜 구현하기

__iter____next__

이터레이터 프로토콜을 구현하면 객체를 for 루프에서 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Countdown:
    """카운트다운 이터레이터"""
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        """이터레이터 객체 반환 (자기 자신)"""
        return self

    def __next__(self):
        """다음 값 반환"""
        if self.current <= 0:
            raise StopIteration

        self.current -= 1
        return self.current + 1

# 사용 예제
for num in Countdown(5):
    print(num)  # 5, 4, 3, 2, 1

실전 예제: 파일 청크 읽기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class FileChunks:
    """파일을 청크 단위로 읽는 이터레이터"""
    def __init__(self, filename, chunk_size=1024):
        self.filename = filename
        self.chunk_size = chunk_size
        self.file = None

    def __iter__(self):
        """이터레이션 시작"""
        self.file = open(self.filename, 'rb')
        return self

    def __next__(self):
        """다음 청크 읽기"""
        if self.file is None:
            raise StopIteration

        chunk = self.file.read(self.chunk_size)

        if not chunk:
            self.file.close()
            raise StopIteration

        return chunk

# 사용 예제 (대용량 파일 처리)
# for chunk in FileChunks("large_file.bin", chunk_size=4096):
#     process_chunk(chunk)

실전 예제: 무한 시퀀스 생성기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FibonacciIterator:
    """피보나치 수열 무한 이터레이터"""
    def __init__(self, max_value=None):
        self.a = 0
        self.b = 1
        self.max_value = max_value

    def __iter__(self):
        return self

    def __next__(self):
        if self.max_value is not None and self.a > self.max_value:
            raise StopIteration

        current = self.a
        self.a, self.b = self.b, self.a + self.b
        return current

# 사용 예제
print("100 이하 피보나치 수열:")
for num in FibonacciIterator(max_value=100):
    print(num, end=" ")
print()

# 처음 10개만
from itertools import islice
print("\n처음 10개:")
for num in islice(FibonacciIterator(), 10):
    print(num, end=" ")

컨테이너 프로토콜

컨테이너처럼 동작하는 클래스를 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Playlist:
    """음악 재생목록 컨테이너"""
    def __init__(self):
        self.songs = []

    def __len__(self):
        """len() 함수 지원"""
        return len(self.songs)

    def __getitem__(self, index):
        """인덱싱 지원 (playlist[0])"""
        return self.songs[index]

    def __setitem__(self, index, value):
        """인덱스 할당 지원 (playlist[0] = song)"""
        self.songs[index] = value

    def __delitem__(self, index):
        """del 지원 (del playlist[0])"""
        del self.songs[index]

    def __contains__(self, song):
        """in 연산자 지원 (song in playlist)"""
        return song in self.songs

    def add(self, song):
        """노래 추가"""
        self.songs.append(song)

# 사용 예제
playlist = Playlist()
playlist.add("Song A")
playlist.add("Song B")
playlist.add("Song C")

print(f"재생목록 크기: {len(playlist)}")  # 3
print(f"첫 번째 노래: {playlist[0]}")     # Song A
print("Song B" in playlist)               # True

# 슬라이싱 지원을 위한 개선
playlist[1] = "New Song"
print(playlist[1])  # New Song

실전 예제: 커스텀 딕셔너리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class CaseInsensitiveDict:
    """대소문자 구분 없는 딕셔너리"""
    def __init__(self):
        self._data = {}

    def __setitem__(self, key, value):
        """키를 소문자로 저장"""
        self._data[key.lower()] = value

    def __getitem__(self, key):
        """키를 소문자로 조회"""
        return self._data[key.lower()]

    def __delitem__(self, key):
        """키를 소문자로 삭제"""
        del self._data[key.lower()]

    def __contains__(self, key):
        """in 연산자"""
        return key.lower() in self._data

    def __len__(self):
        """딕셔너리 크기"""
        return len(self._data)

    def __repr__(self):
        return f"CaseInsensitiveDict({self._data})"

# 사용 예제
config = CaseInsensitiveDict()
config["API_KEY"] = "secret123"

print(config["api_key"])      # secret123 (소문자로 접근)
print(config["Api_Key"])      # secret123 (대소문자 혼합)
print("API_KEY" in config)    # True
print(len(config))            # 1

실전 예제: 순환 버퍼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class CircularBuffer:
    """고정 크기 순환 버퍼"""
    def __init__(self, size):
        self.size = size
        self.data = [None] * size
        self.index = 0
        self.count = 0

    def __len__(self):
        """현재 저장된 항목 수"""
        return min(self.count, self.size)

    def __getitem__(self, index):
        """인덱스로 항목 조회"""
        if index < 0 or index >= len(self):
            raise IndexError("인덱스 범위 초과")
        actual_index = (self.index - len(self) + index) % self.size
        return self.data[actual_index]

    def __iter__(self):
        """이터레이션 지원"""
        for i in range(len(self)):
            yield self[i]

    def append(self, item):
        """항목 추가 (오래된 항목 덮어쓰기)"""
        self.data[self.index] = item
        self.index = (self.index + 1) % self.size
        self.count += 1

    def __repr__(self):
        items = list(self)
        return f"CircularBuffer({items})"

# 사용 예제
buffer = CircularBuffer(5)

for i in range(8):
    buffer.append(i)
    print(f"추가: {i}{buffer}")

print(f"\n최종 버퍼 크기: {len(buffer)}")
print(f"버퍼 내용: {list(buffer)}")

🎯 학습 목표 5: 실전 예제로 종합 정리하기

지금까지 배운 특수 메서드들을 종합적으로 활용한 실전 예제들입니다!

단항 연산자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Vector:
    """벡터 클래스 (단항 연산 지원)"""
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __neg__(self):
        """단항 음수 (-vector)"""
        return Vector(-self.x, -self.y)

    def __pos__(self):
        """단항 양수 (+vector)"""
        return Vector(+self.x, +self.y)

    def __abs__(self):
        """절댓값 (abs(vector))"""
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __invert__(self):
        """비트 NOT (~vector) - 여기서는 반전"""
        return Vector(self.y, self.x)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

# 사용 예제
v = Vector(3, 4)

print(f"v = {v}")
print(f"-v = {-v}")         # Vector(-3, -4)
print(f"+v = {+v}")         # Vector(3, 4)
print(f"abs(v) = {abs(v)}") # 5.0
print(f"~v = {~v}")         # Vector(4, 3)

복합 대입 연산자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Counter:
    """카운터 클래스 (복합 대입 연산)"""
    def __init__(self, value=0):
        self.value = value

    def __iadd__(self, other):
        """+= 연산자"""
        self.value += other
        return self

    def __isub__(self, other):
        """-= 연산자"""
        self.value -= other
        return self

    def __imul__(self, other):
        """*= 연산자"""
        self.value *= other
        return self

    def __repr__(self):
        return f"Counter({self.value})"

# 사용 예제
counter = Counter(10)
print(f"초기값: {counter}")

counter += 5
print(f"+= 5: {counter}")   # Counter(15)

counter -= 3
print(f"-= 3: {counter}")   # Counter(12)

counter *= 2
print(f"*= 2: {counter}")   # Counter(24)

functools.total_ordering 활용

모든 비교 연산자를 하나하나 구현하는 대신 total_ordering 데코레이터를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from functools import total_ordering

@total_ordering
class Version:
    """버전 비교 클래스"""
    def __init__(self, major, minor, patch):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __eq__(self, other):
        """== 연산자 (필수)"""
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor, self.patch) == \
               (other.major, other.minor, other.patch)

    def __lt__(self, other):
        """< 연산자 (필수)"""
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor, self.patch) < \
               (other.major, other.minor, other.patch)

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

# total_ordering이 나머지 연산자를 자동 생성
v1 = Version(1, 0, 0)
v2 = Version(1, 2, 3)
v3 = Version(2, 0, 0)

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

# 정렬 가능
versions = [v3, v1, v2]
print(sorted(versions))  # [Version(1.0.0), Version(1.2.3), Version(2.0.0)]

실전 예제: 우선순위 큐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from functools import total_ordering

@total_ordering
class Task:
    """우선순위 작업"""
    def __init__(self, name, priority):
        self.name = name
        self.priority = priority  # 낮을수록 우선순위 높음

    def __eq__(self, other):
        if not isinstance(other, Task):
            return NotImplemented
        return self.priority == other.priority

    def __lt__(self, other):
        if not isinstance(other, Task):
            return NotImplemented
        return self.priority < other.priority

    def __repr__(self):
        return f"Task('{self.name}', priority={self.priority})"

# 사용 예제
tasks = [
    Task("이메일 확인", 3),
    Task("긴급 버그 수정", 1),
    Task("문서 작성", 5),
    Task("코드 리뷰", 2)
]

# 우선순위 순으로 정렬
tasks_sorted = sorted(tasks)
print("작업 우선순위:")
for task in tasks_sorted:
    print(f"  {task.priority}. {task.name}")

스마트 쇼핑 카트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from functools import total_ordering

@total_ordering
class Product:
    """상품 클래스"""
    def __init__(self, name, price, stock=100):
        self.name = name
        self.price = price
        self.stock = stock

    def __eq__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return self.name == other.name

    def __lt__(self, other):
        """가격 기준 비교"""
        if not isinstance(other, Product):
            return NotImplemented
        return self.price < other.price

    def __hash__(self):
        """해시 가능 (딕셔너리 키로 사용)"""
        return hash(self.name)

    def __repr__(self):
        return f"Product('{self.name}', {self.price:,}원, 재고={self.stock})"

class ShoppingCart:
    """쇼핑 카트 (모든 특수 메서드 활용)"""
    def __init__(self):
        self._items = {}  # {Product: quantity}

    def __len__(self):
        """총 상품 종류 수"""
        return len(self._items)

    def __getitem__(self, product):
        """카트[상품] → 수량"""
        return self._items.get(product, 0)

    def __setitem__(self, product, quantity):
        """카트[상품] = 수량"""
        if quantity <= 0:
            if product in self._items:
                del self._items[product]
        else:
            if quantity > product.stock:
                raise ValueError(f"재고 부족: {product.name} (재고: {product.stock})")
            self._items[product] = quantity

    def __delitem__(self, product):
        """del 카트[상품]"""
        if product in self._items:
            del self._items[product]

    def __contains__(self, product):
        """상품 in 카트"""
        return product in self._items

    def __iter__(self):
        """이터레이션 지원"""
        return iter(self._items.items())

    def __add__(self, other):
        """카트 합치기"""
        if not isinstance(other, ShoppingCart):
            return NotImplemented

        new_cart = ShoppingCart()
        new_cart._items = self._items.copy()

        for product, qty in other:
            new_cart[product] = new_cart[product] + qty

        return new_cart

    def __iadd__(self, product_tuple):
        """카트 += (상품, 수량)"""
        product, quantity = product_tuple
        self[product] = self[product] + quantity
        return self

    def __call__(self):
        """카트() → 총액 계산"""
        return sum(product.price * qty for product, qty in self)

    def __str__(self):
        """사용자용 표현"""
        if not self._items:
            return "🛒 장바구니가 비어있습니다"

        lines = ["🛒 장바구니:"]
        for product, qty in sorted(self._items.items()):
            subtotal = product.price * qty
            lines.append(f"{product.name} x {qty} = {subtotal:,}")
        lines.append(f"  💰 총액: {self():,}")
        return "\n".join(lines)

    def __repr__(self):
        """개발자용 표현"""
        return f"ShoppingCart(items={len(self)}, total={self():,}원)"

# 사용 예제
laptop = Product("노트북", 1500000, stock=10)
mouse = Product("마우스", 30000, stock=50)
keyboard = Product("키보드", 80000, stock=30)

# 장바구니 생성
cart = ShoppingCart()

# 상품 추가 (여러 방법)
cart[laptop] = 1
cart[mouse] = 2
cart += (keyboard, 1)

print(cart)
print()

# 장바구니 조회
print(f"노트북 수량: {cart[laptop]}")
print(f"마우스 포함? {mouse in cart}")
print(f"총 상품 종류: {len(cart)}")
print()

# 총액 계산 (호출 가능 객체)
print(f"총 결제액: {cart():,}")
print()

# 두 번째 장바구니
cart2 = ShoppingCart()
cart2[mouse] = 1
cart2[keyboard] = 2

# 장바구니 합치기
combined = cart + cart2
print("합친 장바구니:")
print(combined)

실행 결과:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🛒 장바구니:
  • 노트북 x 1 = 1,500,000원
  • 마우스 x 2 = 60,000원
  • 키보드 x 1 = 80,000원
  💰 총액: 1,640,000원

노트북 수량: 1
마우스 포함? True
총 상품 종류: 3

총 결제액: 1,640,000원

합친 장바구니:
🛒 장바구니:
  • 노트북 x 1 = 1,500,000원
  • 마우스 x 3 = 90,000원
  • 키보드 x 3 = 240,000원
  💰 총액: 1,830,000원

로그 수집 컨텍스트 매니저

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import time
from datetime import datetime

class LogCollector:
    """로그 수집 및 분석 컨텍스트 매니저"""
    def __init__(self, name):
        self.name = name
        self.logs = []
        self.start_time = None
        self.errors = []

    def __enter__(self):
        """컨텍스트 시작"""
        self.start_time = time.time()
        self.log(f"시작: {self.name}")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """컨텍스트 종료"""
        elapsed = time.time() - self.start_time

        if exc_type is not None:
            self.log(f"오류 발생: {exc_type.__name__}: {exc_val}", level="ERROR")
            self.errors.append((exc_type, exc_val))

        self.log(f"종료: {self.name} (소요 시간: {elapsed:.2f}초)")

        # 오류를 다시 발생시킴
        return False

    def log(self, message, level="INFO"):
        """로그 기록"""
        timestamp = datetime.now().strftime("%H:%M:%S")
        log_entry = {
            'timestamp': timestamp,
            'level': level,
            'message': message
        }
        self.logs.append(log_entry)

    def __call__(self, message):
        """로거를 함수처럼 호출"""
        self.log(message)

    def __len__(self):
        """로그 개수"""
        return len(self.logs)

    def __getitem__(self, index):
        """로그 조회"""
        return self.logs[index]

    def __iter__(self):
        """로그 이터레이션"""
        return iter(self.logs)

    def print_summary(self):
        """로그 요약 출력"""
        print(f"\n📊 {self.name} 로그 요약:")
        print(f"  총 로그: {len(self)}")
        print(f"  오류: {len(self.errors)}")
        print("\n로그 내역:")
        for log in self:
            level_emoji = "🔴" if log['level'] == "ERROR" else "🟢"
            print(f"  {level_emoji} [{log['timestamp']}] {log['message']}")

# 사용 예제
with LogCollector("데이터 처리") as logger:
    logger("데이터 로드 시작")
    time.sleep(0.1)

    logger("데이터 검증 중")
    time.sleep(0.1)

    logger("데이터 처리 완료")

logger.print_summary()

📝 특수 메서드 치트시트

생성 및 소멸

  • __init__(self, ...): 생성자
  • __del__(self): 소멸자

문자열 표현

  • __str__(self): str(obj), print(obj)
  • __repr__(self): repr(obj), 인터프리터 출력
  • __format__(self, spec): f"{obj:spec}"

호출 및 컨텍스트

  • __call__(self, ...): obj()
  • __enter__(self): with obj: 시작
  • __exit__(self, ...): with obj: 종료

이터레이션

  • __iter__(self): iter(obj)
  • __next__(self): next(obj)

컨테이너

  • __len__(self): len(obj)
  • __getitem__(self, key): obj[key]
  • __setitem__(self, key, value): obj[key] = value
  • __delitem__(self, key): del obj[key]
  • __contains__(self, item): item in obj

산술 연산

  • __add__(self, other): obj + other
  • __sub__(self, other): obj - other
  • __mul__(self, other): obj * other
  • __truediv__(self, other): obj / other
  • __floordiv__(self, other): obj // other
  • __mod__(self, other): obj % other
  • __pow__(self, other): obj ** other

비교 연산

  • __eq__(self, other): obj == other
  • __ne__(self, other): obj != other
  • __lt__(self, other): obj < other
  • __le__(self, other): obj <= other
  • __gt__(self, other): obj > other
  • __ge__(self, other): obj >= other

단항 연산

  • __neg__(self): -obj
  • __pos__(self): +obj
  • __abs__(self): abs(obj)
  • __invert__(self): ~obj

복합 대입

  • __iadd__(self, other): obj += other
  • __isub__(self, other): obj -= other
  • __imul__(self, other): obj *= other

해시 및 불린

  • __hash__(self): hash(obj)
  • __bool__(self): bool(obj), if obj:

💡 오늘의 핵심 요약

  1. 특수 메서드: Python이 내부적으로 호출하는 메서드 (__xxx__)

  2. 문자열 표현:
    • __str__: 사용자용 (읽기 쉽게)
    • __repr__: 개발자용 (재생성 가능하게)
  3. 호출 가능 객체: __call__로 객체를 함수처럼 사용

  4. 컨텍스트 매니저: __enter__, __exit__with 문 지원

  5. 이터레이터: __iter__, __next__for 루프 지원

  6. 컨테이너: __len__, __getitem__ 등으로 시퀀스/매핑 동작

  7. 연산자 오버로딩: 산술, 비교 연산자 커스터마이징

Python스러운 코드: 특수 메서드를 활용하면 내장 타입처럼 동작하는 직관적인 클래스를 만들 수 있습니다!


🎯 연습 문제

문제 1: 스마트 온도 클래스

섭씨와 화씨를 자동 변환하고 비교 가능한 Temperature 클래스를 작성하세요.

요구사항:

  • 섭씨 온도로 초기화
  • __str__로 “25.0°C (77.0°F)” 형식 출력
  • __repr__로 재생성 가능한 형식
  • __format__으로 ‘C’ 또는 ‘F’ 포맷 지원
  • 비교 연산자 (<, >, == 등) 지원
  • 산술 연산 (+, -) 지원
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from functools import total_ordering

@total_ordering
class Temperature:
    """스마트 온도 클래스"""
    def __init__(self, celsius):
        self.celsius = celsius

    @property
    def fahrenheit(self):
        """화씨 온도"""
        return self.celsius * 9/5 + 32

    def __str__(self):
        """사용자용 표현"""
        return f"{self.celsius:.1f}°C ({self.fahrenheit:.1f}°F)"

    def __repr__(self):
        """개발자용 표현"""
        return f"Temperature({self.celsius})"

    def __format__(self, spec):
        """커스텀 포맷"""
        if spec == 'C':
            return f"{self.celsius:.1f}°C"
        elif spec == 'F':
            return f"{self.fahrenheit:.1f}°F"
        else:
            return str(self)

    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return abs(self.celsius - other.celsius) < 0.01

    def __lt__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius

    def __add__(self, other):
        """온도 더하기"""
        if isinstance(other, Temperature):
            return Temperature(self.celsius + other.celsius)
        return Temperature(self.celsius + other)

    def __sub__(self, other):
        """온도 빼기"""
        if isinstance(other, Temperature):
            return Temperature(self.celsius - other.celsius)
        return Temperature(self.celsius - other)

# 테스트
t1 = Temperature(25)
t2 = Temperature(30)
t3 = Temperature(20)

print(t1)                    # 25.0°C (77.0°F)
print(repr(t1))              # Temperature(25)
print(f"{t1:C}")             # 25.0°C
print(f"{t1:F}")             # 77.0°F

print(f"\nt2 > t1: {t2 > t1}")
print(f"t1 < t2: {t1 < t2}")

t4 = t1 + Temperature(5)
print(f"\n{t1} + 5°C = {t4}")

temps = [t2, t1, t3]
print(f"\n정렬 전: {temps}")
print(f"정렬 후: {sorted(temps)}")

문제 2: 파일 경로 관리자

파일 경로를 다루는 SmartPath 클래스를 작성하세요.

요구사항:

  • 경로 문자열로 초기화
  • / 연산자로 경로 결합 (path / "subdir")
  • in 연산자로 파일 존재 확인
  • len()으로 경로 깊이 반환
  • 이터레이션으로 경로 구성 요소 순회
  • 컨텍스트 매니저로 임시 디렉토리 생성/삭제
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import os
import shutil

class SmartPath:
    """스마트 파일 경로 관리자"""
    def __init__(self, path):
        self.path = str(path)

    def __truediv__(self, other):
        """경로 결합 (/)"""
        return SmartPath(os.path.join(self.path, str(other)))

    def __contains__(self, item):
        """파일 존재 확인 (in)"""
        full_path = os.path.join(self.path, str(item))
        return os.path.exists(full_path)

    def __len__(self):
        """경로 깊이"""
        return len(self.path.split(os.sep))

    def __iter__(self):
        """경로 구성 요소 순회"""
        return iter(self.path.split(os.sep))

    def __enter__(self):
        """컨텍스트 매니저 진입"""
        os.makedirs(self.path, exist_ok=True)
        print(f"📁 디렉토리 생성: {self.path}")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """컨텍스트 매니저 종료"""
        if os.path.exists(self.path):
            shutil.rmtree(self.path)
            print(f"🗑️  디렉토리 삭제: {self.path}")
        return False

    def __str__(self):
        return self.path

    def __repr__(self):
        return f"SmartPath('{self.path}')"

# 테스트
base = SmartPath("/tmp/test")
subdir = base / "data" / "files"
print(f"경로: {subdir}")
print(f"깊이: {len(subdir)}")
print(f"구성 요소: {list(subdir)}")

# 컨텍스트 매니저
with SmartPath("/tmp/temp_work") as temp_dir:
    print(f"작업 디렉토리: {temp_dir}")
    # 임시 작업 수행
print("디렉토리 자동 삭제됨")

문제 3: 이벤트 로거

이벤트를 기록하고 분석하는 EventLogger 클래스를 작성하세요.

요구사항:

  • 호출 가능 객체 (logger(event))
  • len()으로 이벤트 개수
  • 인덱싱으로 이벤트 조회
  • in으로 이벤트 타입 확인
  • 이터레이션으로 이벤트 순회
  • 컨텍스트 매니저로 자동 요약
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from datetime import datetime
from collections import Counter

class EventLogger:
    """이벤트 로거"""
    def __init__(self, name):
        self.name = name
        self.events = []

    def __call__(self, event_type, message):
        """이벤트 기록"""
        event = {
            'timestamp': datetime.now(),
            'type': event_type,
            'message': message
        }
        self.events.append(event)
        print(f"[{event_type}] {message}")

    def __len__(self):
        """이벤트 개수"""
        return len(self.events)

    def __getitem__(self, index):
        """이벤트 조회"""
        return self.events[index]

    def __contains__(self, event_type):
        """이벤트 타입 확인"""
        return any(e['type'] == event_type for e in self.events)

    def __iter__(self):
        """이벤트 순회"""
        return iter(self.events)

    def __enter__(self):
        """컨텍스트 시작"""
        self("INFO", f"{self.name} 시작")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """컨텍스트 종료"""
        if exc_type:
            self("ERROR", f"오류: {exc_val}")

        self("INFO", f"{self.name} 종료")
        self.print_summary()
        return False

    def print_summary(self):
        """요약 출력"""
        print(f"\n📊 {self.name} 이벤트 요약:")
        print(f"  총 이벤트: {len(self)}")

        event_types = Counter(e['type'] for e in self)
        print("  이벤트 타입별:")
        for event_type, count in event_types.items():
            print(f"{event_type}: {count}")

# 테스트
with EventLogger("데이터 처리") as logger:
    logger("INFO", "데이터 로드 중...")
    logger("INFO", "데이터 검증 중...")
    logger("WARNING", "일부 데이터 누락")
    logger("INFO", "처리 완료")

print(f"\n전체 이벤트 수: {len(logger)}")
print(f"WARNING 있음? {'WARNING' in logger}")

💡 실전 팁 & 주의사항

Tip 1: str__과 __repr 둘 다 구현하기

사용자용과 개발자용 표현을 모두 제공하면 디버깅이 쉬워집니다.

1
2
3
4
5
6
class Point:
    def __str__(self):
        return f"({self.x}, {self.y})"  # 사용자용

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"  # 개발자용

Tip 2: 컨텍스트 매니저로 리소스 관리

파일, 네트워크 연결 등은 반드시 컨텍스트 매니저로 관리하세요.

1
2
3
with MyResource() as resource:
    resource.use()
# 자동으로 정리됨

Tip 3: 연산자 오버로딩은 직관적으로

예상 가능한 동작으로 구현하고, 과도한 오버로딩은 피하세요.

1
2
3
4
5
6
7
# ✅ 직관적
def __add__(self, other):
    return Vector(self.x + other.x, self.y + other.y)

# ❌ 혼란스러움
def __add__(self, other):
    return Vector(self.x * other.x, self.y * other.y)

📝 오늘 배운 내용 정리

특수 메서드 목적 예시
__str__ 사용자 친화적 문자열 print(obj)
__repr__ 개발자용 표현 디버깅
__add__ + 연산자 obj1 + obj2
__len__ len() 함수 len(obj)
__getitem__ 인덱싱 obj[key]
__iter__ 반복 가능 for item in obj
__call__ 호출 가능 obj()
__enter__/__exit__ 컨텍스트 매니저 with obj:

특수 메서드 작성 가이드

권장사항:

  • __str__: 사용자가 읽기 쉬운 형태
  • __repr__: eval(repr(obj)) == obj 원칙
  • 연산자 오버로딩: 직관적이고 예측 가능하게
  • 컨테이너: __len__, __getitem__ 함께 구현

주의사항:

  • 과도한 연산자 오버로딩 지양
  • 예상 밖의 동작 금지
  • 표준 프로토콜 준수

🔗 관련 자료

📚 이전 학습

Day 38: 다형성(Polymorphism) ⭐⭐⭐⭐

어제는 다형성의 개념, 덕 타이핑(Duck Typing), 연산자 오버로딩, 추상 베이스 클래스(ABC)를 배웠습니다!

📚 다음 학습

Day 40: 미니 프로젝트: 도서 관리 시스템 ⭐⭐⭐⭐

내일은 클래스, 상속, 캡슐화, 다형성 종합 활용, 특수 메서드로 사용자 친화적 인터페이스 구현, 실전 도서 관리 시스템 완성으로 Phase 4 최종 프로젝트를 진행합니다!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 39/100 Phase 4: 객체지향 프로그래밍 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.