“ is”整数で演算子が予期しない動作をする
-
08-07-2019 - |
質問
次のPythonで予期しない動作が発生する理由
>>> a = 256
>>> b = 256
>>> a is b
True # This is an expected result
>>> a = 257
>>> b = 257
>>> a is b
False # What happened here? Why is this False?
>>> 257 is 257
True # Yet the literal numbers compare properly
Python 2.5.2を使用しています。 Pythonのいくつかの異なるバージョンを試してみると、Python 2.3.3は上記の99〜100の動作を示しているようです。
上記に基づいて、Pythonが内部的に実装され、「小さな」が整数はより大きな整数とは異なる方法で保存され、 is
演算子は違いを判別できます。なぜ漏れやすい抽象化なのか? 2つの任意のオブジェクトを比較して、それらが数値であるかどうかが事前にわからないときに同じであるかどうかを確認するより良い方法は何ですか?
解決
これを見てください:
>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828
編集:これは、Python 2のドキュメント" Plain Integerで見つけたものです。オブジェクト" ( Python 3 でも同じです):
現在の実装では、 すべての整数オブジェクトの配列 -5〜256の整数 その範囲でintを作成します 実際に参照を取得するだけです 既存のオブジェクト。だからそれは 1の値を変更できます。 Pythonの動作が疑われる このケースは未定義です。 :-)
他のヒント
Pythonの“は”演算子は整数で予期しない動作をしますか?
要約-強調する: is
を使用して整数を比較しないでください。
これは、期待するべき動作ではありません。
代わりに、 ==
と!=
を使用して、それぞれ等価性と不等価性を比較します。例:
>>> a = 1000
>>> a == 1000 # Test integers like this,
True
>>> a != 5000 # or this!
True
>>> a is 1000 # Don't do this! - Don't use `is` to test integers!!
False
説明
これを知るには、次のことを知る必要があります。
まず、は
は何をしますか?これは比較演算子です。 ドキュメントから:
演算子
is
およびis not
は、オブジェクトIDのテストです:x is y
はtrue xとyが同じオブジェクトである場合にのみ。xはy
ではない 逆真理値。
したがって、以下は同等です。
>>> a is b
>>> id(a) == id(b)
ドキュメントから:
id
“ identity”を返しますオブジェクトの。これは整数(またはlong このオブジェクトに対して一意で一定であることが保証されている整数) その存続期間中。重複しないライフタイムを持つ2つのオブジェクトは、 同じid()
値を持ちます。
CPython(Pythonのリファレンス実装)のオブジェクトのIDがメモリ内の場所であるという事実は、実装の詳細であることに注意してください。 Pythonの他の実装(JythonやIronPythonなど)では、 id
の異なる実装を簡単に持つことができます。
では、 is
のユースケースは何ですか? PEP8の説明:
None
などのシングルトンとの比較は、常にis
またはではなく
、等号演算子ではありません。
質問
次の質問を(コードを使用して)お願いします:
次のPythonで予期しない動作が発生するのはなぜですか
>>> a = 256 >>> b = 256 >>> a is b True # This is an expected result
期待される結果ではありません 。なぜそれが期待されているのですか?これは、 a
と b
の両方によって参照される 256
の値の整数が、整数の同じインスタンスであることを意味します。整数はPythonでは不変であるため、変更できません。これはコードに影響を与えません。それは期待されるべきではありません。これは単に実装の詳細です。
しかし、おそらく、256に等しい値を指定するたびに、メモリに新しい個別のインスタンスが存在しないことを嬉しく思うはずです。
>>> a = 257 >>> b = 257 >>> a is b False # What happened here? Why is this False?
メモリ内に 257
の値を持つ整数の2つの個別のインスタンスがあるように見えます。整数は不変なので、これはメモリを浪費します。無駄にしないようにしましょう。私たちはおそらくそうではありません。ただし、この動作は保証されていません。
>>> 257 is 257 True # Yet the literal numbers compare properly
まあ、これはあなたのPythonの特定の実装がスマートになろうとしているようで、必要がない限りメモリに冗長な値の整数を作成していません。 Pythonのリファレント実装(CPython)を使用していることを示しているようです。 CPythonに適しています。
(ルックアップにコストがかかるように)CPythonがグローバルにこれを安価に行うことができれば、さらに良いかもしれません。おそらく別の実装かもしれません。
ただし、コードへの影響に関しては、整数が整数の特定のインスタンスであるかどうかは気にする必要はありません。そのインスタンスの値が何であるかだけに注意する必要があり、そのために通常の比較演算子、つまり ==
を使用します。
is
の機能
is
は、2つの id
が
2つのものが等しいか、同じオブジェクトかを確認するかどうかによって異なります。
is
は、等しいだけでなく、同じオブジェクトであるかどうかを確認します。小さなintはおそらくスペース効率のために同じメモリ位置を指している
In [29]: a = 3
In [30]: b = 3
In [31]: id(a)
Out[31]: 500729144
In [32]: id(b)
Out[32]: 500729144
任意のオブジェクトの同等性を比較するには、 ==
を使用する必要があります。 __ eq __
および __ ne __
属性で動作を指定できます。
遅れましたが、回答にソースが必要ですか? *
CPythonの良い点は、このソースを実際に見ることができることです。ここでは、 3.5
リリースのリンクを使用します。対応する 2.x
を見つけるのは簡単です。
CPythonでは、新しい int
オブジェクトの作成を処理する C-API
関数は PyLong_FromLong(long v)
。この関数の説明は次のとおりです。
現在の実装では、-5〜256のすべての整数に対して整数オブジェクトの配列が保持されます。その範囲でintを作成すると、実際には既存のオブジェクトへの参照が返されます。したがって、1の値を変更できるはずです。この場合のPythonの動作は未定義だと思います。 :-)
あなたのことは知らないが、私はこれを見て考えます:その配列を見つけよう!
CPythonを実装する C
コードをいじっていなかった場合は、すべき、すべてが整理されていて読みやすいです。この場合、 Objects /
サブディレクトリを調べる必要があります。 メインソースコードディレクトリツリー。
PyLong_FromLong
は long
オブジェクトを処理するので、 longobject.c
。中を見た後、物事は混chaとしていると思うかもしれません。恐れはありませんが、探している関数は line 230
がチェックアウトするのを待っています。これは小さな関数なので、本文(宣言を除く)はここに簡単に貼り付けられます:
PyObject *
PyLong_FromLong(long ival)
{
// omitting declarations
CHECK_SMALL_INT(ival);
if (ival < 0) {
/* negate: cant write this as abs_ival = -ival since that
invokes undefined behaviour when ival is LONG_MIN */
abs_ival = 0U-(unsigned long)ival;
sign = -1;
}
else {
abs_ival = (unsigned long)ival;
}
/* Fast path for single-digit ints */
if (!(abs_ival >> PyLong_SHIFT)) {
v = _PyLong_New(1);
if (v) {
Py_SIZE(v) = sign;
v->ob_digit[0] = Py_SAFE_DOWNCAST(
abs_ival, unsigned long, digit);
}
return (PyObject*)v;
}
現在、 C
master-code-haxxorz ではありませんが、愚かでもありません。 CHECK_SMALL_INT(ival);
すべて私たちを誘惑的に覗きます。これには何か関係があることがわかります。 チェックしてみましょう:
#define CHECK_SMALL_INT(ival) \
do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
return get_small_int((sdigit)ival); \
} while(0)
つまり、値 ival
が条件を満たしている場合、関数 get_small_int
を呼び出すマクロです。
if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS)
では、 NSMALLNEGINTS
および NSMALLPOSINTS
とは何ですか?マクロを推測した場合、それはそれほど難しい質問ではなかったため、何も得られません。 とにかく、ここにあります :
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
つまり、条件は if(-5&lt; = ival&amp;&amp; ival&lt; 257)
です。 get_small_int
を呼び出します。
他に行く場所はありませんが、を見て旅を続けます get_small_int
のすべての栄光(まあ、それはおもしろいものだったので、私たちはただその本体を見ていきます):
PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
さて、 PyObject
を宣言し、前の条件が保持されていることを表明し、割り当てを実行します:
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
small_ints
は、私たちが探している配列によく似ています。 いまいましいドキュメントを読むことができたので、ずっと知っています! :
/* Small integers are preallocated in this array so that they
can be shared.
The integers that are preallocated are those in the range
-NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
そうです、これが私たちのやつです。 [NSMALLNEGINTS、NSMALLPOSINTS)
の範囲で新しい int
を作成する場合、事前に割り当てられた既存のオブジェクトへの参照を取得します。
参照以来
ソースファイル intobject.cでチェックインできるように 、Pythonは効率のために小さな整数をキャッシュします。小さな整数への参照を作成するたびに、新しいオブジェクトではなく、キャッシュされた小さな整数を参照しています。 257は小さな整数ではないため、異なるオブジェクトとして計算されます。
そのためには、 ==
を使用することをお勧めします。
あなたの仮説は正しいと思います。 id
(オブジェクトのID)を試してください:
In [1]: id(255)
Out[1]: 146349024
In [2]: id(255)
Out[2]: 146349024
In [3]: id(257)
Out[3]: 146802752
In [4]: id(257)
Out[4]: 148993740
In [5]: a=255
In [6]: b=255
In [7]: c=257
In [8]: d=257
In [9]: id(a), id(b), id(c), id(d)
Out[9]: (146349024, 146349024, 146783024, 146804020)
数字&lt; = 255
はリテラルとして扱われ、上記のものはすべて異なるように扱われているようです!
int、strings、datetimesなどの不変の値オブジェクトの場合、オブジェクトIDは特に役立ちません。平等について考える方が良いです。アイデンティティは本質的に値オブジェクトの実装の詳細です-それらは不変であるため、同じオブジェクトまたは複数のオブジェクトへの複数の参照を持つことに実質的な違いはありません。
is
is 等値演算子( id(a)== id(b)
のように機能します); 2つの等しい数が必ずしも同じオブジェクトであるとは限らないというだけです。パフォーマンス上の理由から、いくつかの小さな整数がメモされているため、同じになる傾向があります(これは不変であるため、行われます。)
PHPの ===
演算子は、 Paulo Freitasのコメントによると、同等性とタイプのチェックとして説明されています: x == yおよびtype(x)== type(y)
。これは一般的な数で十分ですが、 __ eq __
を不条理に定義するクラスの is
とは異なります:
class Unequal:
def __eq__(self, other):
return False
PHPでは、&quot;ビルトイン&quot;クラス(PHPではなくCレベルで実装することを意味します)。少し不合理な使用法はタイマーオブジェクトかもしれません。これは、数値として使用されるたびに異なる値を持ちます。それが time.time()
による評価であることを示すのではなく、Visual Basicの Now
をエミュレートしたい理由はわかりません。
グレッグヒューギル(OP)は、明確なコメントを1つ作成しました&quot;私の目標は、価値の平等ではなく、オブジェクトの同一性を比較することです。数値を除き、オブジェクトの同一性を値の等価と同じように扱いたい場合。&quot;
これは、 ==
と is
のどちらを比較するかを選択するために、物事を数字として分類するかどうかを決定する必要があるため、さらに別の答えがあります。 CPython は、 numberプロトコル、PyNumber_Checkを含むが、これはPython自体からはアクセスできません。
isinstance
を既知のすべての数値タイプで使用することもできますが、これは必然的に不完全です。 typesモジュールにはStringTypesリストが含まれますが、NumberTypesは含まれません。 Python 2.6以降、組み込みの数値クラスにはベースクラス numbersがあります.Number
ですが、同じ問題があります:
import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)
ところで、 NumPy は、低い数字の個別のインスタンスを生成します。
質問のこの変形に対する答えは実際にはわかりません。理論的にはctypesを使用して PyNumber_Check
を呼び出すことができますが、その関数でさえが議論されています、それは確かに移植性がありません。今のところ、テスト対象についてあまり気にする必要はありません。
最終的に、この問題は、のような述語を持つタイプツリーが元々Pythonにないことに起因しています。スキームの number?
、または Haskellの タイプクラス Num 。 is
は、値の等価性ではなく、オブジェクトの同一性をチェックします。 PHPにはカラフルな履歴もあります。 ===
は、
既存の回答のいずれにも指摘されていない別の問題があります。 Pythonは任意の2つの不変の値をマージすることが許可されており、事前に作成された小さなint値はこれが唯一の方法ではありません。 Pythonの実装がこれを行うことは保証されませんが、それらはすべて小さなintを超えるものに対してのみ行われます。
1つには、空の tuple
、 str
、および bytes
など、事前に作成された値がいくつかあります。短い文字列(CPython 3.6では、256個の単一文字Latin-1文字列です)。例:
>>> a = ()
>>> b = ()
>>> a is b
True
しかし、事前に作成されていない値も同じになる可能性があります。これらの例を考慮してください:
>>> c = 257
>>> d = 257
>>> c is d
False
>>> e, f = 258, 258
>>> e is f
True
そして、これは int
の値に限定されません:
>>> g, h = 42.23e100, 42.23e100
>>> g is h
True
明らかに、CPythonには 42.23e100
の事前作成された float
値が付属していません。それで、ここで何が起こっているのですか?
CPythonコンパイラは、 int
、 float
、 str
、 bytes などの既知の不変型の定数値をマージしますcode>、同じコンパイル単位。モジュールの場合、モジュール全体がコンパイル単位ですが、対話型インタープリターでは、各ステートメントは個別のコンパイル単位です。
c
と d
は別々のステートメントで定義されているため、それらの値はマージされません。 e
と f
は同じステートメントで定義されているため、それらの値はマージされます。
バイトコードを逆アセンブルすると、何が起こっているのかを確認できます。 e、f = 128、128
を実行する関数を定義してから、その関数で dis.dis
を呼び出すと、単一の定数値があることがわかります。 (128、128)
>>> def f(): i, j = 258, 258
>>> dis.dis(f)
1 0 LOAD_CONST 2 ((128, 128))
2 UNPACK_SEQUENCE 2
4 STORE_FAST 0 (i)
6 STORE_FAST 1 (j)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
>>> f.__code__.co_consts
(None, 128, (128, 128))
>>> id(f.__code__.co_consts[1], f.__code__.co_consts[2][0], f.__code__.co_consts[2][1])
4305296480, 4305296480, 4305296480
バイトコードで実際に使用されていない場合でも、コンパイラーは定数として 128
を保存していることに気付くかもしれません。これにより、CPythonのコンパイラーがどの程度最適化しないかがわかります。つまり、(空ではない)タプルは実際にはマージされません:
>>> k, l = (1, 2), (1, 2)
>>> k is l
False
それを関数に入れ、 dis
、それを見て co_consts
&#8212; 1
とがあります2
、同じ 1
と 2
を共有するが同一ではない2つの(1、2)
タプル、および((1、2)、(1、2))
2つの異なる等しいタプルを持つタプル。
CPythonが行う最適化がもう1つあります。文字列インターンです。コンパイラ定数の折りたたみとは異なり、これはソースコードリテラルに限定されません:
>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True
一方、 str
タイプと内部ストレージの種類&quot; ascii compact&quot;、&quot; compact&quot ;、または「legacy ready」&quot; 、多くの場合&quot; ascii compact&quot;抑留されます。
いずれにせよ、値の規則は、実装ごとに、また同じ実装のバージョン間で、さらには同じコピーで同じコードを実行する間でも、異なる場合があり、異なる場合があります。同じ実装。
楽しみのために、特定のPythonのルールを学ぶ価値があります。しかし、コードでそれらに依存する価値はありません。唯一の安全なルールは次のとおりです。
- 2つの等しいが別々に作成された不変の値が同一であると想定するコードを記述しないでください。
- 2つの等しいが別々に作成された不変の値が異なると想定するコードを記述しないでください。
または、言い換えると、 is
のみを使用して、文書化されたシングルトン( None
など)をテストするか、コード内の1か所でのみ作成されます( _sentinel = object()
イディオム)。
文字列でも起こります:
>>> s = b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)
これですべてが順調になりました。
>>> s = 'somestr'
>>> b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)
それも期待されています。
>>> s1 = b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, True, 4555308080, 4555308080)
>>> s1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, False, 4555308176, 4555308272)
これは予想外です。