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

#python #string #immutability #python-internals

#python #строка #неизменность #python-внутренние компоненты

Вопрос:

Что-то в 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
  

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

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

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

Обновить

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

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

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

Комментарии:

1. Python не интернирует строки по умолчанию. Во многих внутренних кодах Python явно вводятся строковые значения (имена атрибутов, идентификаторы и т.д.), Но это не распространяется на произвольные строки.

2. Вместо этого Python может повторно использовать слоты памяти . Вам нужно создавать объекты с более длительным сроком службы.

3. @Bach если переменная содержит это значение , правильно ли это утверждение в python? Читать это .

Ответ №1:

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

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

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

Запуск только строки id(<string literal>) в REPL выполняется в несколько этапов:

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

     >>> 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. На объект code ничто не ссылается, количество ссылок падает до 0, а объект code удаляется. Как следствие, то же самое относится и к объекту string.

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

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

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

 if (all_name_chars(v)) {
    PyObject *w = v;
    PyUnicode_InternInPlace(amp;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 использует как идентификаторы, так и уточняющие константы, в конечном итоге вы используете один и тот же объект string как для идентификатора, так и для его значения:

 >>> 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 использует либо оптимизатор peephole (версии Python <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
  

Комментарии:

1. Я не уверен, что полностью понимаю, что такое интернирование в этом смысле, но, думаю, мне придется немного почитать об этом; спасибо.

2. @Bach: Интернирование — это повторное использование строкового объекта, если он уже был создан один раз ранее с тем же значением.

3. Огромное количество знаний, которыми обладает @MartijnPieters в мире Python, сбивает меня с толку ^^

4.@MariusMucenicu да, мой ответ описывает алгоритм в общих чертах. Вы также можете использовать grep для вызовов PyUnicode_InternInPlace и PyUnicode_InternFromString функций в исходном коде Python, чтобы увидеть, где Python интернирует строки (например, поиск на GitHub для любой функции)..

5. @MariusMucenicu сам пул является объектом dict, определенным в unicodeobject.c исходном коде , в котором также размещены функции API для интернирования (за исключением одного макроса ).

Ответ №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  
  

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

Комментарии:

1. @Bach я имею в виду интерактивную оболочку Python.

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

3. @Bach Да, литеральная строка 'so' хранится как одна константа, поэтому каждый раз, когда вы ее используете, загружается одна и та же константа, что позволяет избежать необходимости каждый раз создавать новую строку.

Ответ №3:

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

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

Комментарии:

1. OP повторно привязывает объект string.

2. Ваше объяснение ошибочно; второй пример привязывает новые строковые литералы к тому же имени, а также к другому имени. so это отскок, затем not_so отскок. Это не тот же объект string .

Ответ №4:

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

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

Ответ №5:

Таким образом, хотя Python не гарантирует интернирование строк, он часто будет повторно использовать одну и ту же строку и is может вводить в заблуждение. Важно знать, что вы не должны проверять id or 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
  

Комментарии:

1. @Bach Вы бы сказали, что это отвечает на вопрос сейчас?