Python 강좌 : 제 35강 - 매직 메서드 (1)
매직 메서드(Magic Method)
매직 메서드(Magic Method)는 미리 정의되어 있는 메서드들을 재정의하여 클래스를 활용할 수 있도록 변경합니다.
내장 함수들이 처리하는 연산을 변경해 사용자 정의 클래스나 함수 등을 효율적으로 사용할 수 있습니다.
메서드 이름의 앞뒤에 밑줄(Underscore, _)을 두 개씩 붙여(__이름__) 매직 메서드를 정의할 수 있습니다.
매직 메서드는 던더 메서드(Dunder Method, Double Underscore Method)라고도 부릅니다.
매직 메서드의 가장 큰 특징은 직접 호출하지 않는다는 점입니다. 사용자는 len(x), x + y, print(x)처럼 평소의 문법과 내장 함수를 사용하고, Python 인터프리터가 그 문법에 대응하는 매직 메서드를 대신 호출합니다.
그러므로 매직 메서드를 재정의하면, 직접 만든 클래스도 int, str, list 같은 내장 형식처럼 같은 문법으로 다룰 수 있게 됩니다.
print(len([1, 2, 3]))
print([1, 2, 3].__len__())
print(5 + 3)
print((5).__add__(3))- 결과
- 3
3
8
8
len([1, 2, 3])은 내부적으로 리스트의 __len__ 메서드를 호출하며, 5 + 3은 정수의 __add__ 메서드를 호출합니다. 그러므로 두 방식의 결과가 같습니다.
(5).__add__(3)처럼 소괄호로 감싼 이유는, 5.__add__로 작성하면 5.을 실수로 해석해 SyntaxError가 발생하기 때문입니다.
이처럼 매직 메서드는 직접 호출할 수도 있지만, 일반적으로는 문법이나 내장 함수를 통해 간접적으로 호출되도록 정의합니다.
이번 강좌에서는 인스턴스의 생성·초기화·호출·소멸을 담당하는 매직 메서드와, 인스턴스를 문자열이나 바이트로 표현하는 매직 메서드를 다룹니다. 연산자는 제 36강, 속성 접근은 제 37강에서 이어서 다룹니다.
- Tip : 클래스의 기본 문법은 제 27강에서 확인할 수 있습니다.
인스턴스(Instance) 정의
인스턴스는 클래스명()으로 만들어지지만, 실제로는 할당 → 초기화 → 사용 → 소멸의 여러 단계를 거칩니다.
각 단계마다 대응하는 매직 메서드가 있으며, 이 메서드를 재정의하면 각 단계에서 원하는 작업을 추가할 수 있습니다.
다음 예제는 네 단계의 매직 메서드를 모두 정의해 어떤 순서로 호출되는지 출력합니다.
class Daeheeyun:
def __new__(cls, *args, **kwargs):
print("인스턴스 할당")
return super().__new__(cls)
def __init__(self, site="076923"):
print("인스턴스 초기화")
self.site = site
self.link = site + ".github.io"
def __call__(self, protocol=True):
print("인스턴스 호출")
if protocol == True:
return "https://" + self.link
else:
return "http://" + self.link
def __del__(self):
print("인스턴스 소멸")
instance = Daeheeyun()
print(instance.link)
print(instance(False))
del instance- 결과
- 인스턴스 할당
인스턴스 초기화
076923.github.io
인스턴스 호출
http://076923.github.io
인스턴스 소멸
결과를 한 줄씩 읽으면 다음과 같습니다.
Daeheeyun()을 실행하면 먼저__new__가 호출되어인스턴스 할당이 출력되고, 빈 인스턴스가 만들어집니다.- 이어서 그 인스턴스를
self로 받아__init__이 호출되어인스턴스 초기화가 출력되고,site와link속성이 할당됩니다. instance.link는 일반 속성 조회이므로 매직 메서드 없이076923.github.io가 출력됩니다.instance(False)는 인스턴스를 함수처럼 호출하므로__call__이 실행되어인스턴스 호출이 출력되고,http://주소가 반환됩니다.del instance로 마지막 참조가 사라지면__del__이 호출되어인스턴스 소멸이 출력됩니다.
| 매직 메서드 | 호출하는 문법 | 호출 시점 |
|---|---|---|
__new__(cls, ...) |
클래스명(...) |
인스턴스를 만들 때, 가장 먼저 |
__init__(self, ...) |
클래스명(...) |
__new__가 인스턴스를 반환한 직후 |
__call__(self, ...) |
인스턴스(...) |
인스턴스를 함수처럼 호출할 때마다 |
__del__(self) |
del 이름 등 |
참조 횟수가 0이 되어 객체가 제거되기 직전 |
__new__ : 할당 메서드
새로운 인스턴스를 만들기 위해 가장 먼저 호출되는 메서드입니다.
클래스명()으로 인스턴스를 생성하면, 먼저 __new__가 호출되어 인스턴스를 만들고, 그 인스턴스로 __init__이 호출됩니다.
__new__가 __init__을 직접 호출하는 것은 아닙니다. __new__가 반환한 객체가 해당 클래스의 인스턴스일 때만 __init__이 이어서 실행됩니다.
그러므로 __new__에서 인스턴스를 반환하지 않거나 다른 형식의 객체를 반환한다면 __init__은 실행되지 않습니다.
super().__new__(cls[, ...])의 형태로 슈퍼 클래스의 __new__를 호출해 생성한 인스턴스를 반환합니다.
__new__는 인스턴스가 아직 없는 상태에서 호출되므로, 첫 번째 매개변수로 self가 아닌 클래스 자신인 cls를 받습니다.
클래스명()에 전달한 인자는 __new__와 __init__에 똑같이 전달됩니다. 예제의 __new__가 *args, **kwargs를 받는 이유는, __init__이 받는 인자를 그대로 받아 무시하기 위해서입니다.
__new__는 일반적으로 사용되지 않으며, 불변형(int, str, tuple) 등의 서브 클래스에서 인스턴스 생성을 커스터마이징할 수 있도록 하는 데 사용됩니다.
class Upper(str):
def __new__(cls, text):
return super().__new__(cls, text.upper())
word = Upper("python")
print(word)
print(type(word))
print(isinstance(word, str))- 결과
- PYTHON
<class ‘__main__.Upper’>
True
Upper는 str을 상속받아, 전달된 문자열을 항상 대문자로 저장하는 클래스입니다.
super().__new__(cls, text.upper())는 부모 클래스인 str의 __new__에 대문자로 바꾼 문자열을 전달해 인스턴스를 만듭니다. 그러므로 word는 PYTHON을 값으로 가지는 Upper 인스턴스이며, str의 인스턴스이기도 합니다.
문자열은 불변(immutable) 형식이므로, 인스턴스가 만들어진 뒤에는 값을 바꿀 수 없습니다. 그러므로 값을 바꾸는 작업은 반드시 인스턴스가 만들어지기 전인 __new__에서 처리해야 합니다.
class Upper(str):
def __init__(self, text):
text = text.upper()
print(Upper("python"))- 결과
- python
같은 작업을 __init__에서 처리하면 의도대로 동작하지 않습니다.
__init__이 호출되는 시점에는 이미 str.__new__가 "python"으로 인스턴스를 만든 상태이며, text = text.upper()는 지역 변수 text만 바꿀 뿐 인스턴스의 값을 바꾸지 못합니다.
- Tip :
__new__는 항상 같은 인스턴스를 반환하는싱글턴(Singleton)패턴을 구현할 때도 사용됩니다. 단, 이 경우에도__init__은클래스명()을 호출할 때마다 다시 실행되므로 주의해야 합니다.
__init__ : 초기화 메서드
새로운 인스턴스를 만들 때 사용될 인자들을 선언하는 메서드입니다.
할당된 인자들을 선언해서 사용하며, 일반적으로 매개변수를 할당받을 때 동일한 이름에 self를 추가해 동일한 명칭으로 사용합니다.
__init__은 클래스명(인자)의 인자를 받아, __new__가 만든 인스턴스(self)에 속성을 채워 넣는 역할을 합니다. 인스턴스를 만드는 것은 __new__이고, 만들어진 인스턴스를 사용할 수 있는 상태로 준비하는 것이 __init__입니다.
흔히 생성자라 부르지만, 정확하게는 초기화 메서드이므로 인스턴스를 반환하지 않습니다.
class Daeheeyun:
def __init__(self, site):
self.site = site
return site
Daeheeyun("076923")- 결과
- TypeError: __init__() should return None, not ‘str’
__init__은 반드시 None을 반환해야 합니다. return 문을 작성하지 않으면 자동으로 None이 반환되므로, 일반적으로 return 문을 작성하지 않습니다.
예제처럼 다른 값을 반환하면 인스턴스를 만드는 시점에 TypeError가 발생합니다. 생성 과정에서 다른 객체를 돌려주고 싶다면 __new__를 재정의해야 합니다.
__call__ : 호출 메서드
인스턴스가 함수로 호출될 때 실행되는 메서드입니다.
__call__ 메서드가 존재하면, 호출 가능한 형식(callable type)이 되며, 함수처럼 사용할 수 있습니다.
__call__은 인스턴스를 생성한 다음, instance(False)처럼 인스턴스를 호출할 때마다 값을 전달받을 수 있습니다.
반대로 __init__은 인스턴스를 생성할 때 한 번만 값을 전달받습니다.
__call__은 상태를 기억하는 함수를 만들 때 유용합니다. 일반 함수는 호출이 끝나면 지역 변수가 사라지지만, 인스턴스는 속성에 값을 저장해 두고 호출할 때마다 이어서 사용할 수 있습니다.
class Counter:
def __init__(self):
self.count = 0
def __call__(self, step=1):
self.count += step
return self.count
counter = Counter()
print(counter())
print(counter())
print(counter(10))
print(callable(counter), callable(counter.count))- 결과
- 1
2
12
True False
counter()를 호출할 때마다 counter.__call__()이 실행되어 count 속성이 1, 2로 증가합니다.
counter(10)은 step에 10을 전달하므로, 이전 값 2에 10을 더한 12가 반환됩니다. 호출 사이에 값이 유지되는 것은 count가 인스턴스 속성에 저장되어 있기 때문입니다.
callable()은 객체가 호출 가능한지 확인합니다. counter는 __call__이 정의되어 있으므로 True, 정수인 counter.count는 호출할 수 없으므로 False입니다.
-
Tip :
callable(instance)로 객체가 호출 가능한지 확인할 수 있습니다. -
Tip : 상태를 기억하는 함수는 제 40강의
클로저(Closure)로도 만들 수 있습니다. 상태가 단순하면 클로저, 상태와 함께 여러 메서드가 필요하면__call__을 정의한 클래스가 적합합니다.
__del__ : 소멸 메서드
인스턴스가 제거될 때 호출되는 메서드입니다.
소멸자라 부르며, del 등을 통해 간접적으로 호출될 수 있습니다.
인스턴스가 삭제되기 전에 호출됩니다. 정확하게는 참조 횟수(reference count)가 0이 될 때 호출됩니다.
del instance는 객체를 직접 삭제하는 것이 아니라 instance라는 이름을 제거해 참조 횟수를 줄입니다. 다른 변수가 같은 인스턴스를 참조하고 있다면 __del__은 호출되지 않습니다.
객체가 종료될 때 사용되는 메서드입니다.
class Daeheeyun:
def __del__(self):
print("인스턴스 소멸")
a = Daeheeyun()
b = a
del a
print("a 삭제 후")
del b
print("b 삭제 후")- 결과
- a 삭제 후
인스턴스 소멸
b 삭제 후
b = a는 인스턴스를 복사하지 않고, 같은 인스턴스를 가리키는 이름을 하나 더 만듭니다. 그러므로 인스턴스의 참조 횟수는 2가 됩니다.
del a를 실행해도 b가 여전히 인스턴스를 참조하므로 __del__은 호출되지 않고, a 삭제 후가 먼저 출력됩니다.
del b로 마지막 참조까지 사라지면 그때 __del__이 호출되어 인스턴스 소멸이 출력됩니다.
-
Tip :
__del__이 호출되는 시점은 인터프리터 구현에 따라 달라질 수 있으므로, 파일이나 연결 같은 자원 정리는with문을 사용하는 것이 안전합니다. -
Tip : 서로를 참조하는 객체(순환 참조)는 참조 횟수가 0이 되지 않으므로, 가비지 컬렉터가 나중에 정리할 때
__del__이 호출됩니다. 프로그램이 종료될 때 아직 남아 있는 객체의__del__은 호출이 보장되지 않습니다.
매직 메서드를 찾는 위치
매직 메서드는 일반 메서드와 달리, 인스턴스가 아닌 클래스(형식)에서 찾습니다.
즉, instance()는 instance.__call__()이 아니라 type(instance).__call__(instance)처럼 동작합니다.
class Daeheeyun:
pass
instance = Daeheeyun()
instance.__call__ = lambda: "인스턴스에 추가한 __call__"
print(instance.__call__())
try:
instance()
except TypeError as e:
print("TypeError:", e)- 결과
- 인스턴스에 추가한 __call__
TypeError: ‘Daeheeyun’ object is not callable
예제는 인스턴스에 __call__이라는 이름의 속성을 직접 할당했습니다.
instance.__call__()처럼 이름으로 직접 호출하면 일반 속성처럼 찾으므로 정상적으로 실행됩니다.
하지만 instance()는 클래스인 Daeheeyun에서 __call__을 찾으며, 클래스에는 __call__이 정의되어 있지 않으므로 TypeError가 발생합니다.
그러므로 매직 메서드는 반드시 클래스 내부에 정의해야 문법과 내장 함수에서 동작합니다.
인스턴스(Instance) 표현
인스턴스를 print()로 출력하거나, 문자열 포맷팅에 넣거나, 바이트로 변환할 때도 매직 메서드가 호출됩니다.
아무것도 정의하지 않은 클래스의 인스턴스를 출력하면 <__main__.Daeheeyun object at 0x...>처럼 클래스 이름과 메모리 주소만 출력되어 내용을 알 수 없습니다.
표현 관련 매직 메서드를 재정의하면, 인스턴스가 어떤 값을 가지고 있는지 읽기 쉬운 형태로 보여줄 수 있습니다.
class Daeheeyun:
def __init__(self, site="076923"):
self.site = site
self.link = site + ".github.io"
def __repr__(self):
return str({"site": self.site, "link": self.link})
def __str__(self):
return "Daeheeyun(site = " + self.site + ", link = " + self.link + ")"
def __format__(self, format_spec):
return format(self.link, format_spec)
def __bytes__(self):
return str.encode(self.link)
instance = Daeheeyun()
print(repr(instance))
print(str(instance))
print(format(instance, ">30"))
print(bytes(instance))- 결과
- {‘site’: ‘076923’, ‘link’: ‘076923.github.io’}
Daeheeyun(site = 076923, link = 076923.github.io)
076923.github.io
b’076923.github.io’
네 개의 내장 함수가 각각 다른 매직 메서드를 호출합니다.
repr(instance)는__repr__을 호출해 사전 형태의 문자열을 반환합니다.str(instance)는__str__을 호출해Daeheeyun(site = ...)형태의 문자열을 반환합니다.format(instance, ">30")은__format__을 호출하며,format_spec에">30"이 전달됩니다. 예제는link를 30칸 너비에 오른쪽 정렬했으므로 앞쪽이 공백으로 채워집니다.bytes(instance)는__bytes__를 호출해link를 인코딩한 바이트 문자열을 반환합니다.
| 매직 메서드 | 호출하는 문법/내장 함수 | 반환값 |
|---|---|---|
__repr__(self) |
repr(x), f-string의 !r, 컨테이너 출력, 대화형 셸 |
str |
__str__(self) |
str(x), print(x), f-string의 {x}·!s |
str |
__format__(self, format_spec) |
format(x, spec), f-string의 {x:spec}, str.format() |
str |
__bytes__(self) |
bytes(x) |
bytes |
__repr__ : 형식적인 문자열 메서드
repr()을 호출해 형식적인(official) 문자열 표현에 사용됩니다.
가능한 올바른 표현식을 사용하며, 디버깅을 위해 주로 사용합니다.
명확한 정보를 담아 활용하며, 문자열 형식으로 반환해야합니다.
repr() 외에도 대화형 셸에서 변수 이름만 입력했을 때, 리스트나 사전 안의 요소를 출력할 때, f-string에서 !r을 사용할 때 __repr__이 호출됩니다.
class Daeheeyun:
def __repr__(self):
return 76923
try:
repr(Daeheeyun())
except TypeError as e:
print("TypeError:", e)- 결과
- TypeError: __repr__ returned non-string (type int)
__repr__이 문자열이 아닌 값을 반환하면 TypeError가 발생합니다. __str__, __format__도 마찬가지로 반드시 문자열을 반환해야 합니다.
숫자를 표현하고 싶다면 str(76923)이나 f-string으로 문자열로 변환해 반환합니다.
__str__ : 비형식적인 문자열 메서드
str()을 호출해 비형식적인(informal) 문자열 표현에 사용됩니다.
repr()과는 다르게 사용자가 이해하기 쉬운 형태로 표현할 때 사용합니다.
print()는 전달받은 객체를 str()로 변환해 출력하므로, print(instance)도 __str__을 호출합니다.
repr과 str의 차이점을 표준 라이브러리의 datetime과 Fraction으로 예를 든다면 다음과 같습니다.
import datetime
from fractions import Fraction
date = datetime.date(2019, 10, 6)
print(repr(date))
print(str(date))
half = Fraction(1, 2)
print(repr(half))
print(str(half))
print(eval(repr(date)) == date)- 결과
- datetime.date(2019, 10, 6)
2019-10-06
Fraction(1, 2)
1/2
True
repr()을 통해 반환된 문자열은 객체를 다시 생성하는 Python 코드의 형태입니다. 그러므로 eval(repr(date))처럼 다시 객체로 변환해 사용이 가능합니다.
하지만, str()을 통해 반환된 2019-10-06이나 1/2는 사람이 읽기 쉬운 형태일 뿐, 그대로 코드로 활용할 수 없습니다.
즉, __repr__은 개발자를 위한 정확한 표현이고, __str__은 사용자를 위한 읽기 쉬운 표현입니다.
- Tip :
numpy배열도 같은 방식으로repr()은array([1, 2, 3]),str()은[1 2 3]을 반환합니다.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
point = Point(1, "2")
print(str(point))
print(f"{point!r}")
print([point])- 결과
- Point(1, ‘2’)
Point(1, ‘2’)
[Point(1, ‘2’)]
__str__을 정의하지 않으면, str()과 print()는 __repr__의 결과를 대신 사용합니다.
그러므로 둘 중 하나만 정의한다면 __repr__을 정의하는 것이 좋습니다.
f-string에서 !r을 사용하면 repr()의 결과로 변환되며, 리스트나 사전 같은 컨테이너를 출력할 때도 요소는 repr()로 표현됩니다.
예제의 __repr__에서 {self.y!r}을 사용했기 때문에, 문자열 "2"가 따옴표가 붙은 '2'로 표현되어 숫자 1과 구분됩니다. !r 없이 {self.y}로 작성했다면 Point(1, 2)로 출력되어 두 값의 형식을 구분할 수 없습니다.
반대로 __repr__만 정의하고 __str__을 정의하지 않아도 print()가 동작하지만, __str__만 정의한 경우 리스트 안의 인스턴스는 여전히 <__main__.Point object at 0x...>처럼 출력됩니다.
__format__ : 포맷 형식
format()을 호출해 포맷팅(formatting)할 때 사용합니다.
포맷팅의 format_spec은 format()에서 지원하지 않는 사용자 임의의 형식도 생성할 수 있습니다.
예제의 >30은 우측 정렬, 30 너비 지정입니다.
f"{instance:>30}"처럼 f-string에서 콜론(:) 뒤에 형식을 작성해도 __format__이 호출됩니다.
format_spec은 콜론 뒤에 작성한 문자열 그대로 전달되므로, 클래스마다 자신만의 형식 지정 규칙을 만들 수 있습니다. 예를 들어 datetime은 f"{date:%Y년}"처럼 날짜 전용 형식을 지원합니다.
class Site:
def __init__(self, name):
self.name = name
def __str__(self):
return f"{self.name}.github.io"
def __format__(self, format_spec):
if format_spec == "url":
return f"https://{self}"
if format_spec == "upper":
return str(self).upper()
return format(str(self), format_spec)
site = Site("076923")
print(f"{site}")
print(f"{site:url}")
print(f"{site:upper}")
print(f"[{site:^24}]")- 결과
- 076923.github.io
https://076923.github.io
076923.GITHUB.IO
[ 076923.github.io ]
f"{site}"처럼 형식을 지정하지 않아도 __format__이 호출되며, 이때 format_spec은 빈 문자열("")입니다. 예제는 빈 문자열을 마지막 return에서 처리해 __str__의 결과를 그대로 반환합니다.
f"{site:url}"과 f"{site:upper}"는 직접 정의한 url, upper 형식이 전달되어 각각 주소 형태와 대문자 형태로 반환됩니다.
f"{site:^24}"처럼 직접 처리하지 않는 형식은 format(str(self), format_spec)으로 문자열의 기본 포맷팅에 넘겨, 24칸 너비에 가운데 정렬됩니다.
이처럼 처리할 수 없는 형식은 기존 형식에 위임하면, 직접 정의한 형식과 기본 형식을 함께 사용할 수 있습니다.
class Daeheeyun:
def __init__(self, site="076923"):
self.site = site
instance = Daeheeyun()
print(f"{instance}" == str(instance))
try:
format(instance, ">30")
except TypeError as e:
print("TypeError:", e)- 결과
- True
TypeError: unsupported format string passed to Daeheeyun.__format__
__format__을 정의하지 않으면 object의 기본 __format__이 사용됩니다.
기본 __format__은 형식이 빈 문자열일 때만 str()의 결과를 반환하므로, f"{instance}"는 str(instance)와 같습니다.
하지만 >30처럼 형식을 지정하면 처리할 수 없으므로 TypeError가 발생합니다. 정렬이나 너비를 적용하려면 __format__을 정의하거나, f"{str(instance):>30}"처럼 먼저 문자열로 변환해야 합니다.
__bytes__ : 바이트 형식
bytes()를 호출해 바이트 문자열로 변환할 때 사용합니다.
접두사 b가 추가되며, 바이트 문자열로 처리됩니다.
예제의 str.encode(self.link)는 self.link.encode()와 같으며, 문자열을 UTF-8로 인코딩한 bytes를 반환합니다. 파일이나 네트워크로 데이터를 보낼 때처럼 바이트 형태가 필요한 경우에 사용합니다.
class Daeheeyun:
def __bytes__(self):
return "076923"
try:
bytes(Daeheeyun())
except TypeError as e:
print("TypeError:", e)- 결과
- TypeError: __bytes__ returned non-bytes (type str)
__bytes__는 반드시 bytes 형식을 반환해야 합니다. 예제처럼 문자열을 그대로 반환하면 TypeError가 발생하므로, encode()로 인코딩한 값을 반환합니다.
정리
| 매직 메서드 | 호출하는 문법/내장 함수 | 반환값/특징 |
|---|---|---|
__new__(cls, ...) |
클래스명(...) |
새 인스턴스를 반환, 불변형 상속 시 값 결정 |
__init__(self, ...) |
클래스명(...) (__new__ 다음) |
반드시 None 반환, 속성 초기화 |
__call__(self, ...) |
인스턴스(...) |
임의의 값, callable()이 True가 됨 |
__del__(self) |
참조 횟수가 0이 될 때 (del 등) |
호출 시점이 보장되지 않음 |
__repr__(self) |
repr(), !r, 컨테이너 출력 |
str, 개발자용 정확한 표현 |
__str__(self) |
str(), print(), !s |
str, 없으면 __repr__ 사용 |
__format__(self, format_spec) |
format(), {x:spec} |
str, 사용자 정의 형식 가능 |
__bytes__(self) |
bytes() |
bytes |
매직 메서드는 문법이나 내장 함수가 대신 호출하며, 인스턴스가 아닌 클래스에 정의해야 동작합니다.
문자열 표현을 하나만 정의한다면 __repr__을 정의하고, 사용자에게 보여줄 표현이 따로 필요할 때 __str__을 추가합니다.
댓글 남기기