Немного скучных вещей, которые вам необходимо знать перед погружением

      Комментарии к записи Немного скучных вещей, которые вам необходимо знать перед погружением отключены

Знаете ли вы, что у народа острова Бугенвиль самый короткий алфавит в мире? Алфавит языка ротокас состоит всего из 12 букв: A, E, G, I, K, O, P, R, S, T, U, и V. На другом конце этой своеобразной числовой оси расположились такие языки, как китайский, японский и корейский, насчитывающие тысячи символов. Английский, конечно, содержит всего 26 букв — 52, если считать буквы и верхнего, и нижнего регистров — плюс горстка знаков пунктуации !@#$%.

Когда люди говорят «текст», они подразумевают «буквы и символы на экране компьютера». Но компьютеры не работают с буквами и символами; они работают с битами и байтами. Каждый фрагмент текста, который вы когда-либо видели на экране компьютера, на самом деле хранится в определенной кодировке. Грубо говоря, кодировка символов обеспечивает соответствие того, что вы видите на экране и того, что на самом деле хранится в памяти или на диске. Существует много различных кодировок символов, некоторые из них оптимизированы для конкретных языков, например русского, китайского или английского, другие могут быть использованы сразу для нескольких языков.

В действительности, все гораздо сложнее. Многие символы являются общими для нескольких кодировок, но каждая кодировка может использовать свою последовательность байтов для хранения их в памяти или на диске. Вы можете думать о кодировке символов, как о разновидности криптографического ключа. Всякий раз, когда вам передают последовательность байтов — файл, веб-страницу, все равно — и утверждают, что это «текст», вам необходимо понять, какая кодировка использовалась. Зная кодировку, вы сможете декодировать байты в символы. Если вам дают неправильный ключ или не дают ключа вовсе, вам не остается ничего, кроме как попытаться взломать код самостоятельно. Скорее всего, в результате вы получите кучу крякозябров (gibberish — тарабарщина, невнятная речь, но так понятнее, прим. перев.). Все, что вы знали о строках — неверно.

Все, что вы знали о строках — неверно.

Несомненно, вам приходилось видеть такие веб-страницы, со странными вопросительными знаками на месте апострофов. Обычно это означает, что автор страницы неправильно указал их кодировку, вашему браузеру осталось просто угадать ее, а результатом стала смесь ожидаемых и совершенно неожиданных символов. В английском языке это просто раздражает; в других языках результат может стать совершенно нечитаемым.

Существуют кодировки для всех основных мировых языков. Но, поскольку, языки существенно отличаются друг от друга, а память и дисковое пространство раньше были дорогими, каждая кодировка оптимизирована для конкретного языка. Под этим я подразумеваю то, что для представления символов своего языка, все кодировки используют один и тот же диапазон чисел (0-255). Например, вы вероятно знакомы с кодировкой ASCII, которая хранит символы английского языка в виде чисел от 0 до 127. (65 — заглавная «A», 97 — строчная «a» и т.д.) Английский алфавит очень простой и может быть представлен менее, чем 128 числами. Если вам известна двоичная система счисления, вы понимаете, что в байте задействовано всего 7 битов из 8.

Западноевропейские языки, такие как французский, испанский и немецкий содержат больше символов, чем английский. Точнее, они содержат символы с различными диакритическим знаками, например, испанский символ n. Самая распространенная кодировка для этих языков — CP-1252, также известная как «windows-1252», что связано с широким использованием ее в операционной системе Microsoft Windows. В кодировке CP-1252 символы с номерами от 0 до 127 такие же, как и в ASCII, а остальной диапазон используется для таких символов как n-с-тильдой-сверху (241), u-с-двумя-точками-сверху (252) и т.д. Однако, это все еще однобайтная кодировка; максимально возможный номер 255 еще помещается в один байт.

А еще существуют такие языки, как китайский, японский и корейский, которые имеют так много символов, что они требуют многобайтовых кодировок. Это означает, что каждый «символ» представляется двухбайтовым числом от 0 до 65535. Но различные многобайтные кодировки всё равно имеют ту же проблему, что и различные однобайтные кодировки: каждая кодировка использует одинаковые числа для обозначения разных вещей. Отличие лишь в том, что диапазон чисел больше, потому что нужно кодировать намного больше символов.

