Идентификатор строки не меняется при изменении строки на Python. Сложность добавления символа в строку - PullRequest
0 голосов
/ 06 мая 2020

Я думал, что Python идентификатор строки должен меняться каждый раз после изменения строки. Но я обнаружил, что реальное поведение отличается. Например, не все строки кода ниже вывода различны:

In [1]: s = 'a' 
   ...: for i in range(20): 
   ...:     print(id(s)) 
   ...:     s += 'a' 

Out[1]: 139687167358256
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687049975984
   ...: 139687066878640
   ...: 139687066878640
   ...: 139687066878640
   ...: 139687066878640
   ...: 139687066878640

Вот почему я решил, что ядро ​​Python пытается оптимизировать код и запускает странные манипуляции со строками в памяти. Еще одним аргументом в пользу этого предположения является тот факт, что идентификаторы констант идут с сегментами, размер которых равен степени 2:

In [2]: s = 'a' 
   ...: prev = id(s) 
   ...: count = 1 
   ...: for i in range(150): 
   ...:     s += 'a' 
   ...:     cur = id(s) 
   ...:     if cur != prev: 
   ...:         print(len(s), count) 
   ...:         count = 1 
   ...:     else: 
   ...:         count += 1 
   ...:     prev = cur 

Out[2]: 2 1
   ...: 16 14
   ...: 32 16
   ...: 48 16
   ...: 64 16
   ...: 80 16
   ...: 96 16
   ...: 112 16
   ...: 128 16
   ...: 144 16

Но во всем этом есть еще одна странность. Давайте посмотрим, что произойдет с размерами сегментов при увеличении размера строки:

In [3]: s = 'a' 
   ...: prev = id(s) 
   ...: count = 1 
   ...: for i in range(10 ** 9): 
   ...:     s += 'a' 
   ...:     cur = id(s) 
   ...:     if cur != prev: 
   ...:         print(len(s), count) 
   ...:         count = 1 
   ...:     else: 
   ...:         count += 1 
   ...:     prev = cur 

Out[3]:
   ...: 2 1
   ...: 16 14
   ...: 32 16
   ...: 48 16
   ...: 64 16
   ...: 80 16
   ...: 96 16
   <...>
   ...: 448 16
   ...: 464 16
   ...: 472 8
   ...: 504 32
   ...: 536 32
   ...: 568 32
   ...: 600 32
   ...: 648 48
   ...: 1048 400
   ...: 1272 224
   ...: 1336 64
   ...: 1416 80
   ...: 1512 96
   ...: 1544 32
   ...: 1832 288
   ...: 1864 32
   ...: 1880 16
   ...: 1992 112
   ...: 2040 48
   ...: 2104 64
   ...: 2152 48
   ...: 2216 64
   ...: 39512 37296
   ...: 752776 713264
   ...: 753592 816
   ...: 1511352 757760
   ...: 3026872 1515520
   ...: 6057912 3031040
   ...: 6062008 4096
   ...: 6066104 4096
   ...: 6070200 4096
   <...>
   ...: 8396728 4096
   ...: 16797624 8400896
   ...: 16801720 4096
   <...>
   ...: 33537976 4096
   ...: 33542072 4096
   ...: 67088312 33546240
   ...: 67092408 4096
   ...: 67096504 4096
   ...: 67100600 4096
   ...: 67104696 4096
   ...: 67108792 4096
   ...: 67112888 4096
   ...: 134229944 67117056
   ...: 268464056 134234112
   ...: 536932280 268468224

Наконец, мы можем попытаться приблизительно оценить сложность добавления символа в конец строки. Я снова подумал, что сложность добавления n символов в строку в al oop составляет O (n ^ 2). Но мои эксперименты показывают, что это O (n):

In [4]: def foo(n): 
   ...:     c = time() 
   ...:     s = 'a' 
   ...:     for i in range(n): 
   ...:         s += 'a' 
   ...:     return time() - c 

In [5]: foo(10 ** 6) / foo(10 ** 3)                                                                                                                                                                                                                
Out[5]: 1124.5325443786983

Я не могу объяснить такое поведение, особенно когда строка становится достаточно длинной. Так может ли кто-нибудь мне с этим помочь? Желательно поподробнее, пожалуйста, если есть возможность.

