Pythono の比較演算子ってなに?




整数の比較演算子

Python には比較演算子が定義されていて、おもに if 文で使います。 整数, int 型に対しては ==, !=, <, <=, >, >= と言った比較演算子が定義されています。

f:id:domodomodomo:20190106030141j:plain

◯ サンプルコード


if 1 == 1:
    print('等しい')

if 0 != 1:
    print('等しくない')

if 2 < 3:
    print('小さい')

if 2 <= 3 and 2 <= 2:
    print('小さいか等しい')

if 5 > 3:
    print('大きい')

if 10 >= 7 and 10 >= 10:
    print('大きいか等しい')

◯ 定数は右と左のどっちに書くべき?

ケースバイケースではありますが、基本的には変わらない値、基準となる値を右に持ってくることが多いそうです。 と書籍リーダブルコードに書いてあった気がします。

# 定数を右に書く
for i in range(10):
    if i % 2 == 0:
        print(i)
# 定数を左に書く
for i in range(10):
    if 0 == i % 2:
        print(i)



リストの比較演算子

リストには比較演算子が定義されていて、 あたかも普通の int 型や float 型と同じように2 つのリストを比較することができます。

[0, 1, 2] == [0, 1, 2]  # True
[2, 3, 4] <= [3, 2, 5]  # True



まず良く使うリストの比較演算子について説明して、 そのあとあまり使うことのないリストの比較演算子を説明します。

いやいやリストの大小比較 >, < って何だよって感じですが、 そんなに難しいものではありません。大小比較 >, < は後半でご説明させていただきます。

Python には等号、不等号以外にも、様々な比較演算子があります。

# 良く使う
==
!=
is
is not
in
not in

# 滅多に使わない
<=
<
>=
>





よく使うもの ==, is, in, is not, not in

f:id:domodomodomo:20190108165353j:plain
1. in 比較演算子

要素がリストに含まれているかを判定します。

1 in [1, 2, 3]  # True
4 in [1, 2, 3]  # False
2. not in 比較演算子

ちなみに not in は、それで1つの「比較演算子」になります。 not は単体で and や or と仲間の「ブール演算子」になります。
6.11. ブール演算 (boolean operation)

1 not in [1, 2, 3]  # False
not 1 in [1, 2, 3]  # False
3. == 比較演算子
# 完全に同じなら True
# 123 == 123
[1, 2, 3] == [1, 2, 3]  # True

# 順番が違っていても False
# 123 == 132
[1, 2, 3] == [1, 3, 2]  # False
4. != 比較演算子

== とは結果が反転するだけです。

# 完全に同じなら False
# 123 != 123
[1, 2, 3] != [1, 2, 3]  # False

# 順番が違っていても True
# 123 == 132
[1, 2, 3] != [1, 3, 2]  # True



5. is 比較演算子

同じオブジェクトなら True になります。


5.1. 問題 1
# 1) 
a = [1, 2, 3]
b = a
c = [1, 2, 3]


# 問題 1. True でしょうか False でしょうか
a is b

# 問題 2. True でしょうか False でしょうか
a is c


5.2. 解答 1

何故なら、変数 a と 変数 b には、同じオブジェクトが代入されているから。

>>> # 問題 1. True でしょうか False でしょうか
>>> a is b
True
>>>


何故なら、変数 a と 変数 c には、別のオブジェクトが代入されているから

>>> # 問題 2. True でしょうか False でしょうか
>>> a is c  
False
>>> 


5.3. 問題 2

「値」を理解するにあたって == 比較演算子と is 比較演算子を区別することは大切です。 代入はコピーではありません。

a = [1, 2, 3]
b = a
c = [1, 2, 3]

# 変数 a に代入された
# オブジェクトの要素を1つ pop すると
a.pop()


# 問題 1.
#     1), 2) どちらが出力されるでしょうか?
b
"""
1) [1, 2, 3]
2) [1, 2]
"""


# 問題 2.
#     1), 2) どちらが出力されるでしょうか?
c
"""
1) [1, 2, 3]
2) [1, 2]
"""


5.4. 解答 2
>>> # 問題 1.
... #     1), 2) どちらが出力されるでしょうか?
... b
[1, 2]
>>> 
>>> # 問題 2.
... #     1), 2) どちらが出力されるでしょうか?
... c
[1, 2, 3]
>>> 




この問題は簡単そうに見えて実は結構、複雑です。 まず代入について復習して、次に is と == の違いを区別する必要があります。

Step 1. Python の変数と属性、代入とコピー
Step 2. Python の is と == の動作と違い

6. is not 比較演算子

ちなみに is not は、それで1つの「比較演算子」になります。 not は単体で and や or と仲間の「ブール演算子」になります(大事なことなので2回言いました的な...)。
6.11. ブール演算 (boolean operation)