Это было вполне нормально в несетевом мире, где вы набирали «текст» для себя и иногда распечатывали его. В этом мире не было ничего кроме «обычного текста» (не знаю, м.б. быть вообще не переводить “plain text” — прим. перев.). Исходный код был в кодировке ASCII, а для всего остального использовались текстовые процессоры, которые определяли свои собственные (нетекстовые) форматы, в которых, наряду с информацией о форматировании, отслеживалась и информация о кодировке. Люди читают эти документы с помощью такого же текстового процессора, какой использовался и для их создания, так что, все более или менее работало.

Теперь подумайте о распространении глобальных сетей, таких как e-mail и web. Множество «обычного текста» перемещается вокруг планеты, создаётся на одном компьютере, передаётся через второй и принимается и отображается третьим компьютером. Компьютеры видят только числа, но числа могут иметь различное значение. О нет! Что же делать? Системы были спроектированы таким образом, чтобы передавать информацию о кодировке вместе с каждым отрывком «обычного текста». Вспомните, это криптографический ключ, устанавливающий соответствие между числами и понятными человеку символами. Потерянный ключ означает искаженный текст или кракозябры, если не хуже.

Теперь подумайте о задаче хранения различных отрывков текста в одном месте, например в одной таблице базы данных, хранящей все когда-либо полученные вами email сообщения. Вам всё ещё нужно хранить кодировку вместе с каждым отрывком текста, чтобы иметь возможность прочитать его. Думаете, это трудно? Попробуйте реализовать поиск в этой базе данных, с преобразованием на лету между множеством кодировок. Разве это не забавно?

Теперь подумайте о возможности многоязычных документов, где символы из нескольких языков находятся рядом в одном документе. (Подсказка: программы, которые пытаются делать это, обычно используют коды смены алфавита для переключения «режимов». Бац, и вы в русском режиме KOI8-R, и 241 означает Я; бац, и теперь вы в греческом режиме для Macintosh, и 241 означает ?.) И конечно вы так же захотите осуществлять поиск по этим документам.

Теперь плачьте, т.к. все, что вы знали о строках — неверно, и нет такого понятия «обычный текст».

Юникод

Введение в Юникод.

Юникод спроектирован для представления системой каждого символа любого языка. Юникод представляет каждую букву, символ или идеографию как 4-х байтное число.Каждое число представляет уникальный символ, используемый по крайней мере в одном из языков в мире. (используются больше чем 65535 из них, таким образом 2 байта не были бы достаточны.) У символов, которые используются в разных языках, один код, если нет хорошей этимологической причины. Независимо от всего, есть точно 1 код сответствующий символу, и точно 1 символ соответствующий числовому коду. Каждый код всегда означает только один символ; нет никаких «режимов». U+0041 всегда соответствует ‘A’, даже если в вашем языке нету символа ‘A’.

На первый взгляд, это великолепная идея. Одна кодировка для всего. Множество языков в одном документе. Не надо больше никаких «переключений режимов» для смены кодировок. Но у вас должен возникнуть очевидный вопрос. Четыре байта? На каждый символ? Это кажется ужасно расточительным, особенно для таких языков, как английский или испанский, в которых нужно меньше одного байта (256 чисел) для представления любого возможного символа. На самом деле, это расточительно даже для иероглифических языков (таких как китайский), в которых никогда не нужно больше, чем два байта на символ.

Существует кодировка Юникод, которая использует четыре байта на символ. Она называется UTF-32, так как 32 бита = 4 байтам. UTF-32 — прямолинейная кодировка; каждому символу Юникод (4-байтовому числу) соответствует символ с определенным номером. Это имеет свои преимущества, самое важное из которых заключается в том, что вы можете найти N-ый символ в строке за постоянное время, так как N-ый символ начинается в 4*N байте. Но эта кодировка также имеет и недостатки, самый очевидный из которых — для хранения каждого символа требуется четыре байта.

