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

Что-то о id объектов типа str (в Python 2.7) озадачивает меня. str type является неизменным, поэтому я ожидаю, что после его создания он всегда будет одинаковым 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

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

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

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

Обновить

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

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

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

5 ответов

Решение

CPython не обещает интернировать строки по умолчанию, но на практике многие места в кодовой базе Python повторно используют уже созданные строковые объекты. Многие внутренние компоненты Python используют (эквивалент C) 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 selected string constants */
for (i = PyTuple_Size(consts); --i >= 0; ) {
    PyObject *v = PyTuple_GetItem(consts, i);
    if (!PyString_Check(v))
        continue;
    if (!all_name_chars((unsigned char *)PyString_AS_STRING(v)))
        continue;
    PyString_InternInPlace(&PyTuple_GET_ITEM(consts, i));
}

Поскольку вы создали строки, которые соответствуют этому критерию, они интернированы, поэтому вы видите тот же идентификатор, используемый для '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 предварительно рассчитывает результаты простых выражений, но если это приводит к последовательности, превышающей 20, вывод игнорируется (чтобы предотвратить вздутие объектов кода и использование памяти); поэтому объединение более коротких строк, состоящих только из именных символов, может все еще привести к интернированным строкам, если результат будет 20 символов или короче.

Такое поведение характерно для интерактивной оболочки 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 раза, поэтому идентификаторы совпадают.

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

Раздел "Особенность строки" иллюстрирует ваш вопрос на примере специальных символов.

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

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

Таким образом, хотя 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
Другие вопросы по тегам