Руководство по интернированию строк в Python

Введение Одна из первых вещей, с которой вы сталкиваетесь при изучении основ программирования, - это концепция строк. Подобно различным языкам программирования, строки Python представляют собой массивы байтов, представляющие символы Unicode - массив или последовательность символов. Python, в отличие от многих языков программирования, не имеет отдельного символьного типа данных, и символы считаются строками длины 1. Вы можете определить строку, используя одинарные или двойные кавычки, например, a = "Hello World"

Вступление

Одна из первых вещей, с которой вы сталкиваетесь при изучении основ программирования, - это концепция строк. Подобно различным языкам программирования, строки Python представляют собой массивы байтов, представляющие символы Unicode - массив или последовательность символов. Python, в отличие от многих языков программирования, не имеет определенного символьного типа данных, и символы считаются строками длиной 1.

Вы можете определить строку, используя одинарные или двойные кавычки, например, a = "Hello World" или a = 'Hello World' . Чтобы получить доступ к определенному элементу строки, вы должны использовать квадратные скобки ( [] ) с индексом символа, к которому вы хотите получить доступ (индексирование начинается с 0). Например, вызов a[0] H

При этом давайте взглянем на этот пример кода:

 a = 'Hello World' 
 b = 'Hello World' 
 c = 'Hello Worl' 
 
 print(a is b) 
 print(a == b) 
 print(a is c+'d') 
 print(a == c+'d') 

Все сравниваемые строки содержат значение Hello World ( a , b и c +'d' ). Было бы интуитивно понятно предположить, что вывод будет True для всех этих операторов.

Однако, когда мы запускаем код, это приводит к:

 True 
 True 
 False 
 True 

Что может показаться неинтуитивным в этом выводе, так это то, что a is c + 'd' возвращает False , в то время как очень похожий оператор a is b возвращает True . Таким образом, мы можем заключить, что a и b - это один и тот же объект, а c - другой, даже если они имеют одинаковое значение.

Если вы не знакомы с различием между == и is - is проверяет, ссылаются ли переменные на один и тот же объект в памяти , а == проверяет, имеют ли переменные одинаковое значение .

Это различие между a , b и c является результатом интернирования строк .

Примечание . Среда, в которой вы запускаете код, влияет на то, как работает интернирование строк. Предыдущие примеры были результатом выполнения кода как сценария в неинтерактивной среде с использованием последней версии Python (версия 3.8.5). Поведение будет отличаться при использовании консоли / Jupyter из-за разных способов оптимизации кода или даже между разными версиями Python.

Это связано с тем, что разные среды имеют разные уровни оптимизации.

Строка интернирования

Строки в Python - это неизменяемые объекты. Это означает, что после создания строк мы не можем их изменять или обновлять. Даже если кажется, что строка была изменена, под капотом была создана копия с измененным значением и присвоена переменной, в то время как исходная строка осталась прежней.

Попробуем изменить строку:

 name = 'Wtack Abuse!' 
 name[0] = 'S' 

Поскольку name строки неизменяемо, этот код завершится ошибкой в последней строке:

 name[0] = 'S' 
 TypeError: 'str' object does not support item assignment 

Примечание: если вы действительно хотите изменить конкретный символ строки, вы можете преобразовать строку в изменяемый объект, например list , и изменить желаемый элемент:

 name = 'Wtack Abuse!' 
 name = list(name) 
 name[0] = 'S' 
 # Converting back to string 
 name = "".join(name) 
 
 print(name) 

Что дает нам желаемый результат:

 Stack Abuse! 

Причина, по которой мы могли изменить символ в списке (а не в строке), заключается в том, что списки изменяемы, что означает, что мы можем изменять их элементы.

Интернирование строк - это процесс сохранения в памяти только одной копии каждого отдельного строкового значения.

Это означает, что когда мы создаем две строки с одним и тем же значением

  • вместо того, чтобы выделять память для них обеих, только одна строка фактически сохраняется в памяти. Другой просто указывает на то же место в памяти.

Учитывая эту информацию, вернемся к исходному примеру Hello World

 a = 'Hello World' 
 b = 'Hello World' 
 c = 'Hello Worl' 

Когда a строка a, компилятор проверяет, присутствует ли Hello World во внутренней памяти. Так как это первое вхождение этого строкового значения, Python , создает объект и кэширует эту строку в памяти и указывает этой ссылке. a

Когда b создается, Hello World обнаруживается компилятором во внутренней памяти, поэтому вместо создания другой строки b просто указывает на ранее выделенную память.

строковые значения python впамяти{.ezlazyload}

a is b а в данном случае a == b

Наконец, когда мы создаем строку c = 'Hello Worl' , компилятор создает экземпляр другого объекта во внутренней памяти, потому что он не может найти тот же объект для ссылки.

Когда мы сравниваем a и c+'d' , последнее оценивается как Hello World . Однако, поскольку Python не выполняет интернирование во время выполнения, вместо него создается новый объект. Таким образом, поскольку ни интернирование не было сделано, эти два не тот же объект , и is возвращает False .

В отличие от оператора is == сравнивает значения строк после вычисления выражений времени выполнения Hello World == Hello World .

В то время как a и c+'d' одинаковы по значению, так что это возвращает True .

Проверка

Посмотрим идентификатор созданных нами строковых объектов. Функция id(object) в Python возвращает идентификатор object , который гарантированно будет уникальным в течение всего времени существования указанного объекта. Если две переменные указывают на один и тот же объект, вызывающий id вернет одно и то же число:

 letter_d = 'd' 
 
 a = 'Hello World' 
 b = 'Hello World' 
 c = 'Hello Worl' + letter_d 
 d = 'Hello Worl' + 'd' 
 
 print(f"The ID of a: {id(a)}") 
 print(f"The ID of b: {id(b)}") 
 print(f"The ID of c: {id(c)}") 
 print(f"The ID of d: {id(d)}") 