[1, 2, 3] is not [1, 2, 3]  # True
not [1, 2, 3] is [1, 2, 3]  # True




7. リテラルインスタンス生成のタイミング

細かすぎて本当にどうでもいいのですが、 リテラル に 関するインスタンス生成の挙動は "実装に依存" します。例を2つ挙げます。

まず tuple の is 演算子は、list の時とは同じ結果にはなりません。

(1, 2, 3) is not (1, 2, 3)  # False
not (1, 2, 3) is (1, 2, 3)  # False


次に CPython では int は - 5 以上 256 以下の値とそれ以外で挙動が異なります。 例えば - 5 以上 256 以下では a == b なら a is b となります。 それ以外では a == b であっても a is not b になります。

>>> # - 5 以上 256 以下以外の整数のリストが返されます。
>>> [a for a, b in zip(range(-10, 262), range(-10, 262)) if a is not b]
[-10, -9, -8, -7, -6, 257, 258, 259, 260, 261]
>>>


"実装に依存" と言うのは、例えば大抵の人が使っている Python は C 言語で書かれた CPython と言う実装を使っています。他にも Java で書かれた Jython と言うのもありました。

例えば、 a = 1; b = 1 とすると、 a と b は値 1 を持つ同じオブジェクトを参照するときもあるし、 そうでないときもあります。これは "実装に依存" します。
3.1. オブジェクト、値、および型















後編


この記事では (1) 整数 int , (2) リスト list , そして最後に (3) ユーザ定義クラスについて定義された比較演算子を、それぞれご紹介しています。 この記事はプログラミング言語における とは何かについて考えるために書きました。 この記事は Python における値ってなに? のサブ記事です。

Python を習いたての方には「プログラミング言語における とは何か」や 「自分自身で比較演算子を定義してみる」という話は、 あまりお役に立てないのではないかと思います。

直接業務に影響があるわけではないので、 もしご興味がありましたら、 こんなのもあるんだなーくらいに押さえておいていただければと思います。

オブジェクトのPython ではやや抽象的な概念です: 例えば、オブジェクトの値にアクセスする正統な方法はありません。 また、その全てのデータ属性から構成されるなどの特定の方法で、オブジェクトの値を構築する必要性もありません。

比較演算子は、オブジェクトの とは何かについての特定の概念を実装しています。 この比較の実装によって、間接的にオブジェクトの を定義している 考えることもできます。
6.10.1. 値の比較








あまり使わないもの <=, <, >=, >

リストの比較演算子の考え方は...



各要素は桁を表現しています。






リストの比較演算子とか in と == くらいしか使ったことがないので、>, < の重要性は低いかなと感じています。 なんでこんな実装のされ方されているんだろう... こんなのもあるんだなくらいに押さえておいていただければと思います。

5. <= 比較演算子
# 123 <= 123
[1, 2, 3] <= [1, 2, 3]  # True

# 123 <= 124
[1, 2, 3] <= [1, 2, 4]  # True

# 423 <= 128
[4, 2, 3] <= [1, 2, 8]  # False

# 要素数が足りない方は -無限大が代入される
# 123(-無限大) < 123(-10)
[1, 2, 3] <= [1, 2, 3, -10]  # True

# 123(-10) <= 123(-無限大)
[1, 2, 3, -10] <= [1, 2, 3]  # False


リストの先頭の要素から順番に比較します。 もし2つの要素が同じ数なら次の要素を比較します。 シーケンス番号の小さい要素の大小比較が優先されます。

まず、最初の二つの要素を比較し、 その値が等しくなければその時点で比較結果が決まります。 等しければ次の二つの要素を比較し、 以降シーケンスの要素が尽きるまで続けます。
5.8. シーケンスとその他の型の比較



これと等価です。

from itertools import zip_longest
inf = float('inf')

# <=
def less_equal(lst1, lst2):
    for e1, e2 in zip_longest(lst1, lst2, fillvalue=-inf):
        if e1 == e2:
            continue
        elif e1 < e2:
            return True
        elif e1 > e2:
            return False
    return True


zip_longest は短い方のリストの要素を fillvalue で埋めてくれます。 そんなに重要な関数ではないので、覚える必要はありません。

# zip
#     短い方で止まる。
for e1, e2 in zip([1, 2], [1]):
    e1, e2

# (1, 1)
# zip_longest
#     仮引数 fillvalue で指定された値で埋めてくれる。
from itertools import zip_longest
inf = float('inf')

for e1, e2 in zip_longest([1, 2], [1], fillvalue=-inf):
    e1, e2

# (1, 1)
# (2, -inf)
6. < 比較演算子

これと等価です。

