파이썬 클래스에서 특수 메서드 탐구하기

PythonBeginner
지금 연습하기

소개

본 랩에서는 파이썬의 특수 메서드, 종종 이중 밑줄 (double-underscore) 이름 때문에 "dunder" 메서드라고 불리는 메서드들을 탐구할 것입니다. 이 메서드들이 어떻게 클래스와 객체의 동작을 사용자 정의할 수 있게 해주는지 실질적으로 이해하게 될 것입니다.

인스턴스 생성을 제어하는 __new__ 메서드와 객체 소멸을 위한 __del__ 메서드에 대해 배울 것입니다. 또한, 메모리 사용량을 최적화하고 속성을 제한하기 위해 __slots__를 사용하는 방법과, __call__ 메서드를 사용하여 클래스 인스턴스를 함수처럼 호출 가능하게 만드는 방법을 볼 것입니다. 실습 예제를 통해 더 효율적이고 표현력이 풍부한 파이썬 코드를 작성하는 방법을 배우게 될 것입니다.

이것은 가이드 실험입니다. 학습과 실습을 돕기 위한 단계별 지침을 제공합니다.각 단계를 완료하고 실무 경험을 쌓기 위해 지침을 주의 깊게 따르세요. 과거 데이터에 따르면, 이것은 초급 레벨의 실험이며 완료율은 100%입니다.학습자들로부터 100%의 긍정적인 리뷰율을 받았습니다.

__new__ 메서드 이해 및 사용하기

이 단계에서는 __new__ 메서드를 탐구합니다. __init__은 객체가 생성된 후 속성을 초기화하는 데 일반적으로 사용되지만, __new__는 실제로 인스턴스를 생성하는 메서드입니다. __init__보다 먼저 호출됩니다.

주요 차이점은 다음과 같습니다.

  • __new__는 클래스 (cls) 를 첫 번째 인수로 받는 정적 메서드 (static method) 입니다. 클래스의 새 인스턴스를 생성하고 반환하는 역할을 합니다.
  • __init__은 인스턴스 (self) 를 첫 번째 인수로 받는 인스턴스 메서드입니다. 새로 생성된 객체를 초기화하며 아무것도 반환하지 않습니다.

일반적으로 object 클래스의 기본 구현으로 충분하기 때문에 __new__를 재정의할 필요는 없습니다. 하지만 싱글턴 패턴 (Singleton pattern) 을 구현하거나 불변 타입 (immutable types) 의 인스턴스를 생성하는 것과 같은 고급 사례에 유용합니다.

__new__가 실제로 어떻게 작동하는지 살펴보겠습니다. 인스턴스 생성 중에 메시지를 출력하는 Dog 클래스를 만들 것입니다.

먼저, IDE 왼쪽의 파일 탐색기에서 dog_cat.py 파일을 엽니다.

다음 코드를 dog_cat.py 파일에 추가합니다. 이 코드는 Animal 기본 클래스와 __new__ 메서드를 재정의하는 Dog 서브클래스를 정의합니다.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name
        print(f'Initializing {self._name} in Animal.')

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    ## 첫 번째 매개변수는 클래스 자체를 참조하는 cls 입니다.
    ## 또한 생성자에 전달된 모든 인수를 받아야 합니다.
    def __new__(cls, name, age):
        print('A new Dog instance is being created.')
        ## 인스턴스를 생성하기 위해 부모 클래스의 __new__ 메서드를 호출합니다.
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print(f'Initializing {name} in Dog.')
        ## 이름 설정을 위해 부모 클래스의 __init__을 호출합니다.
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

## Dog 인스턴스 생성
print("Creating a Dog object...")
d = Dog('Buddy', 5)
print("Dog object created.")
print(f"Dog's name: {d._name}, Age: {d.age}")

파일을 저장합니다 (Ctrl+S 또는 Cmd+S).

이제 IDE 에서 터미널을 엽니다 (메뉴 Terminal > New Terminal 사용 가능). 스크립트를 실행하여 메서드 호출 순서를 관찰합니다.

python ~/project/dog_cat.py

다음과 같은 출력을 보게 될 것입니다. __new__가 인스턴스 생성을 위해 먼저 호출된 다음, 초기화를 위해 __init__ 메서드들이 호출되는 것을 확인하세요.

Creating a Dog object...
A new Dog instance is being created.
Initializing Buddy in Dog.
Initializing Buddy in Animal.
Dog object created.
Dog's name: Buddy, Age: 5

이는 __new__가 객체 생성을 제어하고, __init__이 그 후에 객체를 구성함을 보여줍니다.

__del__ 메서드 구현 및 테스트