UPD:

  1. С кортежами вроде не бывает. Их идентификаторы меняются на каждой l oop итерации.

  2. Замена строкового литерала 'a' на переменную со значением 'a' не меняет поведения с постоянными идентификаторами. Таким образом, нельзя заменять s += 'a' на s = s + 'a'. Но! Замена s = s + 'a' на s = 'a' + s приводит к изменению id на каждой l oop итерации.

  3. После замены литерала 'a' переменной с именем a Я предположил, что добавление вызова функции в тело l oop может изменить ситуацию (потому что функция может иметь побочный эффект для переменной a). Итак, я попытался вызвать «формальную» функцию (которая ничего не сделала) между объявлением a и строкой s = s + a. И это не имело значения. Наконец, я попытался добавить строку непостоянной длины в s, но случайную длину:

In [4]: s = 'a' 
   ...: prev = id(s) 
   ...: count = 1 
   ...: for i in range(10 ** 8): 
   ...:     s = s + 'a' * randint(1, 100) 
   ...:     cur = id(s) 
   ...:     if cur == prev: 
   ...:         count += 1 
   ...:     else: 
   ...:         print(len(s), count) 
   ...:         count = 1 
   ...:     prev = cur 


Out[4]:
   ...: 37 1
   ...: 76 1
   ...: 154 1
   ...: 187 1
   ...: 268 1
   ...: 288 1
   ...: 305 1
   ...: 344 2
   ...: 380 1
   ...: 438 1
   ...: 527 1
   ...: 612 2
   ...: 639 1
   ...: 817 2
   ...: 888 2
   ...: 984 3
   ...: 1077 2
   ...: 1166 2
   ...: 1267 2
   ...: 1378 2
   ...: 1641 5
   ...: 1777 2
   ...: 2164 9
   ...: 2509 5
   ...: 2750 6
   ...: 3394 14
   ...: 3674 5
   ...: 4030 5
   ...: 4077 3
   ...: 4569 10
   ...: 4868 5
   ...: 5700 14
   ...: 6840 23
   ...: 8278 25
   ...: 136672 2541
   ...: 19397763 381558
   ...: 19398587 18
   ...: 19402713 84
   ...: 19406810 81
   ...: 19410889 81
   ...: 19415002 82
   ...: 19419075 83
   ...: 19423225 80
   ...: 19427293 70
   ...: 19431357 88
   <And so on...>
Если мы будем использовать s += 'a', но с вызовом функции ha sh из s, после этого идентификаторы будут меняться на каждой итерации.

1 Ответ

0 голосов
/ 06 мая 2020

Предполагая, что вы используете Python 3, , это поведение не определяется Python языком .

На самом деле стандартные состояния Python 3 :

Строки являются неизменяемыми последовательностями кодовых точек Unicode. [...] Также нет изменяемого строкового типа, но str.join () или io.StringIO можно использовать для эффективного создания строк из нескольких фрагментов .

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

Что касается id, стандартных состояний Python :

Возвращает «идентичность» объекта. Это целое число, которое гарантированно будет уникальным и постоянным для этого объекта в течение его времени жизни . Два объекта с неперекрывающимся временем жизни могут иметь одинаковое значение id ().

CPython детали реализации: это адрес объекта в памяти .

Итак, вот трюк: результаты вызова id для одной итерации могут совпадать или не совпадать с результатами предыдущей итерации. Это undefined , потому что объект, на который ссылается s, отличается от объекта, на который ссылались в предыдущей итерации (поскольку на предыдущую строку больше не ссылаются, ее можно освободить и ее время жизни закончится).

Интерпретатор CPython внутренне перерабатывает строковый объект в памяти, чтобы работать быстрее (в основном, чтобы избежать выделения памяти). Возникающий в результате побочный эффект , возможно, является более сложной задачей. Однако альтернативные реализации Python могут не выполнить этот трюк. Например, PyPy не .

Таким образом, используйте str.join() или io.StringIO для быстрой конкатенации (как стандартные состояния Python) и не полагайтесь на неопределенное поведение или CPython детали реализации как это поведение может измениться в будущих версиях интерпретатора .

...