Об изменении идентификатора неизменяемой строки
Кое-что об 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
CPython не обещает интернировать все строки по умолчанию, но на практике многие места в кодовой базе Python повторно используют уже созданные строковые объекты. Многие внутренние компоненты Python используют (эквивалент C) sys.intern()
вызов функции для явного объединения строк Python, но если вы не воспользуетесь одним из этих особых случаев, два идентичных строковых литерала Python будут выдавать разные строки.
Python также может свободно повторно использовать ячейки памяти, и Python также оптимизирует неизменяемые литералы, сохраняя их один раз, во время компиляции, с байт-кодом в объектах кода. Python REPL (интерактивный интерпретатор) также сохраняет самый последний результат выражения в _
имени, что еще больше запутывает ситуацию.
Таким образом, вы будете время от времени видеть один и тот же идентификатор.
Запуск только строки id(<string literal>)
в REPL выполняется в несколько этапов:
Строка компилируется, что включает в себя создание константы для объекта string:
>>> compile("id('foo')", '<stdin>', 'single').co_consts
('foo', None)Здесь показаны сохраненные константы с скомпилированным байт-кодом; в данном случае строка
'foo'
иNone
синглтон. Простые выражения, состоящие из, которые выдают неизменяемое значение, могут быть оптимизированы на этом этапе, см. Примечание об оптимизаторах ниже.При выполнении строка загружается из констант кода и
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На объект code ничто не ссылается, количество ссылок падает до 0, и объект code удаляется. Как следствие, то же самое происходит и с объектом string.
Python can then perhaps reuse the same memory location for a new string object, if you re-run the same code. This usually leads to the same memory address being printed if you repeat this code. This does depend on what else you do with your Python memory.
ID reuse is not predictable; if in the meantime the garbage collector runs to clear circular references, other memory could be freed and you'll get new memory addresses.
Next, the Python compiler will also intern any Python string stored as a constant, provided it looks enough like a valid identifier. The Python code object factory function PyCode_New will intern any string object that contains only ASCII letters, digits or underscores, by calling intern_string_constants()
. This function recurses through the constants structures and for any string object v
found there executes:
if (all_name_chars(v)) {
PyObject *w = v;
PyUnicode_InternInPlace(&v);
if (w != v) {
PyTuple_SET_ITEM(tuple, i, v);
modified = 1;
}
}
where all_name_chars()
is documented as
/* all_name_chars(s): true iff s matches [a-zA-Z0-9_]* */
Since you created strings that fit that criterion, they are interned, which is why you see the same ID being used for the 'so'
string in your second test: as long as a reference to the interned version survives, interning will cause future 'so'
literals to reuse the interned string object, even in new code blocks and bound to different identifiers. In your first test, you don't save a reference to the string, so the interned strings are discarded before they can be reused.
Incidentally, your new name so = 'so'
binds a string to a name that contains the same characters. In other words, you are creating a global whose name and value are equal. As Python interns both identifiers and qualifying constants, you end up using the same string object for both the identifier and its value:
>>> compile("so = 'so'", '<stdin>', 'single').co_names[0] is compile("so = 'so'", '<stdin>', 'single').co_consts[0]
True
If you create strings that are either not code object constants, or contain characters outside of the letters + numbers + underscore range, you'll see the id()
value not being reused:
>>> 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
The Python compiler either uses the peephole optimizer (Python versions < 3.7) or the more capable AST optimizer (3.7 and newer) to pre-calculate (fold) the results of simple expressions involving constants. The peepholder limits it's output to a sequence of length 20 or less (to prevent bloating code objects and memory use), while the AST optimizer uses a separate limit for strings of 4096 characters. This means that concatenating shorter strings consisting only of name characters can still lead to interned strings if the resulting string fits within the optimizer limits of your current Python version.
E.g. on Python 3.7, 'foo' * 20
will result in a single interned string, because constant folding turns this into a single value, while on Python 3.6 or older only 'foo' * 6
would be folded:
>>> 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
and
>>> 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
This behavior is specific to the Python interactive shell. If I put the following in a .py file:
print id('so')
print id('so')
print id('so')
and execute it, I receive the following output:
2888960
2888960
2888960
In CPython, a string literal is treated as a constant, which we can see in the bytecode of the snippet above:
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
The same constant (i.e. the same string object) is loaded 3 times, so the IDs are the same.
Ответ 3
In your first example a new instance of the string 'so'
is created each time, hence different id.
In the second example you are binding the string to a variable and Python can then maintain a shared copy of the string.
Ответ 4
A more simplified way to understand the behaviour is to check the following Data Types and Variables.
Section "A String Pecularity" illustrates your question using special characters as example.