Хотя в Юникоде существует огромное количество символов, на самом деле большинство людей никогда не используют те, номера которых выше 65535 (2^16). Поэтому существует другая кодировка Юникода, называемая UTF-16 (очевидно что 16 бит = 2 байтам). UTF-16 кодирует каждый символ в номерах 0–65535; для представления же редко используемых запредельных символов с номерами выше 65535 приходится прибегать к некоторым уловкам. Самое очевидное преимущество: UTF-16 дважды эффективнее по потреблению памяти, нежели UTF-32, так как каждый символ требует 2 байта вместо 4-х (кроме случаев с теми самыми запредельными символами). И, как и в случае с UTF-32, можно легко отыскать нужный N-ый символ в строке за постоянное время, если вы уверены, что текст не содержит запредельных символов; и всё хорошо, если это действительно так.

Тем не менее существуют неочевидные недостатки, как UTF-32, так и UTF-16. На различных компьютерных платформах отдельные байты хранятся по-разному. Это означает, что символ U+4E2D может быть сохранён в UTF-16 либо как 4E 2D, либо как 2D 4E (задом наперёд), в зависимости от используемого порядка байт: big-endian или little-engian. (Для UTF-32 видов порядков даже больше.) Пока вы храните свои документы исключительно у себя на компьютере, вы в безопасности — различные приложения на одном компьютере всегда используют один и тот же порядок. Но в тот момент, когда вы захотите передать документы на другой компьютер в Интернете или иной сети, вам понадобится способ пометки документа, какой у вас используется порядок байт. В противном случае, принимающая документ система не имеет понятия, что представляет последовательность байт 4E 2D: U+4E2D или U+2D4E.

Для решения этой проблемы многобайтовые кодировки Юникода имеют “отметку о порядке байт” (BOM), которая представляет собой специальный непечатный символ, который вы можете включить в начало документа для сохранения информации о используемом порядке байт. Для UTF-16, эта отметка имеет номер U+FEFF. Если вы принимаете документ с UTF-16, который начинается с байт FF FE, — это однозначно оповещает о прямом порядке; если же начинается с байт FE FF, следовательно порядок обратный.

На самом деле, UTF-16 не идеален, особенно если вы имеете дело с большим количеством символов ASCII. Вы не думали о том, что даже китайская веб-страница может содержать большое количество символов ASCII — все элементы и атрибуты, окружающие печатные китайские символы (и на них тоже тратится 2 байта, хотя они и умещаются в один). Возможность искать N-ый символ в строке за постоянное время заманчива, однако до сих пор существует надоевшая всем проблема с теми запредельными символами, которая заключается в том, что вы не можете гарантировать, что каждый символ хранится точно в двух байтах, вследствие чего поиск за постоянное время также становится невозможным (если вы только не имеете отдельный индекс по символам). Открою вам секрет: и до сих пор в мире существует огромное число ASCII текстов…

Кое-кто до вас тоже задумывался над этой проблемой и пришёл вот к такому решению:

UTF-8

UTF-8 это кодировка Юникода с переменным числом байт. Это означает, что различные символы занимают разное число байт. Для символов ASCII (A-Z, цифр и т.п.) UTF-8 использует только 1 байт на символ (действительно, а больше и не требуется). Причём и на деле для них зарезервированы точно те самые номера, как и в ASCII; первые 128 символов (0–127) таблицы UTF-8 неотличимы от той же части ASCII. “Расширенные” символы, такие как n и o занимают два байта. (bytes are not simply the Unicode code point like they would be in UTF-16; there is some serious bit-twiddling involved.) Китайские символы, такие как 中 занимают три байта. Самые редко используемые символы — четыре.

Недостатки: так как каждый символ занимает различное число байт, поиск N-го символа обладает сложностью O(N), что означает, что время поиска пропорционально длине строки. Кроме того, bit-twiddling, применяемый для кодирования символов в байты, также увеличивает время поиска. (прим. перев. в кодировке с фиксированным числом байт на символ время поиска составляет O(1), то есть оно не зависит от длины строки).

Преимущества: крайне эффективное кодирование наиболее часто используемых символов ASCII. Не хуже, чем хранение расширенных символов в UTF-16. Лучше, чем UTF-32 для китайских символов. Также (не хочу грузить вас математикой, так что вам придётся поверить мне на слово), в связи с самой природой bit twiddling, проблемы с порядком байт просто не существует. Документ, закодированный в UTF-8, использует один и тот же порядок байт на любом компьютере!

Погружение

