自作クラスを比較可能にする方法
Python では自作クラスに比較演算子を使えるようにするには、以下のように __eq__
, __ne__
, __lt__
, __le__
, __gt__
, __ge__
メソッドを定義します。
class Value(object):
def __init__(self, value):
self._value = value
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value == other._value
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value != other._value
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value < other._value
def __le__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value <= other._value
def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value > other._value
def __ge__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value >= other._value
この Value クラスは次のように使うことができます。
Value(1) == Value(2) # => False
Value(1) != Value(2) # => True
Value(1) < Value(2) # => True
Value(1) <= Value(2) # => True
Value(1) > Value(2) # => False
Value(1) >= Value(2) # => False
functools.total_ordering で楽をする方法
標準ライブラリの functools には total_ordering というデコレータが含まれています。
total_ordering を使うと 2 つの比較演算子 (__eq__
と __lt__
, __le__
, __gt__
, __ge__
の中から 1 つ) を定義しておけば、残りの比較演算子を自動生成することができます。
from functools import total_ordering
@total_ordering
class Value(object):
def __init__(self, value):
self._value = value
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value == other._value
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value < other._value
6 つの比較演算子を全て自分で定義する場合と比較するとコーディングは楽になります。
functools.total_ordering について思うこと
total_ordering を使うには 2 つの比較演算子を定義しなければなりません。それに対して思うことが 2 つあります。一つ目は「__eq__
に加えてもう一つどの比較演算子を定義しようかと迷いたくない」、二つ目は「大小比較を行うメソッドだけ定義すれば比較演算子は全て自動生成してほしい」です。
具体的には以下のように書けると嬉しいです。
@comparable
class Value(object):
def __init__(self, value):
self._value = value
# self < other の場合は負数,
# self > other の場合は正数,
# self == other の場合は 0 を返す.
def compare(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value - other._value
もしくはメタクラスで実現されていても良いと思います。
class Value(object, metaclass=Comparable):
def __init__(self, value):
self._value = value
# self < other の場合は負数,
# self > other の場合は正数,
# self == other の場合は 0 を返す.
def compare(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value - other._value
大小比較メソッドから比較演算子を自動生成するデコレータを作る
以下ように実現できます。
def comparable(target_class):
target_class.__eq__ = lambda self, other: self.compare(other) == 0
target_class.__ne__ = lambda self, other: self.compare(other) != 0
target_class.__lt__ = lambda self, other: self.compare(other) < 0
target_class.__le__ = lambda self, other: self.compare(other) <= 0
target_class.__gt__ = lambda self, other: self.compare(other) > 0
target_class.__ge__ = lambda self, other: self.compare(other) >= 0
return target_class
使い方は前述の通りですが、このようになります。
@comparable
class Value(object):
def __init__(self, value):
self._value = value
# self < other の場合は負数,
# self > other の場合は正数,
# self == other の場合は 0 を返す.
def compare(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value - other._value
大小比較メソッドから比較演算子を自動生成するメタクラスを作る
以下ように実現できます。
class Comparable(type):
def __new__(self, name, bases, namespace, **kwargs):
namespace["__eq__"] = lambda self, other: self.compare(other) == 0
namespace["__ne__"] = lambda self, other: self.compare(other) != 0
namespace["__lt__"] = lambda self, other: self.compare(other) < 0
namespace["__le__"] = lambda self, other: self.compare(other) <= 0
namespace["__gt__"] = lambda self, other: self.compare(other) > 0
namespace["__ge__"] = lambda self, other: self.compare(other) >= 0
return super(Comparable, self).__new__(self, name, bases, namespace, **kwargs)
使い方は前述の通りですが、このようになります。
class Value(object, metaclass=Comparable):
def __init__(self, value):
self._value = value
# self < other の場合は負数,
# self > other の場合は正数,
# self == other の場合は 0 を返す.
def compare(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value - other._value
パフォーマンスを比較する
ここまでで自作クラスを比較可能にする方法がいくつかあることが分かりました。
- 比較演算子を全て定義する方法
- functools.total_ordering を使う方法
- 自作した comparable デコレータを使う方法
- 自作した Comparable メタクラスを使う方法
それぞれのパフォーマンスを比較します。
from functools import total_ordering
import six
# 全ての比較演算子を明示的に定義する
class Value1(object):
def __init__(self, value):
self._value = value
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value == other._value
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value != other._value
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value < other._value
def __le__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value <= other._value
def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value > other._value
def __ge__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value >= other._value
# functools.total_ordering を使う
@total_ordering
class Value2(object):
def __init__(self, value):
self._value = value
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value == other._value
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value < other._value
# 自作した comparable を使う
@comparable
class Value3(object):
def __init__(self, value):
self._value = value
def compare(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value - other._value
# 自作した Comparable メタクラスを使う
class Value4(object, six.with_metaclass(Comparable, object)):
def __init__(self, value):
self._value = value
def compare(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._value - other._value
def measure(target_class, n=100000):
start_time = time()
for i in range(n):
target_class(random()) == target_class(random())
target_class(random()) != target_class(random())
target_class(random()) < target_class(random())
target_class(random()) <= target_class(random())
target_class(random()) > target_class(random())
target_class(random()) >= target_class(random())
return time() - start_time
def main():
print("---- warm up")
n = 10000
measure(Value1, n)
measure(Value2, n)
measure(Value3, n)
measure(Value4, n)
print("---- measure")
n = 1000000
seconds1 = measure(Value1, n)
seconds2 = measure(Value2, n)
seconds3 = measure(Value3, n)
seconds4 = measure(Value4, n)
print("---- report")
print("Explicit defined ... {} sec ({} %)".format(seconds1, seconds1 / seconds1 * 100))
print("@total_ordering ... {} sec ({} %)".format(seconds2, seconds2 / seconds1 * 100))
print("@comparable ... {} sec ({} %)".format(seconds3, seconds3 / seconds1 * 100))
print("Comparable ... {} sec ({} %)".format(seconds4, seconds4 / seconds1 * 100))
if __name__ == "__main__":
main()
比較は Python2 と Python3 それぞれ、3 回ずつ実行しました。
まずは Python2 で比較します。独自メタクラスが速く、次いで total_ordering, 独自デコレータの順に遅いです。
# Python2 (1 回目)
---- warm up
---- measure
---- report
Explicit defined ... 7.17792105675 sec (100.0 %)
@total_ordering ... 8.72567009926 sec (121.562636734 %)
@comparable ... 8.9874830246 sec (125.21011242 %)
Comparable ... 8.23461413383 sec (114.721436315 %)
# Python2 (2 回目)
---- warm up
---- measure
---- report
Explicit defined ... 7.14997816086 sec (100.0 %)
@total_ordering ... 8.72958898544 sec (122.092526565 %)
@comparable ... 8.9977478981 sec (125.843012324 %)
Comparable ... 8.26385092735 sec (115.578687675 %)
# Python2 (3 回目)
---- warm up
---- measure
---- report
Explicit defined ... 7.16386914253 sec (100.0 %)
@total_ordering ... 8.74700498581 sec (122.098893933 %)
@comparable ... 9.00440502167 sec (125.691924887 %)
Comparable ... 8.23617601395 sec (114.968264357 %)
次に Python3 で比較します。独自メタクラスと total_ordering が同じくらい速く、独自デコレータはそれより遅いです。
# Python3 (1 回目)
---- warm up
---- measure
---- report
Explicit defined ... 7.0163891315460205 sec (100.0 %)
@total_ordering ... 8.265613794326782 sec (117.80438113336942 %)
@comparable ... 9.118405103683472 sec (129.9586572627035 %)
Comparable ... 8.322255849838257 sec (118.61166326167682 %)
# Python3 (2 回目)
---- warm up
---- measure
---- report
Explicit defined ... 7.011993169784546 sec (100.0 %)
@total_ordering ... 8.281635046005249 sec (118.10671866726467 %)
@comparable ... 9.249107122421265 sec (131.90410912373233 %)
Comparable ... 8.478234052658081 sec (120.91047220627267 %)
# Python3 (3 回目)
---- warm up
---- measure
---- report
Explicit defined ... 6.999313116073608 sec (100.0 %)
@total_ordering ... 8.270577192306519 sec (118.16269761262015 %)
@comparable ... 9.161770343780518 sec (130.8952777486254 %)
Comparable ... 8.345261096954346 sec (119.22971523862576 %)
まとめ
以下のことがわかりました。
- Python2 だと独自メタクラスが速く、次いで total_ordering, 独自デコレータの順に遅い。
- Python3 だと独自メタクラスと total_ordering が同じくらい速く、独自デコレータはそれより遅い。
実現方法による違いはありますが、比較演算子を自動生成する場合は 15-30% くらい遅くなるという肌感覚を持っておいた方が良さそうです。
これは比較演算子を自動生成するべきではないということではなく、Python のドキュメントに書かれている通り「パフォーマンスが問題になるなら明示的に比較演算子を定義しましょう」ということですね。