파이썬 – OOP Part 6. 매직 메소드 (Magic Method)

4 분 소요

안녕하세요. 스쿨오브웹의 이상희입니다.

오늘은 우리가 항상 사용하지만 정확한 개념을 가지고 있지 않으며 심지어는 자신이 사용을 하고 있다는 자체도 모르는 매직 메소드에 대해서 알아 보겠습니다. 매직 메소드에 대한 정의는 다음과 같습니다.

매직 메소드란?

  • 클래스안에 정의할 수 있는 스페셜 메소드이며 클래스를 int, str, list등의 파이썬의 빌트인 타입(built-in type)과 같은 작동을 하게 해준다.
  • +, -, >, < 등의 오퍼레이터에 대해서 각각의 데이터 타입에 맞는 메소드로 오버로딩하여 백그라운드에서 연산을 한다.
  • __init__이나 __str__과 같이 메소드 이름 앞뒤에 더블 언더스코어(“__”)를 붙인다.

클래스를 만들때 항상 사용하는 __init__이나 __str__는 가장 대표적인 매직 메소드이며 우리들이 가장 잘 아는 매직 메소드입니다. 평소에 많이 쓰기는 하지만 이것들을 뭐라고 어떻게 불러야 하는지 모르는 분들이 많으실겁니다. “언더스코어 init 언더스코어”라고 부르는 분도 있으며, “더블 언더스코어 init 더블 언더스코어”라고 부르는 분도 있을겁니다. 그런데 가장 이상적인 호칭은 “던더 init 던더”입니다.

우리는 평소에 클래스를 생성하면서 직접적으로 __init__메소드를 호출하지 않지만 내부적으로는 실행되는 것을 알고 있습니다. 예제를 보시죠.

oop-6.py
class Dog(object):
def __init__(self, name, age):
print('이름: {}, 나이: {}'.format(name, age))
 
dog_1 = Dog('Pink', '12')
실행 결과
이름: Pink, 나이: 12

위의 코드를 보시면 클래스를 생성하니 자동으로 __init__메소드가 실행된것을 알 수 있습니다. 이번에는 다른 종류의 매직 메소드를 살펴 보도록 하죠. 우리가 평소에 아무 생각없이 사용하는 +- 또한 매직 메소드를 호출하는 오퍼레이터입니다. x + y를 실행하면 x가 가지고 있는 매직 메소드인 __add__가 실행됩니다. 결국 백그라운드에서는 x.__add__(y)가 실행되는거죠. 예제를 통해서 사실인지 확인해 보죠.

먼저 int 타입을 베이스로하는 커스텀 클래스를 만들어 보겠습니다.

oop-6.py
# int를 부모 클래스로 가진 새로운 클래스 생성
class MyInt(int):
pass
 
# 인스턴스 생성
my_num = MyInt(5)
 
# 타입 확인
print(type(my_num))  # => <class '__main__.MyInt'>
 
# int의 인스턴스인지 확인
print(isinstance(my_num, int))  # => True
 
# MyInt의 베이스 클래스 확인
print(MyInt.__bases__)  # => (<type 'int'>,)
실행 결과
<class '__main__.MyInt'>
True
(<class 'int'>,)

MyInt 클래스가 int 타입인 것을 확인했습니다. 그러면 일반 int 타입과 덧샘을 해보겠습니다.

oop-6.py
# int를 부모 클래스로 가진 새로운 클래스 생성
class MyInt(int):
pass
 
# 인스턴스 생성
my_num = MyInt(5)
 
# 덧샘 실행
print(my_num + 5)  # => 10
실행 결과
10

5 + 5를 실행한 것과 똑같은 결과를 출력했습니다. my_num이 정말 매직 메소드를 가지고 있는지 확인해 보죠.

oop-6.py
# int를 부모 클래스로 가진 새로운 클래스 생성
class MyInt(int):
pass
 
# 인스턴스 생성
my_num = MyInt(5)
 
# 매직 메소드를 가지고 있는지 확인
print(dir(my_num))
실행 결과
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dict__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

부모 클래스인 int로 부터 상속받은 매직 메소드를 엄청 많이 가지고 있네요. 그렇다면 이번에는 매직 메소드를 직접 호출해보죠.

oop-6.py
# int를 부모 클래스로 가진 새로운 클래스 생성
class MyInt(int):
pass
 
# 인스턴스 생성
my_num = MyInt(5)
 
# 매직 메소드를 직접 호출
print(my_num.__add__(5))  # => 10
실행 결과
10

my_num + 5의 결과 값과 같은 결과값이 출력됐습니다. 그럼 이번에는 매직 메소드인를 수정하여 리턴값을 정수가 아닌 문자열로 바꿔볼까요? ㅎ

oop-6.py
# int를 부모 클래스로 가진 새로운 클래스 생성
class MyInt(int):
# __add__ 변경
def __add__(self, other):
return '{} 더하기 {}{} 입니다'.format(self.real, other.real, self.real + other.real)
 
# 인스턴스 생성
my_num = MyInt(5)
 
print(my_num + 5)  # => 5 더하기 5 는 10 입니다
실행 결과
5 더하기 5 10 입니다

어떤가요? 진짜로 덧셈의 결과값을 정수 값이 아닌 문자열로 리턴했습니다.

이렇게 built-in type인 int, str, list, dict등은 사용자들의 편리함을 위해서 자신에 타입에 맞게 각종 오퍼레이터를 오버로딩하는 매직 메소드를 포함하고 있습니다. 조금 더 예를 들어보도록 하죠.

oop-6.py
# 리스트의 덧셈
print([1,2,3] + [4,5,6])
# 매직 메소드로 덧셈
print([1,2,3].__add__([4,5,6]))
 