Это приводит к:

 The ID of a: 16785960 
 The ID of b: 16785960 
 The ID of c: 17152424 
 The ID of d: 16785960 

Только c имеет другой идентификатор. Все ссылки теперь указывают на объект с тем же значением Hello World Однако c вычислялась не во время компиляции, а во время выполнения. Даже d , который мы сгенерировали путем добавления 'd' теперь указывает на тот же объект, a указывают a и b

Как струны интернированы

В Python есть два способа интернирования строк на основе взаимодействия программиста:

  • Неявное интернирование
  • Явное интернирование

Неявное интернирование

Python автоматически обрабатывает некоторые строки в момент их создания. Будет ли строка интернирована или нет, зависит от нескольких факторов:

  • Интернируются все пустые строки и строки длины 1.

  • Вплоть до версии 3.7 Python использовал оптимизацию глазка, и все строки длиной более 20 символов не интернировались. Однако теперь он использует оптимизатор ASTÂ , и (большинство) строк длиной до 4096 символов интернированы.

  • Имена функций, классов, переменных, аргументов и т. Д. Неявно интернируются.

  • Ключи словарей, используемые для хранения атрибутов модуля, класса или экземпляра, интернированы.

  • Строки интернируются только во время компиляции, это означает, что они не будут интернированы, если их значение не может быть вычислено во время компиляции.

    • Эти строки будут интернированы, например:
    1
    
    <!-- -->
    
     a = 'why' 
     b = 'why' * 5 
    
    • Следующее выражение вычисляется во время выполнения, поэтому строка не интернируется.
    1
    
    <!-- -->
    
     b = "".join(['w','h','y']) 
    
  • Строки, содержащие символы помимо ASCII, скорее всего, интернироваться не будут.

Если вы помните, мы говорили, что 'Hello Worl' + letter_d было вычислено во время выполнения, и поэтому оно не будет интернировано. Поскольку не существует единого стандарта интернирования строк, хорошим практическим правилом является идея времени компиляции / времени выполнения, когда вы можете предположить, что строка будет интернирована, если ее можно вычислить во время компиляции.

Явное интернирование

Мы часто сталкиваемся со строками, которые не лежат в условиях неявного интернирования в Python, но есть способ интернировать любую строку, которую вы хотите. В модуле sys есть функция под intern(immutable_object) , эта функция сообщает Python, что нужно сохранить immutable_object (строка в нашем случае) в таблице интернированной памяти.

Вы можете интернировать любую строку следующим образом:

 import sys 
 c = sys.intern('Hello World'+'!') 

Мы видим, что это сработает в нашем предыдущем примере:

 import sys 
 
 letter_d = 'd' 
 
 a = sys.intern('Hello World') 
 b = sys.intern('Hello Worl' + letter_d) 
 
 print(f"The ID of a: {id(a)}") 
 print(f"The ID of b: {id(b)}") 
 print(f"a is b? {a is b}") 

Дала бы вывод:

 The ID of a: 26878464 
 The ID of b: 26878464 
 a is b? True 

Теперь, когда мы знаем, как и какие строки интернируются в Python. Остается один вопрос - почему было введено интернирование строк?

Преимущества интернирования строк

Интернирование строк имеет несколько преимуществ:

  • Сохранение памяти: нам никогда не нужно сохранять в памяти два строковых объекта по отдельности, если они одинаковы. Каждая новая переменная с тем же содержимым просто указывает на ссылку в интернированном табличном литерале. Если по какой-то причине вы хотите иметь список, содержащий каждое слово и его появление в книге Джейн Остин «Гордость и предубеждение» , без явного интернирования вам потребуется 4,006,559 байта, а с явным интернированием каждого слова вам понадобится только 785,509 байт памяти.
  • Быстрые сравнения: сравнение интернированных строк намного быстрее, чем неинтернированных строк, что полезно, когда в вашей программе много сравнений. Это происходит потому, что для сравнения интернированных строк вам нужно только сравнить, совпадают ли их адреса в памяти, а не сравнивать содержимое.
  • Быстрый поиск в словаре: если ключи поиска интернированы, сравнение может быть выполнено путем сравнения указателя вместо сравнения строк, которое работает по тому же принципу, что и предыдущий пункт.

Недостатки интернирования строк

Однако у интернирующих строк есть некоторые недостатки, которые следует учитывать перед использованием:

  • Стоимость памяти: в случае, если ваша программа имеет большое количество строк с разными значениями и относительно меньшее количество сравнений в целом, потому что сама интернированная таблица потребляет память. Это означает, что вы хотите интернировать строки, если у вас относительно мало строк и много сравнений между ними.
  • Затраты времени: вызов функции intern() стоит дорого, так как он должен управлять интернированной таблицей.
  • Многопоточные среды: интернированная память (таблица) - это глобальный ресурс в многопоточной среде, синхронизацию которой необходимо изменить. Эта проверка может потребоваться только при доступе к интернированной таблице, т. Е. При создании новой строки, но это может быть дорогостоящим.

Заключение

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

Всегда не забывайте добавлять комментарии или документацию, если вы используете интернирование строк, чтобы другие члены команды знали, как работать со строками в программе.

Хотя результаты могут различаться в зависимости от реализации вашего интерпретатора Python, а также от среды, в которой вы запускаете свой код, вам определенно следует поэкспериментировать с intern() чтобы вам было комфортно с ней работать. Эта концепция может помочь вам улучшить дизайн и производительность вашего кода. Это также может помочь вам на следующем собеседовании.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus