7
4

More than 5 years have passed since last update.

Python で比較演算子を自動生成する方法とパフォーマンス比較

Posted at

自作クラスを比較可能にする方法

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 のドキュメントに書かれている通り「パフォーマンスが問題になるなら明示的に比較演算子を定義しましょう」ということですね。

7
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4