Python – OOP Part 6. マジックメソッド (Magic Method)

読了 7分

今日は、私たちが常に使ってはいるけれど正確な概念を持っておらず、さらには自分が使っていることそれ自体も知らないマジックメソッドについて学んでいきます。マジックメソッドの定義は次のとおりです。

マジックメソッドとは?

  • クラスの中に定義できるスペシャルメソッドであり、クラスを intstrlist などの Python のビルトイン型(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 足す 510 です

いかがですか?本当に足し算の結果値を整数値ではなく文字列で返しました。

このように、built-in type である intstrlistdict などはユーザの利便性のために、自身の型に合わせて各種オペレータをオーバーロードするマジックメソッドを含んでいます。もう少し例を挙げてみましょう。

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

今度は2つのインスタンスを作って、その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 < 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()

Python 2の場合は False が出力され、Python 3の場合は TypeError が発生します。なぜでしょうか?それは、Python が私たちの Food クラスに対して < 演算をどう処理すべきか分からないからです。 それなら、False という結果値はどうやって出てきたのでしょうか?この結果値は、単にインスタンスのメモリアドレス値を比較した結果値です。確認してみましょう。*下の例題は Python 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'

TypeError が発生しました。+ 演算ができない型だと言っていますね。それでは __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)
X