Pythonの所属検査演算(in)について

Pythonでは、ある要素が集合や配列に存在しているかどうか、inという式を使って判定できます。この度改めてドキュメントを読んでみたのと、配列の配列などちょっと特殊な用途について挙動を調べたのでまとめておきます。

ドキュメントはこちらです。in のことは所属検査演算と呼ぶようです。URLから推測すると英語名は、 membership test operations のようですね。
参考: 6.10.2. 所属検査演算 式 (expression) — Python 3.9.4 ドキュメント

演算子 in および not in は所属関係を調べます。とある通りで、
x in s は xがsの要素だったらTrue、そうでない場合はFalseを返します。not in は in の否定です。この記事ではこの s を色々変えながら挙動を見ていきましょう。

配列や集合、タプルに対する挙動

まずは一番基本的な配列や集合に対する挙動です。まず配列についてみていきますが、これは特に説明することもなく、配列sの要素のどれかとxが一致すれば x in s はTrueになります。

list_data = [1, 2, 3, 4, 5]
print(3 in list_data)
# True
print(8 in list_data)
# False
print(2 not in list_data)
# False

集合やタプルの場合も同様です。タプルはこれ以降コード例を省略しますが配列と同じように動きます。

set_data = {1, 2, 3, 4, 5}
print(3 in set_data)
# True
print(8 in set_data)
# False
print(2 not in set_data)
# False

tuple_data = (1, 2, 3, 4, 5)
print(3 in tuple_data)
# True
print(8 in tuple_data)
# False
print(2 not in tuple_data)
# False

ここで、少し注意が必要なのは、 None についても機能するということです。SQLの挙動に慣れていると、NULLが絡むとTrueでもFalseでもなくNULLが返ってくるので、Noneが絡むとNoneが返ってくるような気がしてしまいますが、None == None とみなすようでSQLとは違った動きになります。

print(None in [1, 2, None, 3])
# True
print(None in [1, 2, 4, 5])
# False
print(None in {1, 2, None, 3})
# True

もっと言うと、numpyの nan についても使えます。np.nan == np.nan は False なのでこれは不思議な挙動です。

import numpy as np


print(np.nan == np.nan)
# False
print(np.nan in [1, 2, 3, np.nan])
# True

配列の部分列や、集合の部分集合については使えません。 xがsの部分列や部分集合の場合も
x in s はFalseが返ってきます。
部分集合のジャッジをしたい場合は不等号が使えるのでそちらを使いましょう。

# 部分列はFalseになる
print([2, 3] in [1, 2, 3, 4])
# False

# 部分集合もFalseになる
print({2, 3} in {1, 2, 3, 4})
# False

# 部分集合は不等号で判定できる。
print({2, 3} <= {1, 2, 3, 4})
# True

次に、配列の配列について検証しましたが、なんとこれが正常に動作します。hashableな形でないとダメだと思い込んでいたので意外でした。

# 配列の配列も動く
print([1, 2] in [[1, 2], [3, 4], [5, 6]])
# True

# もちろん、含まない場合はFalse
print([2, 3] in [[1, 2], [3, 4], [5, 6]])
# False

# 要素の要素についてはFalseになる
print(3 in [[1, 2], [3, 4], [5, 6]])
# False

では、集合の集合は?と思ったのですが、集合(set)はhashableなものしか要素に持てないので、集合の集合自体作れません。なので気にしなくて大丈夫です。
タプルのタプルは、当然配列と同じように動作してくれます。

辞書(dict)に対する挙動

辞書sに対して、x in s を使うと、xが辞書sのキーに含まれていた場合にTrue、含まれていない場合にFalseを返してくれます。キーではなく値の中にあるかどうかを知りたいってばあいはvalues()、キーだけでなくキーと値のペアで含まれているかどうかを知りたいって場合はitems()をそれぞれ併用しましょう。

dict_data = {
    "apple": "りんご",
    "orange": "みかん",
    "banana": "バナナ" 
}

# キーの中に一致するものがあればTrue
print("apple" in dict_data)
# True

# keys()メソッドでキーの一覧を取得して判定しても挙動は同じ
print("apple" in dict_data.keys())
# True