В языке программирования Python 3 все строки представляют собой последовательность Unicode символов. В Python нет такого понятия, как строка в кодировке UTF-8, или строка в кодировке CP-1251. Некорректным является вопрос: Это строка в UTF-8? UTF-8 — это способ закодировать символы в последовательность байт. Если Вы хотите взять строку и превратить её в последовательность байт в какой-либо кодировке, то Python 3 может помочь Вам в этом. Если же Вы желаете превратить последовательность байт в строку, то и здесь Python 3 Вам пригодится. Байты — это не символы, байты — это байты. Символы — это абстракция. А строка — это последовательность таких абстракций.

s = ‘深入 Python’ ?
len(s) ?
9
s[0] ?
‘深’
s + ‘ 3’ ?
‘深入 Python 3’

  • ? Чтобы создать строку окружите её кавычками. В Python строки можно создавать как с помощью одинарных (‘), так и с помощью двойных кавычек ().
  • ? Стандартная функция len() возвращает длину строки, т.е. количество символов в ней. Эта же функция используется для определения длины списков, кортежей, множеств и словарей. Строка в данном случае похожа на кортеж символов.
  • ? Так же как и со списками, Вы можете получить произвольный символ из строки, зная его индекс.
  • ? Так же как и со списками, Вы можете объединять строки, используя оператор +.

Форматирование строк

Строки можно создавать как с помощью одинарных, так и с помощью двойных кавычек.

Давайте взглянем еще раз на humansize.py :

SUFFIXES = {1000: [‘KB’, ‘MB’, ‘GB’, ‘TB’, ‘PB’, ‘EB’, ‘ZB’, ‘YB’], ?
1024: [‘KiB’, ‘MiB’, ‘GiB’, ‘TiB’, ‘PiB’, ‘EiB’, ‘ZiB’, ‘YiB’]}

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
»’Convert a file size to human-readable form. ?

Keyword arguments:
size — file size in bytes
a_kilobyte_is_1024_bytes — if True (default), use multiples of 1024
if False, use multiples of 1000

Returns: string

»’ ?
if size0:
raise ValueError(‘number must be non-negative’) ?

multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
for suffix in SUFFIXES[multiple]:
size /= multiple
if sizemultiple:
return ‘{0:.1f} {1}’.format(size, suffix) ?

raise ValueError(‘number too large’)

  • ? ‘KB’, ‘MB’, ‘GB’… — это все строки.
  • ? Комментарии к функции — это тоже строка. Комментарии к функции могут быть многострочными, поэтому используются тройные кавычки вначале и в конце строки.
  • ? Эти тройные кавычки заканчивают комментарии к функции.
  • ? Здесь еще одна строка, которая передается конструктору исключения как удобочитаемый текст ошибки.
  • ? Здесь … ого, это ещё что такое?

Python 3 поддерживает форматирование значений в строки. Форматирование может включать очень сложные выражение. Самое простое использование — это вставка значения в поле подстановки строки.

username = ‘mark’
password = ‘PapayaWhip’ ?
{0}’s password is {1}.format(username, password) ?
mark’s password is PapayaWhip

  • ? Вы же не думаете, что мой пароль действительно PapayaWhip
  • ? Здесь много чего происходит. Во первых, вызывается метод format(…) для строки. Строки — это объекты, а у объектов есть методы. Во вторых, значением всего выражения будет строка. В третьих, {0} и {1} являются полями, которые заменяются аргументами, переданными методу format()

Составные имена полей

Предыдущий пример показал простейший способ форматирования строк: поля в строке представляют из себя целые числа. Эти числа в фигурных скобках означают порядковые номера аргументов в списке параметрах метода format(). Это означает, что {0} заменяется первым аргументом (в данном случаем username), а {1} заменяется на второй аргумент (password), c. Вы можете иметь столько номеров полей, сколько аргументов есть у метода format(). А аргументов может быть сколько угодно. Но имена полей гораздо более мощный инструмент, чем может показаться на первый взгляд.