# <
def less_than(lst1, lst2):
    # return <= and not ==
    return less_equal(lst1, lst2) and not equal(lst1, lst2)

初等教育では <、> の「(等号を含まない)不等号」を先に導入するが、
数学一般においては等号を含めた
「≤」を先に定義する方が自然な場合が多く、
「<」のほうが「a ≤ b かつ a ≠ b」として定義される。
不等号 | Wikipedia

7. >= 比較演算子

省略

8. > 比較演算子

省略










ユーザ定義クラスの比較演算子

◯ 比較演算子を作ってみよう

特殊メソッド __eq__, __lt__, __le__ を定義することで比較演算子を定義することができます。 __eq__ は equal, __lt__ は less than, __le__ は less equal の略称と思われます。

1. 量の比較

さっそく作ってみました。

# 対話モード >>> にコピペで実行できます。
class Norm(object):
    def __init__(self, a):
        self.a = a
    
    def __eq__(self, other):
        return self.a == other.a
    
    def __lt__(self, other):
        return self.a < other.a
    
    def __le__(sef, other):
        return any((
            self == other,
            self < other,
        ))

assert Norm(3) == Norm(3)
assert Norm(3) <  Norm(4)
assert Norm(3) <= Norm(4)
assert Norm(3) <= Norm(4)


このあたりの挙動については、公式ドキュメントの以下の部分に記載されています。


any 関数 は タプルまたはリストなどのイテラブルの要素のどれか1つでも True であれば True を返してくれます。

any 関数とは別に all 関数 は タプルまたはリストなどのイテラブルの要素のすべてが True であれば True を返してくれます。

ここから先でご紹介するコードと上のコードを比較する時に and や or でつなぐよりも違いを明確にしやすいので採用しました。 Guido いち押しの関数です。

さあ any 関数と all 関数を、組み込み関数に追加しよう。
Let's add any() and all() to the standard builtins,
The fate of reduce() in Python 3000


assert 文 は True であれば何もしませんが、False であれば AssertionError を投げます。 assert 文は、テストのための文です。


◯ 比較演算子の概念を量から領域に拡張してみよう

Wikipedia の「「≤」を先に定義する方が自然な場合が多く...」というのは、 たしか包含関係を考える場合は、その方が良かった気がします。

< と = を別の概念です。 そのため < と = をそれぞれ別々に説明してあげてから、 ≤ は < もしくは = ですよ、と説明した方がわかりやすいです。

しかし比較を領域に拡大すると、ちょっと話が変わってきます。 < と = をそれぞれ別々に定義すると、すこし面倒になってしまうのです。

頑張って図にしてみました。 字の汚さ、なんとかならないかな...

f:id:domodomodomo:20181218150639p:plain f:id:domodomodomo:20181218150646p:plain f:id:domodomodomo:20181218151347p:plain

2. 領域の比較

いま「<」から定義したクラス Linea があります。 ちなみに Linea はラテン語で線という意味です (alc 調べ)

# 対話モード >>> にコピペで実行できます。
class Linea(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b
    
    def __eq__(self, other):
        return all((
            self.a == other.a,
            self.b == other.b,
        ))
    
    def __lt__(self, other):
        return any((
            all((
                other.a < self.a < other.b,
                self.b == other.b,
            )),
            all((
                other.a == self.a,
                other.a < self.b < other.b,
            )),
            all((
                other.a < self.a < other.b,
                other.a < self.b < other.b,
            )),
        ))
    
    def __le__(self, other):
        return any((
            self == other,
            self < other,
        ))


assert Linea(3, 4) < Linea(1, 6)
assert Linea(4, 9) < Linea(4, 10)
assert Linea(4, 9) < Linea(3, 9)
assert not(Linea(7, 8) < Linea(7, 8))


Python では other.a < self.a < other.b こんな感じで、比較演算子を繋げて書くことができます。

3. 問題

なんだか上のコードはちょっと汚いですね。 これを 「≤」から先に定義したクラス Line に書き直してみましょう。

# コピペでは動きません。
class Line(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __eq__(self, other):
        return all((
            self.a == other.a,
            self.b == other.b
        ))

    def __le__(self, other):
        return all((
            other.a <= self.a <= other.b,
            other.a <= self.b <= other.b,
        ))

    def __lt__(self, other):
        return all((
            ...  # ここを書き換えてください。
        ))


assert Line(3, 4) < Line(1, 6)
assert Line(4, 9) < Line(4, 10)
assert Line(4, 9) < Line(3, 9)
assert not(Line(7, 8) < Line(7, 8))


答えはこちらから、どうぞ
comparison.py - GitHubGist





list も値として大小関係 >, <, 等号 == が定義されていることがわかりました。 では、プログラミング言語における とは、そもそも一体何物なのでしょうか?