Об изменении идентификатора неизменяемой строки

Что-то о id объектов типа str (в python 2.7) меня озадачивает. Тип str неизменен, поэтому я ожидал бы, что после его создания он всегда будет иметь тот же id. Я считаю, что я не очень хорошо формулирую себя, поэтому вместо этого я отправлю пример последовательности ввода и вывода.

>>> id('so')
140614155123888
>>> id('so')
140614155123848
>>> id('so')
140614155123808

поэтому между тем он меняется все время. Однако после изменения переменной, указывающей на эту строку, все меняется:

>>> so = 'so'
>>> id('so')
140614155123728
>>> so = 'so'
>>> id(so)
140614155123728
>>> not_so = 'so'
>>> id(not_so)
140614155123728

Итак, похоже, что он зависает от id, как только переменная содержит это значение. Действительно, после del so и del not_so вывод id('so') снова начинает меняться.

Это не то же поведение, что и с (малыми) целыми числами.

Я знаю, что нет реальной связи между неизменностью и тем же id; Тем не менее, я пытаюсь выяснить источник этого поведения. Я считаю, что кто-то, кто знаком с внутренностями python, будет менее удивлен, чем я, поэтому я пытаюсь достичь той же точки...

Update

Попытка того же с другой строкой давала разные результаты...

>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384

Теперь он равен...

Ответ 1

CPython не обещает интернировать все строки по умолчанию, но на практике многие места в кодовой базе Python действительно используют уже созданные строковые объекты. Многие внутренние компоненты Python используют (эквивалент C) вызов функции sys.intern() для явного интернирования строк Python, но если вы не нажмете один из этих особых случаев, два идентичных строковых литерала Python будут создавать разные строки.

Python также может свободно использовать области памяти, и Python также оптимизирует неизменяемые литералы, сохраняя их один раз, во время компиляции, с байт-кодом в объектах кода. Python REPL (интерактивный интерпретатор) также хранит самый последний результат выражения в имени _, что еще больше запутывает.

Таким образом, время от времени будет появляться один и тот же идентификатор.

Выполнение только строки id(<string literal>) в REPL проходит через несколько шагов:

  1. Строка компилируется, что включает создание константы для строкового объекта:

    >>> compile("id('foo')", '<stdin>', 'single').co_consts
    ('foo', None)
    

    Здесь показаны сохраненные константы с скомпилированным байт-кодом; в этом случае строка 'foo' и синглтон None. На этом этапе можно оптимизировать простые выражения, состоящие из неизменяемых значений, см. примечание по оптимизаторам ниже.

  2. При выполнении строка загружается из констант кода, и id() возвращает ячейку памяти. Полученное значение int привязывается к _, а также печатается:

    >>> import dis
    >>> dis.dis(compile("id('foo')", '<stdin>', 'single'))
      1           0 LOAD_NAME                0 (id)
                  3 LOAD_CONST               0 ('foo')
                  6 CALL_FUNCTION            1
                  9 PRINT_EXPR          
                 10 LOAD_CONST               1 (None)
                 13 RETURN_VALUE        
    
  3. На объект кода ничего не ссылается, счетчик ссылок падает до 0, а объект кода удаляется. Как следствие, строковый объект тоже.

Затем Python может повторно использовать ту же ячейку памяти для нового строкового объекта, если вы повторно запустите тот же код. Обычно это приводит к тому, что при повторении этого кода печатается тот же адрес памяти. Это зависит от того, что еще вы делаете со своей памятью Python.

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

Далее, компилятор Python также интернирует любую строку Python, хранящуюся как константу, при условии, что она выглядит достаточно как действительный идентификатор. Функция фабрики объектов кода Python PyCode_New будет интернировать любой строковый объект, содержащий только буквы, цифры или подчеркивания ASCII, путем вызова intern_string_constants(). Эта функция проходит через структуры констант и для любого найденного там строкового объекта v выполняет:

if (all_name_chars(v)) {
    PyObject *w = v;
    PyUnicode_InternInPlace(&v);
    if (w != v) {
        PyTuple_SET_ITEM(tuple, i, v);
        modified = 1;
    }
}

где all_name_chars() задокументировано как

/* all_name_chars(s): true iff s matches [a-zA-Z0-9_]* */

Поскольку вы создали строки, которые соответствуют этому критерию, они интернированы, поэтому вы видите тот же идентификатор, который используется для строки 'so' во втором тесте: до тех пор, пока сохраняется ссылка на интернированную версию, интернинг будет заставить будущие литералы 'so' повторно использовать объект внутренней строки, даже в новых блоках кода и привязанных к различным идентификаторам. В первом тесте вы не сохраняете ссылку на строку, поэтому интернированные строки отбрасываются, прежде чем их можно будет использовать повторно.