import humansize
si_suffixes = humansize.SUFFIXES[1000] ?
si_suffixes
[‘KB’, ‘MB’, ‘GB’, ‘TB’, ‘PB’, ‘EB’, ‘ZB’, ‘YB’]
‘1000{0[0]} = 1{0[1]}’.format(si_suffixes) ?
‘1000KB = 1MB’

  • ? Вместо того, чтобы вызывать какие-либо функции модуля humansize, Вы просто используете один из словарей, которые в этом модуле определены: список суффиксов СИ (степени 1000)
  • ? Этот кусок выглядит сложным, хотя это и не так. {0} ссылается на первый аргумент, переданный методу format() (переменная si_suffixes). Но si_suffixes — это список. Поэтому {0[0]} ссылается на первый элемент этого списка: ‘KB’. В тоже время {0[1]} ссылается на второй элемент того же списка: ‘MB’. Все, что находится за фигурными скобками — включая 1000, знак равенства, и пробелы — остается нетронутым. В результате мы получим строку ‘1000KB = 1MB’.

{0} is replaced by the 1st format() argument. {1} is replaced by the 2nd.

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

  • Передать список и получить доступ к элементу списка по его индексу (как в предыдущем примере);
  • Передать словарь и получить доступ к значению словаря по его ключу;
  • Передать модуль и получить доступ к его переменным и функциям зная их имена;
  • Передать экземпляр класса и получить доступ к его свойствам и методам по их именам;
  • Любая комбинация выше перечисленных.

И чтобы взорвать ваш мозг, вот пример которые использует все вышеперечисленные возможности:

import humansize
import sys
‘1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}’.format(sys)
‘1MB = 1000KB’

Вот как это работает:

Модуль sys содержит информацию об работающем интерпретаторе Python. Так как Вы его импортировали, то можете использовать в качестве аргумента метода format(). То есть поле {0} ссылается на модуль sys. sys.modules представляет из себя словарь со всеми модулями, которые на данный момент импортированы интерпретатором Python. Ключи этого словаря — это строки с именами модулей; значения — объекты, представляющие импортированные модули. Таким образом поле {0.modules} ссылается на словарь импортированных модулей. sys.modules[‘humansize’] — это объект, представляющий собой модуль humansize, который Вы только что импортировали. Таким образом составное поле {0.modules[humansize]} ссылается на модуль humansize. Заметьте, что синтаксис здесь отличается. В синтаксисе Python ключи словаря sys.modules являются строками, и чтобы обратиться к значениям словаря необходимо окружить кавычками имя модуля (например ‘humansize’). Вот цитата из PEP 3101: Расширенное форматирование строк: Правила для парсинга ключей очень простые. Если он начинается с цифры, то его нужно интерпретировать как число. Иначе — это строка. sys.modules[‘humansize’].SUFFIXES — это словарь, определенный в самом начале модуля humansize. Поле {0.modules[humansize].SUFFIXES} ссылается на этот словарь.

sys.modules[‘humansize’].SUFFIXES[1000] — это список суффиксов системы СИ: [‘KB’, ‘MB’, ‘GB’, ‘TB’, ‘PB’, ‘EB’, ‘ZB’, ‘YB’]. Таким образом поле {0.modules[humansize].SUFFIXES[1000]} ссылается на этот список. А sys.modules[‘humansize’].SUFFIXES[1000][0] — это первый элемент списка суффиксов: ‘KB’. Таким образом окончательное составное поле {0.modules[humansize].SUFFIXES[1000][0]} заменяется на строку из двух символов KB.

Описатели формата

Постойте! Есть еще кое-что. Давайте взглянем на еще одну странную строку из humansize.py:

if sizemultiple:
return ‘{0:.1f} {1}’.format(size, suffix)

{1} заменяется на второй аргумент метода format(), то есть на значение переменной suffix. Но что означает {0:.1f}? Здесь две вещи: {0}, которой Вы уже знаете и :.1f, о которой Вы еще не слышали. Вторая половина (двоеточие и все что после него) описывает формат, который уточняет каким образом замещающее значение должно быть отформатировано.

? Описатель формата позволяет Вам модифицировать замещающий текст многими полезными способами, как функция printf() в языке программирования C. Вы можете добавить заполнение нулями или пробелами, горизонтальное выравнивание текста, контролировать десятичную точность и даже конвертировать числа в 16-ричную систему.

Внутри замещаемого поля символ двоеточие (:) и все что идет после него означает описатель формата. Описатель формата .1 означает округлить до десятых (то есть показывать только один знак после запятой). Описатель формата f означает число с фиксированной запятой (fixed-point number) (в отличие от экспоненциального или какого-либо другого представления десятичных чисел). Таким образом если переменная size имеет значение 698.24 а suffix — ‘GB’, форматированная строка получится ‘698.2 GB’, потому что число 698.24 округлено до одного знака после запятой, и к нему добавлен суффикс.