이 단계에서는 __del__ 메서드에 대해 배웁니다. 이 메서드는 최종자 (finalizer) 또는 소멸자 (destructor) 라고 불립니다. 객체의 참조 횟수 (reference count) 가 0 이 되어 파이썬의 가비지 컬렉터 (garbage collector) 에 의해 소멸될 예정일 때 호출됩니다. 네트워크 연결이나 파일 핸들 (file handle) 을 닫는 것과 같은 정리 작업에 자주 사용됩니다.

del 문을 사용하여 객체에 대한 참조를 제거할 수 있습니다. 마지막 참조가 사라지면 __del__이 자동으로 호출됩니다.

객체가 언제 소멸되는지 확인하기 위해 Dog 클래스에 __del__ 메서드를 추가해 보겠습니다.

dog_cat.py 파일을 다시 엽니다. 파일의 전체 내용을 다음 코드로 교체합니다. 이 버전은 (모듈 임포트 시 객체 생성을 피하기 위해) Dog 인스턴스를 생성하는 코드를 제거하고, Dog 클래스에 __del__ 메서드를 추가합니다.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    def __new__(cls, name, age):
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

    ## 이 메서드를 Dog 클래스에 추가합니다
    def __del__(self):
        print(f'The Dog object {self._name} is being deleted.')

dog_cat.py 파일을 저장합니다.

이제 이 동작을 테스트하기 위한 별도의 스크립트를 작성해 보겠습니다. 파일 탐색기에서 test_del.py 파일을 엽니다.

test_del.py에 다음 코드를 추가합니다. 이 스크립트는 두 개의 Dog 인스턴스를 생성한 다음 그중 하나를 명시적으로 삭제합니다.

## File Name: test_del.py

from dog_cat import Dog
import time

print("Creating two Dog objects: d1 and d2.")
d1 = Dog('Tom', 3)
d2 = Dog('John', 5)

print("\nDeleting reference to d1...")
del d1
print("Reference to d1 deleted.")

## 가비지 컬렉터가 즉시 실행되지 않을 수 있습니다.
## 실행 시간을 주기 위해 약간의 지연을 추가합니다.
time.sleep(1)

print("\nScript is about to end. d2 will be deleted automatically.")

파일을 저장합니다. 이제 터미널에서 test_del.py 스크립트를 실행합니다.

python ~/project/test_del.py

출력을 관찰합니다. Tom에 대한 __del__ 메시지는 del d1이 호출된 후에 나타납니다. John에 대한 메시지는 스크립트가 종료될 때 d2 객체가 가비지 컬렉션되므로 맨 끝에 나타납니다.

Creating two Dog objects: d1 and d2.

Deleting reference to d1...
The Dog object Tom is being deleted.
Reference to d1 deleted.

Script is about to end. d2 will be deleted automatically.
The Dog object John is being deleted.

참고: 가비지 컬렉션의 정확한 타이밍은 다를 수 있습니다. __del__del이 사용된 직후가 아니라 객체가 수집될 때 호출됩니다.

__slots__ 을 이용한 속성 제어

이 단계에서는 __slots__에 대해 배웁니다. 기본적으로 파이썬은 인스턴스 속성을 __dict__라는 특수한 딕셔너리에 저장합니다. 이를 통해 언제든지 객체에 새 속성을 추가할 수 있습니다. 하지만 이러한 유연성은 추가 메모리를 사용합니다.

클래스에 __slots__ 속성을 정의하면 인스턴스가 가질 수 있는 속성의 고정된 목록을 지정할 수 있습니다. 이는 두 가지 주요 효과를 가져옵니다.

  1. 메모리 절약: 파이썬은 각 인스턴스에 대해 __dict__ 대신 더 간결한 내부 구조를 사용하므로, 특히 많은 객체를 생성할 때 메모리 사용량을 크게 줄일 수 있습니다.
  2. 속성 제한: __slots__에 나열되지 않은 속성을 인스턴스에 더 이상 추가할 수 없습니다. 이는 오타를 방지하고 엄격한 객체 구조를 적용하는 데 도움이 됩니다.

__slots__이 어떻게 작동하는지 확인하기 위한 예제를 만들어 보겠습니다. 파일 탐색기에서 slots_example.py 파일을 엽니다.

slots_example.py에 다음 코드를 추가합니다.

## File Name: slots_example.py

class Player:
    ## __slots__를 사용하여 허용된 속성을 정의합니다
    __slots__ = ('name', 'level')

    def __init__(self, name, level):
        self.name = name
        self.level = level

## Player 인스턴스 생성
p1 = Player('Hero', 10)

## 허용된 속성에 접근
print(f"Player name: {p1.name}")
print(f"Player level: {p1.level}")

## 이제 __slots__에 없는 새 속성을 추가해 봅니다
print("\nTrying to add a 'score' attribute...")
try:
    p1.score = 100
    print(f"Player score: {p1.score}")
except AttributeError as e:
    print(f"Caught an error: {e}")