# 値の中に一致するものがあったとしてもこれはFalse
print("みかん" in dict_data)
# False

# 値の中に一致するものがあるかどうか見る場合は、values()メソッドを使う
print("みかん" in dict_data.values())
# True

# キーと値のペアで判定をしたい場合はitems()メソッドを使う。
print(("apple", "りんご") in dict_data.items())
# True

# キーと値がそれぞれ存在していても組み合わせが違うとFalseになる
print(("apple", "バナナ") in dict_data.items())
# False

文字列に対する挙動

文字列sと、文字x対してinを使うと、xがsに含まれている場合にTrueを返してきます。これだけだと、配列と要素の場合と同じように見えるのですが、実は文字列の独特の挙動として、文字列xが文字列sの部分文字列の場合もTrueを返してくれると言うものがあります。便利ですね。実装としては、 x in y は y.find(x) != -1 と等価になっているそうです。

# 文字が含まれていればTrue
print("c" in "abcde" )
# True

# 部分列であればTrue
print("bcd" in "abcde" )
# True

# 個々の文字が含まれていても順番が違うとFalse
print("ba" in "abcde" )
# False

文字列についてはもう一つ注意があって、空文字列は他の任意の文字列の部分文字列とみなされます。要するに次の式はどちらもTrueです。

print("" in "abcde")
# True

print("" in "")
# True

その他の型 (ユーザー定義型)における in

これまで、Pythonの基本的な各型における所属検査演算子の使い方を見てきましたが、各ライブライで実装されているようなクラスにおいても in は使えますし、自分で実装するクラスにおいても、inの振る舞いを定義して実装することができます。
その方法は、 class において、 __contains__() メソッドを実装することです。

__contains__() メソッドが実装されているクラスにおいては、
x in y は、 y.__contains__(x) が Trueを返す場合にTrueになり、そうでない場合にFalseになります。

実験したところ、__contains__が、if文でTrueと判定されるようなもの、(空白ではない文字列、0ではない数値、空ではない配列など)を返した場合は Trueになり、if文でFalseと判定されるようなもの(False,None,0など)を返した場合はFalseになるので、 __contains__ と in の結果が一致する、と言うわけではないようです。

大変奇妙な例で恐縮ですが実験したのが次の結果です。

class myclass():
    def __contains__(self, y):
        return "含みます"


mc = myclass()

# __contains__ メソッドを呼び出すとメソッドの結果がそのまま返される
print(mc.__contains__("a"))
# 含みます

# in だと True か False に変換される
print("a" in mc)
# True

__contains__ が実装されていないが __iter__ が実装されているクラスの場合(要するにイテレーター)の場合は、反復の途中で x に等しい要素が登場した場合に Trueになります。
また変な例なのですが、__iter__(と、セットで使う__next__)だけ実装したようなクラスを作ったのでそれで実験します。このクラスは[1,2,3,4,5]を順番に返します。

class myclass2():
    def __init__(self):
        self._i = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._i == 5:
            raise StopIteration()
        self._i += 1
        return self._i


mc2 = myclass2()
print(2 in mc2)
# True
mc2 = myclass2()
print(6 in mc2)
# False

__contains__も__iter__も実装されていない場合は、最後に、__getitem__()が試されます。
__getitem__() は 辞書型のように[]でアクセスしてきた時の挙動を定義する特殊メソッドですね。これは単に x == y[i] となる iが見つかれば True, そうでない場合はFalseとなるようです。
これもまた変な例ですが、__getitem__だけ実装されたクラスで実験しました。

class myclass3():
    def __getitem__(self, i):
        # 無限ループを避けるためにiが大きくなったらエラーにする
        if i >= 100:
            raise
        return i**2


mc3 = myclass3()

# 平方数ならTrue、 mc3[4] == 16 だから。
print(16 in mc3)
# True

# 平方数でない場合はエラーになるまで探し続ける
print(18 in mc3)
# RuntimeError: No active exception to reraise

改めてドキュメントを読んでみて色々試した結果、それなりに理解が深まった気がします。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です