‘{0:.1f} {1}’.format(698.24, ‘GB’)
‘698.2 GB’

За всеми деталями описателей формата обратитесь в раздел Format Specification Mini-Language официальной документации Python 3.

Другие общие методы строк

Помимо форматирования строки позволяют делать множество полезных трюков.

s = »’Finished files are the re- ?
… sult of years of scientif-
… ic study combined with the
… experience of years.»’
s.splitlines() ?
[‘Finished files are the re-‘,
‘sult of years of scientif-‘,
‘ic study combined with the’,
‘experience of years.’]
print(s.lower()) ?
finished files are the re-
sult of years of scientif-
ic study combined with the
experience of years.
s.lower().count(‘f’) ?
6

  • ? В интерактивной оболочке Python Вы можете вводить многострочный текст. Такой текст начинается с тройного символа кавычек. А когда Вы нажмете ENTER интерактивная оболочка предложит Вам продолжить вводить текст. Заканчиваться многострочный текст должен также тройным символом кавычек. Когда Вы нажмете ENTER интерактивная оболочка Python выполнит команду (запишет текст в переменную s).
  • ? Метод splitlines() берет многострочный текст и возвращает список строк, по одной на каждую строку оригинального текста. Заметьте, что символы перевода строки не добавляются в результирующие строки.
  • ? Метод lower() переводит все символы строки в нижний регистр. (Аналогично метод upper() переводит строку в верхний регистр.)
  • ? Метод count() подсчитывает количество появлений подстроки. Да, в этом предложении 6 букв f.

Вот еще один часто встречающийся случай. Пусть у Вас есть список пар ключ-значение в виде key1=value1key2=value2, и Вы хотите разделить их и получить словарь в виде {key1: value1, key2: value2}.

query = ‘user=pilgrimdatabase=masterpassword=PapayaWhip’
a_list = query.split(») ?
a_list
[‘user=pilgrim’, ‘database=master’, ‘password=PapayaWhip’]
a_list_of_lists = [v.split(‘=’, 1) for v in a_list] ?
a_list_of_lists
[[‘user’, ‘pilgrim’], [‘database’, ‘master’], [‘password’, ‘PapayaWhip’]]
a_dict = dict(a_list_of_lists) ?
a_dict
{‘password’: ‘PapayaWhip’, ‘user’: ‘pilgrim’, ‘database’: ‘master’}

  • ? Метод split() принимает один аргумент, разделитель, и разбивает строку по разделителям на список строк. В данном случае разделителем выступает аперсанд (), но разделитель может быть каким угодно.
  • ? Теперь у Вас есть список строк, каждая из которых состоит из ключа, знака = и значения. Мы можем использовать генераторы списков чтобы пройтись по всему списку и разбить каждую строку в месте первого знака = на две строки: ключ и значение. (Теоретически значение также может содержать знак равенства. Если просто сделаем ‘key=value=foo’.split(‘=’)), то получим список из трех элементов [‘key’, ‘value’, ‘foo’].)
  • ? Наконец Python может превратить этот список в словарь используя функцию dict().

? Предыдущий пример похож на грамматический разбор параметров в URL, в реальной жизни такой разбор намного сложнее. Если Вам необходимо работать с параметрами URL, то лучше использовать функцию urllib.parse.parse_qs(), которая умеет обрабатывать некоторые неочевидные специфические случаи.

Разрезание строк

Как только Вы создали строку, Вы можете получить любую её часть как новую строку. Это называется разрезание строк. Разрезание работает также как срезы для списков, что вполне логично, так как строки это те же последовательности символов.

a_string = ‘My alphabet starts where your alphabet ends.’
a_string[3:11] ?
‘alphabet’
a_string[3:-3] ?
‘alphabet starts where your alphabet en’
a_string[0:2] ?
‘My’
a_string[:18] ?
‘My alphabet starts’
a_string[18:] ?
‘ where your alphabet ends.’

Статьи к прочтению:

Приколы в играх | WDF 89 | Скучный выпуск


Похожие статьи: