Python — OOP Part 6: Magic Methods
Today we’ll look at magic methods — things we use all the time but don’t always have a clear understanding of, and sometimes don’t even realize we’re using. Here’s what a magic method is.
What is a magic method?
- It’s a special method you can define inside a class that makes the class behave like Python’s built-in types such as
int,str, andlist. - For operators like
+,-,>,<, etc., it overloads each one with a method appropriate for the data type and performs the operation in the background. - Like
__init__or__str__, the method name is wrapped in double underscores ("__") on both sides.
__init__ and __str__, which you always use when creating a class, are the most representative magic methods and the ones we know best. Many people use them all the time but aren’t sure how to pronounce them. Some say “underscore init underscore,” others say “double underscore init double underscore.” The most idiomatic term is “dunder init dunder.”
We don’t call __init__ directly when we instantiate a class, yet we know it runs under the hood. Let’s see an example.
class Dog(object):
def __init__(self, name, age):
print('이름: {}, 나이: {}'.format(name, age))
dog_1 = Dog('Pink', '12')이름: Pink, 나이: 12As you can see in the code above, when the class is instantiated, the __init__ method runs automatically. Now let’s look at another kind of magic method. The + and - operators we use without a second thought are also operators that invoke magic methods. When you run x + y, the __add__ magic method on x is called. In the background, x.__add__(y) is what actually runs. Let’s verify this with an example.
First, let’s create a custom class based on the int type.
# Create a new class with int as its parent
class MyInt(int):
pass
# Create an instance
my_num = MyInt(5)
# Check the type
print(type(my_num)) # => <class '__main__.MyInt'>
# Check whether it's an instance of int
print(isinstance(my_num, int)) # => True
# Check MyInt's base class
print(MyInt.__bases__) # => (<type 'int'>,)<class '__main__.MyInt'>
True
(<class 'int'>,)We’ve confirmed that MyInt is of type int. Now let’s add it together with a regular int.
# Create a new class with int as its parent
class MyInt(int):
pass
# Create an instance
my_num = MyInt(5)
# Run addition
print(my_num + 5) # => 1010It produced the same result as 5 + 5. Let’s check whether my_num really has magic methods.
# Create a new class with int as its parent
class MyInt(int):
pass
# Create an instance
my_num = MyInt(5)
# Check whether it has magic methods
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']It inherited a huge number of magic methods from its parent class int. Now let’s call a magic method directly.
# Create a new class with int as its parent
class MyInt(int):
pass
# Create an instance
my_num = MyInt(5)
# Call the magic method directly
print(my_num.__add__(5)) # => 1010The result is the same as my_num + 5. Now let’s modify the magic method so that the return value is a string instead of an integer.
# Create a new class with int as its parent
class MyInt(int):
# Override __add__
def __add__(self, other):
return '{} 더하기 {} 는 {} 입니다'.format(self.real, other.real, self.real + other.real)
# Create an instance
my_num = MyInt(5)
print(my_num + 5) # => 5 더하기 5 는 10 입니다5 더하기 5 는 10 입니다How about that? It really did return the result of the addition as a string instead of an integer.
Built-in types like int, str, list, and dict come with magic methods that overload various operators in a way appropriate to their type, all for the user’s convenience. Let’s look at a few more examples.
# List addition
print([1,2,3] + [4,5,6])
# Addition via the magic method
print([1,2,3].__add__([4,5,6]))
# Check the length of a dictionary
print(len({'one':1, 'two': 2, 'three': 3}))
# Check the length of a dictionary via the magic method
print({'one':1, 'two': 2, 'three': 3}.__len__())[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
3
3Do you have a sense of what magic methods are now? But why do we need to know about them? After all, the result is the same whether we know what’s running in the background or not. The reason is so we can apply magic methods to the classes we build ourselves. Let’s try it.
First, let’s create an instance of a simple class.
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
food_1 = Food('아이스크림', 3000)
# Print the instance
print(food_1)<__main__.Food object at 0x101a07710>Printing the instance gave us <__main__.Food object at 0x103cc0ad0>. That’s just the instance’s memory address — not useful to anyone. In a case like this, we define the __str__ magic method to return something more meaningful. Let’s look at an example.
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 the instance
print(food_1)아이템: 아이스크림, 가격: 3000Now let’s create two instances and try a few operators on them.
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
# Check whether food_2 is greater than food_1
print(food_1 < food_2)# In Python 2
False
# In Python 3
Traceback (most recent call last):
File "oop-6.py", line 12, in
print(food_1 < food_2)
TypeError: unorderable types: Food() < Food()In Python 2 it prints False, but in Python 3 it raises a TypeError. Why? Because Python doesn’t know how to perform the < operation on our Food class. So how did Python 2 produce False? It simply compared the memory addresses of the two instances. Let’s verify that. The example below only works in Python 2.
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price
food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
# Check whether food_2 is greater than food_1
print(food_1)
print(food_2)
print(food_1 < food_2)<__main__.Food object at 0x103cc0b50>
<__main__.Food object at 0x1039b5a90>
FalseWe’ve confirmed that the result simply compared the memory addresses 103cc0b50 and 1039b5a90. Now let’s modify the __lt__ method so that prices get compared instead.
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)
# Check whether food_2 is greater than food_1
print(food_1 < food_2) # 3000 < 5000
print(food_2 < food_3) # 5000 < 2000True
FalseThe price comparison works correctly. Finally, let’s try the __add__ method.
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'A TypeError was raised — it says the + operation isn’t supported for these types. Let’s add an __add__ method so that the prices get added together.
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)80008000, the sum of the ice cream’s price (3000) and the hamburger’s price (5000), printed correctly.
Now that you know what magic methods are, take a look at the various examples below and think about which magic methods you can use in your own classes to make them more convenient. Nice work. Happy coding!
Various magic methods #
| Operator | Method |
|---|---|
| + | 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) |
| Operator | Method |
|---|---|
| += | 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) |
| Operator | Method |
|---|---|
| – | 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) |
| Operator | Method |
|---|---|
| < | object.lt(self, other) |
| <= | object.le(self, other) |
| == | object.eq(self, other) |
| != | object.ne(self, other) |
| >= | object.ge(self, other) |
| > | object.gt(self, other) |