Оператор «is» ведет себя неожиданно с целыми числами

StackOverflow https://stackoverflow.com/questions/306313

Вопрос

Почему в 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 оператор может заметить разницу.Почему дырявая абстракция?Какой лучший способ сравнить два произвольных объекта, чтобы увидеть, одинаковы ли они, если я заранее не знаю, являются ли они числами или нет?

Это было полезно?

Решение

Взгляните на это:

>>> 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» неожиданно ведет себя с целыми числами?

Подводя итог - подчеркну: Не использовать 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 и is not проверка на идентичность объекта: x is y является истиной Тогда и только тогда, когда x и y являются одним и тем же объектом. x is not y возвращает значение Обратное истинностное значение.

Итак, следующие утверждения эквивалентны.

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

Из документация:

idВозвращает «идентичность» объекта.Это целое число (или длинное целое число), которое гарантированно будет уникальным и постоянным для данного объекта в течение всей своей жизни.Два объекта с неперекрывающимся временем жизни могут имеют то же самое id() ценить.

Обратите внимание: тот факт, что идентификатор объекта в CPython (эталонной реализации Python) является местоположением в памяти, является деталью реализации.Другие реализации Python (например, Jython или IronPython) могут легко иметь другую реализацию для id.

Итак, каков вариант использования is? PEP8 описывает:

Сравнения с синглтонами, например None всегда следует делать с is или is not, никогда операторы равенства.

Вопрос

Вы задаете и формулируете следующий вопрос (с кодом):

Почему в Python происходит неожиданное поведение?

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

Это нет ожидаемый результат.Почему это ожидается?Это означает лишь то, что целые числа со значением 256 на которые ссылаются оба a и b являются одним и тем же экземпляром целого числа.Целые числа в Python неизменяемы, поэтому они не могут изменяться.Это не должно влиять ни на какой код.Этого не следует ожидать.Это просто деталь реализации.

Но, возможно, нам следует порадоваться, что в памяти не возникает нового отдельного экземпляра каждый раз, когда мы указываем значение, равное 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

Что ж, похоже, что ваша конкретная реализация Python пытается быть умной и не создавать в памяти целые числа с избыточными значениями, если в этом нет необходимости.Кажется, вы указываете, что используете референтную реализацию Python, то есть CPython.Хорошо для 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 - делать нет используйте его для проверки равенства целых чисел, строк, кортежей и других подобных вещей.

Это зависит от того, хотите ли вы увидеть, равны ли 2 вещи или один и тот же объект.

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 , которая обрабатывает создание нового объекта int , является PyLong_FromLong (long v) . Описание этой функции:

  

Текущая реализация хранит массив целочисленных объектов для всех целых чисел от -5 до 256, когда вы создаете int в этом диапазоне, вы фактически просто получаете ссылку на существующий объект . Так что должно быть возможно изменить значение 1. Я подозреваю, что поведение Python в этом случае не определено. : -)

Не знаю как вы, но я вижу это и думаю: Давайте найдем этот массив!

Если вы не возились с кодом C , реализующим CPython , вы должны , все довольно организовано и читабельно. В нашем случае нам нужно заглянуть в Objects / подкаталог. дерева каталогов основного исходного кода .

PyLong_FromLong имеет дело с объектами long , поэтому нетрудно понять, что нам нужно заглянуть внутрь 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_int , если значение ival удовлетворяет условию:

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

Так что же такое NSMALLNEGINTS и NSMALLPOSINTS ? Если вы угадали макросы, вы ничего не получите, потому что это не такой сложный вопрос .. В любом случае, вот они :

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

Итак, наше условие: if (-5 < = ival & amp; ival < 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];

Да, это наш парень. Когда вы захотите создать новый int в диапазоне [NSMALLNEGINTS, NSMALLPOSINTS) , вы просто получите ссылку на уже существующий объект, который был предварительно выделен.

Поскольку ссылка

Как вы можете проверить в исходный файл intobject.c , Python для эффективности кэширует маленькие целые числа. Каждый раз, когда вы создаете ссылку на маленькое целое число, вы ссылаетесь на кешированное маленькое целое число, а не на новый объект. 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 рассматриваются как литералы, а все вышеперечисленное обрабатывается по-другому!

