[python] “is”연산자는 정수로 예기치 않게 동작합니다

왜 파이썬에서 다음이 예기치 않게 동작합니까?

>>> 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 사이의 위의 동작을 보여줍니다.

위의 내용을 바탕으로 파이썬이 내부적으로 구현되어 “작은”정수가 큰 정수와 다른 방식으로 저장되고 is연산자가 그 차이를 알 수 있다고 가정 할 수 있습니다. 왜 새는 추상화인가? 두 개의 임의의 객체를 비교하여 숫자인지 여부를 미리 알 수없는 경우 동일한 지 여부를 확인하는 더 좋은 방법은 무엇입니까?



답변

이것 좀 봐 :

>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828

다음은 Python 2 문서 “Plain Integer Objects” 에서 찾은 내용입니다 ( Python 3 과 동일 ).

현재 구현에서는 -5와 256 사이의 모든 정수에 대해 정수 객체 배열을 유지합니다.이 범위에서 int를 만들면 실제로는 기존 객체에 대한 참조를 다시 얻습니다. 따라서 1의 값을 변경할 수 있어야합니다.이 경우 Python의 동작이 정의되지 않은 것 같습니다. 🙂


답변

파이썬의 “is”연산자는 정수로 예기치 않게 동작합니까?

요약하자면 다음과 같이 강조하겠습니다 . 정수를 비교 하는 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합니까? 비교 연산자입니다. 로부터 문서 :

x와 y가 동일한 객체 인 경우에만 연산자 isis not객체 동일성 테스트 : x is ytrue입니다. x is not y역 진리 값을 산출합니다.

따라서 다음은 동일합니다.

>>> a is b
>>> id(a) == id(b)

로부터 문서 :

id
객체의 “정체성”을 반환합니다. 이것은 수명 동안이 개체에 대해 고유하고 일정하게 보장되는 정수 (또는 긴 정수)입니다. 겹치지 않는 수명을 가진 두 개체의 id()값 이 동일 할 수 있습니다 .

CPython에서 객체의 id (Python의 참조 구현)가 메모리의 위치라는 사실은 구현 세부 사항입니다. 다른 Python 구현 (예 : Jython 또는 IronPython)은에 대해 다른 구현을 쉽게 가질 수 있습니다 id.

그렇다면 유스 케이스는 is무엇입니까? PEP8은 다음을 설명합니다 .

싱글 톤과의 비교 None는 항상 등호 연산자를 사용하지 않고 is또는
로 수행해야합니다 is not.

질문

다음 질문을 묻고 진술하십시오 (코드 포함).

왜 파이썬에서 다음이 예기치 않게 동작합니까?

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result

예상 된 결과 가 아닙니다 . 왜 예상됩니까? 그것은 단지 가치 정수는 것을 의미 256모두에 의해 참조 ab정수의 동일한 인스턴스입니다. 정수는 파이썬에서 변경할 수 없으므로 변경할 수 없습니다. 이것은 코드에 영향을 미치지 않습니다. 예상해서는 안됩니다. 그것은 단지 구현 세부 사항입니다.

그러나 우리는 256이라는 값을 말할 때마다 메모리에 새로운 별도의 인스턴스가 없다는 것을 기뻐해야 할 것입니다.

>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?

257메모리에 값이 있는 두 개의 정수 인스턴스가있는 것처럼 보입니다 . 정수는 불변이므로 메모리가 낭비됩니다. 우리가 많이 낭비하지 않기를 바랍니다. 우리는 아마 아닐 것입니다. 그러나이 동작은 보장되지 않습니다.

>>> 257 is 257
True           # Yet the literal numbers compare properly

글쎄, 이것은 파이썬의 특정 구현이 똑똑하고 노력하지 않는 한 메모리에 중복 값을 생성하지 않는 것처럼 보입니다. CPython 인 Python의 참조 구현을 사용하고 있음을 나타냅니다. CPython에 좋습니다.

CPython이 전 세계적 으로이 작업을 수행 할 수 있다면 더 저렴 할 수 있다면 (검색 비용이 들기 때문에) 아마도 다른 구현 일 수 있습니다.