## 또한 인스턴스에 __dict__ 속성이 있는지 확인합니다
print("\nChecking for __dict__...")
try:
    print(p1.__dict__)
except AttributeError as e:
    print(f"Caught an error: {e}")

파일을 저장합니다. 이제 터미널에서 slots_example.py 스크립트를 실행합니다.

python ~/project/slots_example.py

출력은 namelevel에는 값을 할당할 수 있지만, score에 값을 할당하려고 하면 AttributeError가 발생함을 보여줍니다. 또한 인스턴스에 __dict__가 없음을 확인시켜 줍니다.

Player name: Hero
Player level: 10

Trying to add a 'score' attribute...
Caught an error: 'Player' object has no attribute 'score'

Checking for __dict__...
Caught an error: 'Player' object has no attribute '__dict__'

이는 __slots__이 어떻게 고정된 속성 집합을 적용하고 인스턴스 딕셔너리를 제거하여 메모리를 최적화할 수 있는지 보여줍니다.

__call__ 을 사용하여 인스턴스를 호출 가능하게 만들기

이 단계에서는 __call__ 메서드를 탐구합니다. 파이썬에서 괄호 ()를 사용하여 함수처럼 "호출"할 수 있는 객체를 호출 가능 객체 (callable objects) 라고 합니다. 함수와 메서드는 본질적으로 호출 가능합니다.

기본적으로 클래스 인스턴스는 호출 가능하지 않습니다. 하지만 클래스에 __call__ 특수 메서드를 정의하면 해당 인스턴스가 호출 가능해집니다. 인스턴스를 함수처럼 호출할 때, 해당 인스턴스의 __call__ 메서드 내부 코드가 실행됩니다. 이는 자체 내부 상태를 유지하면서 함수처럼 동작하는 객체를 만드는 데 유용합니다.

인스턴스를 호출할 수 있는 클래스를 만들어 보겠습니다. 파일 탐색기에서 callable_instance.py 파일을 엽니다.

callable_instance.py에 다음 코드를 추가합니다.

## File Name: callable_instance.py

class Greeter:
    def __init__(self, greeting):
        ## 이 상태는 인스턴스와 함께 저장됩니다
        self.greeting = greeting
        print(f'Greeter initialized with "{self.greeting}"')

    ## 인스턴스를 호출 가능하게 만들기 위해 __call__ 메서드를 정의합니다
    def __call__(self, name):
        ## 이 코드는 인스턴스가 호출될 때 실행됩니다
        print(f"{self.greeting}, {name}!")

## Greeter 인스턴스 생성
hello_greeter = Greeter("Hello")

## 내장 함수 callable() 을 사용하여 인스턴스가 호출 가능한지 확인합니다
print(f"Is hello_greeter callable? {callable(hello_greeter)}")

## 이제 인스턴스를 함수처럼 호출합니다
print("\nCalling the instance:")
hello_greeter("Alice")
hello_greeter("Bob")

## 다른 상태를 가진 또 다른 인스턴스 생성
goodbye_greeter = Greeter("Goodbye")
print("\nCalling the second instance:")
goodbye_greeter("Charlie")

파일을 저장합니다. 이제 터미널에서 callable_instance.py 스크립트를 실행합니다.

python ~/project/callable_instance.py

출력은 hello_greeter 인스턴스가 실제로 호출 가능하다는 것을 보여줍니다. 호출할 때마다 __call__ 메서드가 실행되며, 초기화 중에 설정된 상태 (self.greeting) 를 사용합니다.

Greeter initialized with "Hello"
Is hello_greeter callable? True

Calling the instance:
Hello, Alice!
Hello, Bob!
Greeter initialized with "Goodbye"

Calling the second instance:
Goodbye, Charlie!

이는 __call__이 어떻게 상태를 가지는 함수와 같은 객체를 생성할 수 있게 하는지 보여주며, 이는 객체 지향 프로그래밍에서 강력한 기능입니다.

요약

본 실습에서는 파이썬의 강력한 특수 메서드 (special methods) 몇 가지를 살펴보았습니다. __new__를 사용하여 인스턴스 생성 프로세스를 제어하고 __init__이 호출되기 전에 개입할 수 있는 방법을 배웠습니다. 객체가 가비지 컬렉션될 때 실행되는 정리 (cleanup) 로직을 정의하기 위해 __del__ 메서드를 구현했습니다. 또한, 인스턴스 __dict__ 생성을 방지하여 메모리를 최적화하고 엄격한 속성 모델을 적용하기 위해 __slots__을 사용했습니다. 마지막으로, __call__ 메서드를 구현하여 객체가 함수처럼 동작하도록 만들었습니다. 이러한 더블 언더바 메서드 (dunder methods) 를 숙달하면 더욱 유연하고 효율적이며 파이써닉 (Pythonic) 한 클래스를 작성할 수 있습니다.