Для объектов с неизменяемыми значениями, таких как целые, строки или даты и времени, идентификация объекта не особенно полезна. Лучше подумать о равенстве. Идентичность - это, по сути, деталь реализации для объектов-значений - поскольку они неизменны, нет эффективной разницы между наличием нескольких ссылок на один и тот же объект или несколько объектов.

is is оператор равенства идентичности (работает как id (a) == id (b) ); просто два равных числа не обязательно являются одним и тем же объектом. По соображениям производительности некоторые небольшие целые числа запоминаются , поэтому они, как правило, одинаковы (это может быть сделано, так как они неизменны).

оператор PHP === , с другой стороны, описывается как проверка равенства и типа: x == y и type (x) == type (y) согласно комментарию Пауло Фрейтаса. Это будет достаточно для обычных чисел, но отличается от is для классов, которые определяют __ eq __ абсурдным образом:

class Unequal:
    def __eq__(self, other):
        return False

PHP, очевидно, допускает то же самое для " встроенного " классы (которые я имею в виду, реализованные на уровне C, а не в PHP) Чуть менее абсурдным может быть использование объекта-таймера, значение которого будет отличаться при каждом использовании в качестве числа. Именно поэтому вы захотите эмулировать Now в Visual Basic вместо того, чтобы показывать, что это оценка с time.time () , я не знаю.

Грег Хьюгилл (OP) сделал один уточняющий комментарий "Моя цель - сравнить идентичность объекта, а не равенство значений. За исключением чисел, где я хочу трактовать идентичность объекта так же, как равенство значений. & Quot;

Это могло бы дать еще один ответ, поскольку мы должны классифицировать вещи как числа или нет, чтобы выбрать, будем ли мы сравнивать с == или is . CPython определяет номер протокола , включая 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 изначально не было дерева типов с предикатами, такими как номер схемы или Haskell's класс шрифтов Num . is проверяет идентичность объекта, а не равенство значений. PHP также имеет красочную историю, где === , по-видимому, ведет себя как is только для объектов в PHP5, но не в PHP4 . Такова растущая боль при перемещении между языками (включая версии одного).

Есть еще одна проблема, которая не указана ни в одном из существующих ответов.Python разрешено объединять любые два неизменяемых значения, и предварительно созданные небольшие целочисленные значения — не единственный способ сделать это.Реализация Python никогда не гарантированный чтобы сделать это, но все они делают это не только для небольших целых чисел.


Во-первых, есть и другие заранее созданные значения, например пустое 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 не поставляется с заранее созданным float ценность для 42.23e100.Итак, что здесь происходит?

Компилятор CPython объединит константные значения некоторых известных неизменяемых типов, таких как int, float, str, bytes, в том же модуле компиляции.Для модуля весь модуль является единицей компиляции, но в интерактивном интерпретаторе каждый оператор представляет собой отдельную единицу компиляции.С 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-есть 1 и 2, два (1, 2) кортежи, которые имеют одно и то же 1 и 2 но не идентичны и ((1, 2), (1, 2)) кортеж, который имеет два различных равных кортежа.


Есть еще одна оптимизация, которую выполняет CPython:строковая стажировка.В отличие от свертывания констант компилятора, это не ограничивается литералами исходного кода:

>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True

С другой стороны, оно ограничивается str типа и строкам тип внутренней памяти «компактный ascii», «компактный» или «готовый к использованию устаревших версий», и во многих случаях интернируется только «ascii Compact».


В любом случае, правила относительно того, какие значения должны быть, могут быть или не могут быть разными, различаются от реализации к реализации, а также от версий одной и той же реализации и, возможно, даже между запусками одного и того же кода в одной и той же копии одной и той же реализации. .

Возможно, ради интереса стоит изучить правила для одного конкретного Python.Но полагаться на них в своем коде не стоит.Единственное безопасное правило:

  • Не пишите код, который предполагает, что два равных, но отдельно созданных неизменяемых значения идентичны.
  • Не пишите код, который предполагает, что два равных, но отдельно созданных неизменяемых значения различны.

Или, другими словами, используйте только is для проверки документированных синглтонов (например, None) или создаются только в одном месте кода (например, _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)

Теперь это неожиданно.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top