Інтернування рядка Python


92

Хоча це питання практично не застосовується на практиці, мені цікаво, як Python виконує інтернінг рядків. Я помітив наступне.

>>> "string" is "string"
True

Це як я очікував.

Ви також можете це зробити.

>>> "strin"+"g" is "string"
True

І це досить розумно!

Але ви не можете цього зробити.

>>> s1 = "strin"
>>> s2 = "string"
>>> s1+"g" is s2
False

Чому Python не оцінює s1+"g"і не усвідомлює, що це те саме, що s2і не вказує на ту саму адресу? Що насправді відбувається в цьому останньому блоці, щоб він повернувся False?

Відповіді:


95

Це залежить від реалізації, але ваш інтерпретатор, ймовірно, інтернує константи часу компіляції, але не результати виразів виконання.

Далі я використовую CPython 2.7.3.

У другому прикладі вираз "strin"+"g"обчислюється під час компіляції та замінюється на "string". Це змушує перші два приклади поводитися однаково.

Якщо ми вивчимо байт-коди, то побачимо, що вони абсолютно однакові:

  # s1 = "string"
  2           0 LOAD_CONST               1 ('string')
              3 STORE_FAST               0 (s1)

  # s2 = "strin" + "g"
  3           6 LOAD_CONST               4 ('string')
              9 STORE_FAST               1 (s2)

Третій приклад включає конкатенацію часу виконання, результат якої не інтернується автоматично:

  # s3a = "strin"
  # s3 = s3a + "g"
  4          12 LOAD_CONST               2 ('strin')
             15 STORE_FAST               2 (s3a)

  5          18 LOAD_FAST                2 (s3a)
             21 LOAD_CONST               3 ('g')
             24 BINARY_ADD          
             25 STORE_FAST               3 (s3)
             28 LOAD_CONST               0 (None)
             31 RETURN_VALUE        

Якби вручну intern()отримати результат третього виразу, ви отримали б той самий об’єкт, що і раніше:

>>> s3a = "strin"
>>> s3 = s3a + "g"
>>> s3 is "string"
False
>>> intern(s3) is "string"
True

22
І для протоколу: оптимізація викриття Python попередньо обчислює арифметичні операції з константами ( "string1" + "s2", 10 + 3*20тощо) під час компіляції, але обмежує отримані послідовності лише 20 елементами (щоб запобігти [None] * 10**1000надмірному розширенню вашого байт-коду). Саме ця оптимізація , яка обрушилася "strin" + "g"на "string"; результат менше 20 символів.
Мартін Пітерс

13
І щоб було подвійно зрозумілим: тут взагалі не відбувається стажування. Натомість незмінні літерали зберігаються як константи з байт-кодом. Інтернування дійсно має місце для імен , використовуваних в коді, але не для строкових значень , створених програмою , якщо Спеціально не інтернована intern()функцією.
Мартін Пітерс

9
Для тих, хто намагається знайти internфункцію в Python 3 - вона перенесена на sys.intern
Тимофій Черноусов

1

Випадок 1

>>> x = "123"  
>>> y = "123"  
>>> x == y  
True  
>>> x is y  
True  
>>> id(x)  
50986112  
>>> id(y)  
50986112  

Випадок 2

>>> x = "12"
>>> y = "123"
>>> x = x + "3"
>>> x is y
False
>>> x == y
True

Тепер, ваше запитання, чому ж ідентифікатор в разі 1 , а не в разі 2.
У разі 1, ви призначили строковий літерал "123"до xі y.

Оскільки рядок є незмінним, інтерпретатору має сенс зберігати строковий літерал лише один раз і вказувати всі змінні на один і той же об'єкт.
Отже, ви бачите ідентифікатор ідентичним.

У випадку 2 ви модифікуєте xза допомогою конкатенації. І те, xі yінше має однакові цінності, але не однакову ідентичність.
Обидва вказують на різні об'єкти в пам'яті. Отже, у них різний оператор idі isповерненийFalse


Як так, оскільки рядки незмінні, присвоєння x + "3" (і пошук нового місця для зберігання рядка) не присвоює тому самому посиланню, що і y?
nicecatch

Тому що тоді йому потрібно порівняти новий рядок з усіма існуючими рядками; потенційно дуже дорога операція. Можливо, це може зробити у фоновому режимі після призначення, щоб зменшити пам’ять, але тоді ви отримаєте ще більш дивну поведінку: id(x) != id(x)наприклад, тому що рядок був переміщений в процесі оцінки.
DylanYoung

1
@AndreaConte, оскільки конкатенація рядків не робить зайвої роботи, щоб шукати пул усіх використовуваних рядків кожного разу, коли він генерує новий. З іншого боку, інтерпретатор "оптимізує" вираз x = "12" + "3"у x = "123"(об'єднання двох рядкових літералів в одному виразі), тому присвоєння фактично виконує пошук і знаходить той самий "внутрішній" рядок, що і для y = "123".
derenio

Насправді, це не те, що присвоєння виконує пошук, а не кожен літеральний рядок з вихідного коду стає "внутрішнім", і цей об'єкт використовується повторно у всіх інших місцях.
derenio
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.