# 사전의 길이 확인
print(len({'one':1, 'two': 2, 'three': 3}))
# 매직 메소드로 사전의 길이 확인
print({'one':1, 'two': 2, 'three': 3}.__len__())
실행 결과
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
3
3

어떠신가요? 이제 매직 메소드가 어떤것인지 아시겠죠? 그런데 이런 매직 메소드에 대해서 왜 알아야 할까요? 어차피 백그라운드에서 어떤 연산이 되는지 어떤 메소드가 실행되는지 몰라도 결과값을 같은데 말이죠… 그것은 우리가 만드는 클래스에 매직 메소드를 적용해서 사용하기 위함입니다. 직접 해보도록 하죠.

먼저 간단한 클래스의 인스턴스를 만들어 보겠습니다.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
 
food_1 = Food('아이스크림', 3000)
 
# 인스턴스 출력
print(food_1)
실행 결과
<__main__.Food object at 0x101a07710>

인스턴스를 출력해 보았더니 <__main__.Food object at 0x103cc0ad0>라는 값을 출력했습니다. 사용자에게는 별로 도움이 안되는 정보인 인스턴스의 메모리 주소값을 출력했습니다. 이런 경우에 사용자에게 보다 유익한 정보를 전달하기 위해서 __str__라는 매직 메소드를 사용합니다. 예제를 보시죠.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
 
    def __str__(self):
        return '아이템: {}, 가격: {}'.format(self.name, self.price)
 
food_1 = Food('아이스크림', 3000)
 
# 인스턴스 출력
print(food_1)
실행 결과
아이템: 아이스크림, 가격: 3000

이번에는 두개의 인스턴스를 만들고 두 인스턴스를 통해 몇가지 오퍼레이터를 테스트해 보겠습니다.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
 
food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
 
# food_2가 food_1보다 큰지 확인
print(food_1 < food_2)
실행 결과
# python 2인 경우
False
# python 3인 경우
Traceback (most recent call last):
File "oop-6.py", line 12, in
print(food_1 < food_2)
TypeError: unorderable types: Food() < Food()

파이썬 2의 경우는 False로 출력이 되고 파이썬 3의 경우는 타입에러가 발생합니다. 왜 그럴까요? 그 이유는 파이썬이 우리의 Food 클래스에 대해서 < 연산을 어떻게 해야하는지 모르기 때문입니다. 그런데 False라는 결과값은 어떻게 나왔을까요? 이 결과 값은 단순히 인스턴스의 메모리 주소값을 비교한 결과값입니다. 확인해보죠. *아래의 예제는 파이썬 2에서만 확인됩니다.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
 
food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
 
# food_2가 food_1보다 큰지 확인
print(food_1)
print(food_2)
print(food_1 < food_2)
실행 결과
<__main__.Food object at 0x103cc0b50>
<__main__.Food object at 0x1039b5a90>
False

단순히 메모리 주소값인 103cc0b50와 1039b5a90의 값을 비교한 결과라는 것이 확인 됐습니다. 이번에는 __lt__ 메소드를 수정하여 가격이 비교되도록 만들어보죠.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
 
    def __lt__(self, other):
        if self.price < other.price:
            return True
        else:
            return False
 
food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
food_3 = Food('콜라', 2000)
 
# food_2가 food_1보다 큰지 확인
print(food_1 < food_2)  # 3000 < 5000
print(food_2 < food_3)  # 5000 < 2000
실행 결과
True
False

가격을 비교한 결과가 잘 출력되었습니다. 마지막으로 __add__ 메소드를 사용해보죠.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
 
food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
 
print(food_1 + food_2)
실행 결과
Traceback (most recent call last):
File "oop-6.py", line 11, in
print(food_1 + food_2)
TypeError: unsupported operand type(s) for +: 'Food' and 'Food'

타입에러가 발생했습니다. + 연산을 할 수 없는 타입이라고 하네요. 그럼 __add__ 메소드를 추가해서 가격이 더해지도록 해보죠.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
 
    def __add__(self, other):
        return self.price + other.price
 
food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
 
print(food_1 + food_2)
실행 결과
8000

아이스크림의 값인 3000과 햄버거의 값인 5000의 합인 8000이 정상적으로 출력 되었습니다.

이제 매직 메소드가 뭔지 아셨으니 아래에 여러가지 매직 메소드 예를 보시고 자신이 만든 클래스에는 어떤 매직 메소드를 편리하게 사용할 수 있을지 고민해 보세요. 수고하셨습니다. 해피 코딩~! 😀

각종 매직 메소드

OperatorMethod
+object.add(self, other)
object.sub(self, other)
*object.mul(self, other)
//object.floordiv(self, other)
/object.div(self, other)
%object.mod(self, other)
**object.pow(self, other[, modulo])
>>object.lshift(self, other)
<<object.rshift(self, other)
&object.and(self, other)
^object.xor(self, other)
|object.or(self, other)
OperatorMethod
+=object.iadd(self, other)
-=object.isub(self, other)
*=object.imul(self, other)
/=object.idiv(self, other)
//=object.ifloordiv(self, other)
%=object.imod(self, other)
**=object.ipow(self, other[, modulo])
<<=object.ilshift(self, other)
=object.irshift(self, other)
&=object.iand(self, other)
^=object.ixor(self, other)
|== object.ior(self, other)
OperatorMethod
object.neg(self)
+object.pos(self)
abs()object.abs(self)
~object.invert(self)
complex()object.complex(self)
int()object.int(self)
long()object.long(self)
float()object.float(self)
oct()object.oct(self)
hex()object.hex(self)
OperatorMethod
<object.lt(self, other)
<=object.le(self, other)
==object.eq(self, other)
!=object.ne(self, other)
>=object.ge(self, other)
>object.gt(self, other)