Кстати, ваше новое имя so = 'so' связывает строку с именем, содержащим те же символы. Другими словами, вы создаете глобал, имя и значение которого равны. Поскольку Python объединяет и идентификаторы, и квалифицирующие константы, вы в конечном итоге используете один и тот же строковый объект как для идентификатора, так и для его значения:

>>> compile("so = 'so'", '<stdin>', 'single').co_names[0] is compile("so = 'so'", '<stdin>', 'single').co_consts[0]
True

Если вы создаете строки, которые не являются константами объекта кода или содержат символы вне диапазона букв + цифры + подчеркивания, вы увидите, что значение id() не используется повторно:

>>> some_var = 'Look ma, spaces and punctuation!'
>>> some_other_var = 'Look ma, spaces and punctuation!'
>>> id(some_var)
4493058384
>>> id(some_other_var)
4493058456
>>> foo = 'Concatenating_' + 'also_helps_if_long_enough'
>>> bar = 'Concatenating_' + 'also_helps_if_long_enough'
>>> foo is bar
False
>>> foo == bar
True

Компилятор Python либо использует оптимизатор глазков (версии Python & lt; 3.7), либо более способный оптимизатор AST (версии 3.7 и новее) для предварительного вычисления (свертывания) результатов. простых выражений с участием констант. Портфолдер ограничивает выходной поток последовательностью длиной 20 или менее (для предотвращения раздувания объектов кода и использования памяти), тогда как оптимизатор AST использует отдельное ограничение для строк длиной 4096 символов. Это означает, что объединение более коротких строк, состоящих только из символов имени, может все еще привести к интернированным строкам, если получающаяся строка соответствует ограничениям оптимизатора вашей текущей версии Python.

Например. в Python 3.7 'foo' * 20 приведет к единственной интернированной строке, потому что постоянное сворачивание превращает это в одно значение, в то время как на Python 3.6 или более ранней версии 'foo' * 6 будет свернуто:

>>> import dis, sys
>>> sys.version_info
sys.version_info(major=3, minor=7, micro=4, releaselevel='final', serial=0)
>>> dis.dis("'foo' * 20")
  1           0 LOAD_CONST               0 ('foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo')
              2 RETURN_VALUE

и

>>> dis.dis("'foo' * 6")
  1           0 LOAD_CONST               2 ('foofoofoofoofoofoo')
              2 RETURN_VALUE
>>> dis.dis("'foo' * 7")
  1           0 LOAD_CONST               0 ('foo')
              2 LOAD_CONST               1 (7)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE

Ответ 2

Это поведение специфично для интерактивной оболочки Python. Если я поставлю следующее в файле .py:

print id('so')
print id('so')
print id('so')

и выполнить его, я получаю следующий вывод:

2888960
2888960
2888960

В CPython строковый литерал рассматривается как константа, которую мы можем видеть в байт-коде фрагмента выше:

  2           0 LOAD_GLOBAL              0 (id)
              3 LOAD_CONST               1 ('so')
              6 CALL_FUNCTION            1
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       

  3          11 LOAD_GLOBAL              0 (id)
             14 LOAD_CONST               1 ('so')
             17 CALL_FUNCTION            1
             20 PRINT_ITEM          
             21 PRINT_NEWLINE       

  4          22 LOAD_GLOBAL              0 (id)
             25 LOAD_CONST               1 ('so')
             28 CALL_FUNCTION            1
             31 PRINT_ITEM          
             32 PRINT_NEWLINE       
             33 LOAD_CONST               0 (None)
             36 RETURN_VALUE  

Одна и та же константа (т.е. тот же строковый объект) загружается 3 раза, поэтому идентификаторы одинаковы.

Ответ 3

В первом примере каждый раз создается новый экземпляр строки 'so', отсюда и другой идентификатор.

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

Ответ 4

Таким образом, хотя Python не гарантируется для внутренних строк, он часто будет повторно использовать одну и ту же строку, а is может ввести в заблуждение. Важно знать, что вы не должны проверять id или is на равенство строк.

Чтобы продемонстрировать это, один из способов, которым я обнаружил, чтобы заставить новую строку в Python 2.6 по крайней мере:

>>> so = 'so'
>>> new_so = '{0}'.format(so)
>>> so is new_so 
False

и здесь немного больше исследований Python:

>>> id(so)
102596064
>>> id(new_so)
259679968
>>> so == new_so
True

Ответ 5

Более упрощенный способ понять поведение - проверить следующие Типы данных и переменные.

Раздел "Строчная чистота" иллюстрирует ваш вопрос, используя специальные символы в качестве примера.