그러나 코드에 미치는 영향에 대해서는 정수가 정수의 특정 인스턴스인지는 신경 쓰지 않아야합니다. 해당 인스턴스의 값이 무엇인지 신경 써야하며, 이에 대한 일반 비교 연산자를 사용해야합니다 (예 🙂 ==.

무엇 is합니까

is검사는 것을 id두 객체는 동일하다. CPython에서이 id위치는 메모리의 위치이지만 다른 구현에서 다른 고유 번호 일 수 있습니다. 이것을 코드로 바꾸려면 :

>>> a is b

와 같다

>>> id(a) == id(b)

왜 우리가 사용하고 싶 is습니까?

이것은 두 개의 매우 긴 문자열의 값이 같은지 확인하는 것과 비교하여 매우 빠른 검사 일 수 있습니다. 그러나 객체의 고유성에 적용되므로 사용 사례가 제한적입니다. 실제로 우리는 주로 None싱글 톤 (메모리의 한 곳에 존재하는 유일한 인스턴스) 인 확인에 사용하려고합니다 . 서로 혼동 할 가능성이있는 경우 다른 싱글 톤을 만들 수도 있습니다 is. 다음은 예제입니다 (Python 2 및 3에서 작동). 예 :

SENTINEL_SINGLETON = object() # this will only be created one time.

def foo(keyword_argument=None):
    if keyword_argument is None:
        print('no argument given to foo')
    bar()
    bar(keyword_argument)
    bar('baz')

def bar(keyword_argument=SENTINEL_SINGLETON):
    # SENTINEL_SINGLETON tells us if we were not passed anything
    # as None is a legitimate potential argument we could get.
    if keyword_argument is SENTINEL_SINGLETON:
        print('no argument given to bar')
    else:
        print('argument to bar: {0}'.format(keyword_argument))

foo()

어떤 지문 :

no argument given to foo
no argument given to bar
argument to bar: None
argument to bar: baz

따라서 is센티넬을 사용하면 bar인수없이 호출 할 때 와 호출 할 때를 구분할 수 None있습니다. 이것들은 주요 유스 케이스입니다 is– 정수, 문자열, 튜플 또는 이와 같은 다른 것들의 동등성을 테스트하는 데 사용 하지 마십시오 .


답변

두 가지가 동일한 지 또는 동일한 객체인지 여부에 따라 다릅니다.

is동일한 객체가 아닌 동일한 객체인지 확인합니다. 작은 정수는 아마도 공간 효율성을 위해 동일한 메모리 위치를 가리키고 있습니다.

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에서 새 오브젝트 작성을 처리 하는 C-API 함수 intPyLong_FromLong(long v)입니다. 이 기능에 대한 설명은 다음과 같습니다.

현재 구현은 -5에서 256 사이의 모든 정수에 대해 정수 객체 배열을 유지합니다.이 범위에서 int를 만들면 실제로는 기존 객체에 대한 참조를 다시 얻습니다 . 따라서 1의 값을 변경할 수 있어야합니다.이 경우 Python의 동작이 정의되지 않은 것 같습니다. 🙂

(내 이탤릭체)

당신에 대해 모르지만 나는 이것을보고 생각합니다 : 그 배열을 찾아 봅시다!

CPython 구현하는 C 코드를 다루지 않았다면 ; 모든 것이 꽤 체계적이고 읽기 쉽습니다. 우리의 경우, 우리는에서 볼 필요가 Objects하위 디렉토리주요 소스 코드 디렉토리 트리 .

PyLong_FromLonglong객체를 다루 므로 내부를 들여다 볼 필요가 있다고 추론해서는 안됩니다 longobject.c. 내부를 살펴본 후 상황이 혼란 스럽다고 생각할 수 있습니다. 그들은 우리가 찾고있는 기능이 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)

따라서 get_small_intival이 조건을 만족하면 함수를 호출하는 매크로입니다 .

if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS)

그래서 무엇 NSMALLNEGINTSNSMALLPOSINTS? 매크로! 여기 있습니다 :

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

우리의 상태는 if (-5 <= ival && ival < 257)call get_small_int입니다.

다음 get_small_int으로 모든 영광을 보자 (자, 우리는 단지 흥미로운 것들이있는 곳이기 때문에 몸을 볼 것입니다) :

PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);

자, a를 선언 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];

그래,이 사람은 우리 남자 야 int범위에서 새 항목을 만들려면 [NSMALLNEGINTS, NSMALLPOSINTS)이미 할당 된 기존 개체에 대한 참조를 다시 가져옵니다.

참조는 동일한 객체를 참조하므로 id()직접 발행 하거나 ID를 확인 is하면 정확히 동일한 결과가 반환됩니다.

그러나 언제 할당됩니까?

_PyLong_Init파이썬 에서 초기화하는 동안 기꺼이 for 루프에 들어가면 다음과 같이하십시오.

for (ival = -NSMALLNEGINTS; ival <  NSMALLPOSINTS; ival++, v++) {

루프 바디를 읽으려면 소스를 확인하십시오!

내 설명으로 C 물건을 명확하게 만들었 기를 바랍니다.


그러나 257 is 257? 뭐야?

이것은 실제로 설명하기가 더 쉬우 며 이미 그렇게 시도했습니다 . 파이썬 이이 대화 형 문을 단일 블록으로 실행한다는 사실 때문입니다.

>>> 257 is 257

이 문장을 완성하는 동안 CPython은 두 개의 일치하는 리터럴이 있으며 동일한 PyLongObject표현 을 사용한다는 것을 알 수 있습니다 257. 컴파일을 직접 수행하고 내용을 검사하면 이것을 볼 수 있습니다.

>>> codeObj = compile("257 is 257", "blah!", "exec")
>>> codeObj.co_consts
(257, None)

CPython이 작업을 수행하면 이제 정확히 동일한 객체를로드합니다.

>>> import dis
>>> dis.dis(codeObj)
  1           0 LOAD_CONST               0 (257)   # dis
              3 LOAD_CONST               0 (257)   # dis again
              6 COMPARE_OP               8 (is)

그래서 is돌아갑니다 True.


답변

소스 파일 intobject.c를 체크인 할 수 있으므로 , 파이썬은 효율성을 위해 작은 정수를 캐시합니다. 작은 정수에 대한 참조를 만들 때마다 새 객체가 아닌 캐시 된 작은 정수를 참조합니다. 257은 작은 정수가 아니므로 다른 객체로 계산됩니다.

==그 목적 으로 사용 하는 것이 좋습니다.


답변

나는 당신의 가설이 맞다고 생각합니다. 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)

숫자 <= 255는 리터럴로 취급되며 위의 내용은 다르게 취급됩니다!


답변

정수, 문자열 또는 날짜 시간과 같은 변경 불가능한 값 객체의 경우 객체 ID가 특히 유용하지 않습니다. 평등에 대해 생각하는 것이 좋습니다. 아이덴티티는 본질적으로 가치 객체에 대한 구현 세부 사항입니다. 불변이기 때문에 동일한 객체 또는 여러 객체에 대한 다중 참조를 갖는 것 사이에는 효과적인 차이가 없습니다.