Я думал, что 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:
С кортежами вроде не бывает. Их идентификаторы меняются на каждой l oop итерации.
Замена строкового литерала 'a' на переменную со значением 'a' не меняет поведения с постоянными идентификаторами. Таким образом, нельзя заменять s += 'a'
на s = s + 'a'
. Но! Замена s = s + 'a'
на s = 'a' + s
приводит к изменению id на каждой l oop итерации.
После замены литерала '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
, после этого идентификаторы будут меняться на каждой итерации.