Internowanie stringów w Pythonie

92

Chociaż to pytanie nie ma żadnego rzeczywistego zastosowania w praktyce, jestem ciekawy, jak Python dokonuje internowania ciągów. Zauważyłem, co następuje.

>>> "string" is "string"
True

To jest tak, jak się spodziewałem.

Ty też możesz to zrobić.

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

I to całkiem sprytne!

Ale nie możesz tego zrobić.

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

Dlaczego Python miałby nie ocenić s1+"g"i zdać sobie sprawę, że to to samo, co s2i skierować go pod ten sam adres? Co właściwie dzieje się w tym ostatnim bloku, że wraca False?

Ze'ev G.
źródło

Odpowiedzi:

95

Jest to specyficzne dla implementacji, ale twój interpreter prawdopodobnie internalizuje stałe czasu kompilacji, ale nie wyniki wyrażeń czasu wykonywania.

W dalszej części używam CPythona 2.7.3.

W drugim przykładzie wyrażenie "strin"+"g"jest oceniane w czasie kompilacji i zastępowane przez "string". To sprawia, że ​​pierwsze dwa przykłady zachowują się tak samo.

Jeśli zbadamy kody bajtowe, zobaczymy, że są dokładnie takie same:

  # 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)

Trzeci przykład obejmuje konkatenację w czasie wykonywania, której wynik nie jest automatycznie internowany:

  # 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        

Gdybyś miał ręcznie intern()wpisać wynik trzeciego wyrażenia, otrzymałeś ten sam obiekt co poprzednio:

>>> s3a = "strin"
>>> s3 = s3a + "g"
>>> s3 is "string"
False
>>> intern(s3) is "string"
True
NPE
źródło
22
A dla przypomnienia: optymalizacja peep-hole Pythona będzie wstępnie obliczyć operacje arytmetyczne na stałe ( "string1" + "s2", 10 + 3*20etc.) w czasie kompilacji, ale ograniczenia wynikające sekwencje do zaledwie 20 elementów (w celu zapobieżenia [None] * 10**1000z nadmiernie rozszerza swoją kodu bajtowego). To właśnie ta optymalizacja zapadła "strin" + "g"się "string"; wynik jest krótszy niż 20 znaków.
Martijn Pieters
13
I żeby to było podwójnie jasne: w ogóle się tu nie odbywa staż. Niezmienne literały są zamiast tego przechowywane jako stałe z kodem bajtowym. Internowanie ma miejsce w przypadku nazw używanych w kodzie, ale nie w przypadku wartości łańcuchowych utworzonych przez program, chyba że intern()funkcja jest specjalnie internowana .
Martijn Pieters
9
Dla tych, którzy próbują znaleźć internfunkcję w Pythonie 3 - zostaje przeniesiona do sys.intern
Timofey
1

Przypadek 1

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

Przypadek 2

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

Teraz twoje pytanie brzmi, dlaczego identyfikator jest taki sam w przypadku 1, a nie w przypadku 2.
W przypadku 1, przypisałeś literał łańcuchowy "123"do xi y.

Ponieważ ciągi znaków są niezmienne, sensowne jest, aby interpreter przechował literał ciągu tylko raz i wskazywał wszystkie zmienne na ten sam obiekt.
Dlatego widzisz identyfikator jako identyczny.

W przypadku 2 modyfikujesz xza pomocą konkatenacji. Zarówno xi yma te same wartości, ale nie samą tożsamość.
Oba wskazują na różne obiekty w pamięci. Stąd mają inny idi iszwrócony operatorFalse

cppcoder
źródło
Dlaczego, skoro łańcuchy są niezmienne, przypisanie x + "3" (i szukanie nowego miejsca do przechowywania łańcucha) nie przypisuje się do tego samego odniesienia co y?
nicecatch
Ponieważ wtedy musi porównać nowy ciąg ze wszystkimi istniejącymi ciągami; potencjalnie bardzo kosztowna operacja. Mógłby to zrobić w tle po przypisaniu, jak przypuszczam, aby zmniejszyć pamięć, ale wtedy skończyłoby się to jeszcze dziwniejszym zachowaniem: id(x) != id(x)na przykład, ponieważ ciąg został przesunięty w procesie oceny.
DylanYoung
1
@AndreaConte, ponieważ konkatenacja ciągów nie wykonuje dodatkowej pracy polegającej na wyszukiwaniu w puli wszystkich używanych ciągów za każdym razem, gdy generuje nowy. Z drugiej strony interpreter „optymalizuje” wyrażenie x = "12" + "3"do x = "123"(konkatenacja dwóch literałów łańcuchowych w jednym wyrażeniu), więc przypisanie faktycznie przeprowadza wyszukiwanie i znajduje ten sam „wewnętrzny” ciąg, co for y = "123".
derenio
Właściwie nie jest tak, że to przypisanie dokonuje wyszukiwania, a nie każdy literał ciągu z kodu źródłowego zostaje „zinternalizowany”, a obiekt zostaje ponownie użyty we wszystkich innych miejscach.
derenio