Ответ может вас удивить

Как вы думаете, что выводит следующий код Python?

Подобные вопросы вызывают у меня желание немедленно открыть Python REPL и попробовать код, потому что я думаю, что знаю ответ, но не очень уверен в нем.

Вот мои мысли, когда я впервые увидел этот вопрос:

  • Строка flag содержит один символ.
  • Срез [::-1] переворачивает строку flag.
  • Обращение строки с одним символом такое же, как и в исходной строке.
  • Следовательно, reversed_flag должно быть ”🇺🇸”.

Это совершенно верный аргумент. Но верен ли вывод? Взглянем:

Что здесь происходит?

Действительно ли ”🇺🇸” содержит один символ?

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

Строка flag содержит один символ.

Это так? Как узнать, сколько символов содержит строка?

В Python вы можете использовать встроенную функцию len(), чтобы получить общее количество символов в строке:

Oh.

Это странно. Вы можете видеть только одну элемент в строке ”🇺🇸”, а именно флаг США, но длина 2 соответствует результату flag[::-1]. Поскольку обратная сторона ”🇺🇸” — это ”🇸🇺”, это, кажется, подразумевает, что каким-то образом ”🇺🇸” == “🇺 🇸”.

Как узнать, какие символы есть в строке?

Есть несколько разных способов увидеть все символы в строке с помощью Python:

Смайлик с флагом США — не единственный смайлик с двумя символами:

А вот и шотландский флаг:

Хорошо, о чем это все?

Задание. Сможете ли вы найти какие-либо строки, не являющиеся смайликами, которые выглядят как один символ, но на самом деле содержат два или более символов?

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

Или, возможно, более глубоко, это заставляет вас усомниться в вашем понимании термина «характер».

Что такое характер?

Термин персонаж в информатике может сбивать с толку. Его часто путают со словом символ, которое, если честно, является синонимом слова персонаж в английском просторечии.

На самом деле, когда я погуглил character computer science, самым первым результатом, который я получил, была ссылка на статью в Технопедии, в которой персонаж определяется как:

«[A] отображаемая единица информации, эквивалентная одной букве или символу алфавита».

— Технопедия, «Персонаж (Char)»

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

Второй результат Google, который я получаю, — это Википедия. В этой статье определение персонажа немного более либеральное:

Символ [A] — это единица информации, которая примерно соответствует «графеме, графемоподобной единице или символу, например, в алфавите или слоговом письме в письменной форме естественного языка».

— Википедия, «Персонаж (вычисление)»

Хм… использование слова «примерно» в определении делает определение, скажем так, неокончательным.

Но статья в Википедии продолжает объяснять, что термин «символ» исторически использовался для «обозначения определенного количества смежных битов».

Затем важный ключ к вопросу о том, как строка с одним символом может содержать два или более символов:

«Сегодня чаще всего считается, что символ относится к 8 битам (одному байту)… Все [символы] могут быть представлены одной или несколькими 8-битными кодовыми единицами с UTF-8».

— Википедия, «Персонаж (вычисление)»

ХОРОШО! Может быть, вещи начинают иметь немного больше смысла. символ – это один байт информации, представляющий единицу текста. символы, которые мы видим в строке, могут состоять из нескольких 8-битных (1 байт) кодовых единиц UTF-8.

Символы — это не то же самое, что символы. Теперь кажется разумным, что один символ может состоять из нескольких символов, как смайлики с флагами.

Но что такое кодовая единица UTF-8?

Немного дальше в статье Википедии о символах есть раздел Кодировка, в котором объясняется:

«Компьютеры и коммуникационное оборудование представляют символы, используя кодировку символов, которая присваивает каждому символу что-то — обычно целое число, представленное последовательностью цифр, — которое может храниться или передаваться по сети. Двумя примерами обычных кодировок являются ASCII и кодировка UTF-8 для Unicode».

— Википедия, «Персонаж (вычисление)»

Есть еще одно упоминание UTF-8! Но теперь мне нужно знать, что такое кодировка символов.

Что такое кодировка символов?

Согласно Википедии, кодировка символов присваивает каждому символу число. Что это значит?

Разве это не означает, что вы можете соединить каждый символ с числом? Итак, вы можете сделать что-то вроде пары каждой прописной буквы английского алфавита с целым числом от 0 до 25.

Вы можете представить эту пару с помощью кортежей в Python:

Остановитесь на мгновение и спросите себя: "Могу ли я создать список кортежей, подобный приведенному выше, без явного написания каждой пары?"

Один из способов — использовать функцию Python enumerate(). enumerate() принимает аргумент с именем iterable и возвращает кортеж, содержащий счетчик, который по умолчанию равен 0, и значения, полученные в результате итерации по iterable.

Посмотрите на enumerate() в действии:

Есть и более простой способ сделать все буквы.

Модуль Python string имеет переменную с именем ascii_uppercase, которая указывает на строку, содержащую все заглавные буквы английского алфавита:

Итак, мы связали символы с целыми числами. Это означает, что у нас есть кодировка символов!

Но как вы его используете?

Чтобы закодировать строку ”PYTHON” как последовательность целых чисел, вам нужен способ найти целое число, связанное с каждым символом. Но искать что-то в списке кортежей сложно. Это также действительно неэффективно. (Почему?)

Словари хороши для поиска вещей. Если мы преобразуем enumerated_letters в словарь, мы сможем быстро найти букву, связанную с целым числом:

Однако, чтобы закодировать строку ”PYTHON”, вам нужно найти целое число, связанное с символом. Вам нужна обратная сторона int_to_char.

Как поменять местами ключи и значения в словаре Python?

Один из способов — использовать функцию reversed() для обращения пар ключ-значение из словаря int_to_char:

Вы можете написать выражение генератора, которое переворачивает все пары в int_to_char.items(), и использовать это выражение генератора для заполнения словаря:

Хорошо, что вы соединили каждую букву с уникальным целым числом. В противном случае это обращение словаря не сработало бы. (Почему?)

Теперь вы можете кодировать строки как список целых чисел, используя словарь char_to_int и понимание списка:

И вы можете преобразовать список целых чисел в строку символов верхнего регистра, используя int_to_char в выражении генератора с помощью метода Python string `.join()`:

Но есть проблема.

Ваша кодировка не может обрабатывать строки с такими вещами, как пунктуация, строчные буквы и пробелы:

Один из способов исправить это — создать кодировку, используя строку, содержащую все строчные буквы, знаки препинания и пробельные символы, которые вам нужны.

Но в Python почти всегда есть лучший способ. Модуль Python string содержит переменную с именем printable, которая дает вам строку, содержащую целый набор печатных символов:

Включили бы вы все эти символы, если бы создавали свою собственную строку с нуля?

Теперь вы можете создавать новые словари для кодирования и декодирования символов в string.printable:

Вы можете использовать эти словари для кодирования и декодирования более сложных строк:

Теперь вы создали две разные кодировки символов! И они действительно разные. Просто посмотрите, что происходит, когда вы декодируете один и тот же список целых чисел, используя обе кодировки:

Даже не близко!

Итак, теперь мы знаем кое-что о кодировках символов:

  • Кодировка символов объединяет символы с уникальными целыми числами.
  • Некоторые кодировки символов исключают символы, включенные в другие кодировки символов.
  • Две разные кодировки символов могут декодировать одни и те же целые числа в две разные строки.

Какое отношение это имеет к UTF-8?

Что такое UTF-8?

В статье Википедии о символах упоминаются две разные кодировки символов:

«Двумя примерами обычных кодировок являются ASCII и кодировка UTF-8 для Unicode».

— Википедия, «Персонаж (вычисления)»

Итак, ASCII и UTF-8 — это особые кодировки символов.

Согласно статье Википедии об ASCII:

«ASCII была самой распространенной кодировкой символов во Всемирной паутине до декабря 2007 года, когда ее превзошла кодировка UTF-8; UTF-8 обратно совместим с ASCII».

— Википедия, "ASCII"

UTF-8 — это не просто доминирующая кодировка символов для Интернета. Это также основная кодировка символов для операционных систем Linux и macOS и даже по умолчанию для кода Python.

Фактически, вы можете видеть, как UTF-8 кодирует символы как целые числа, используя метод .encode() для строковых объектов Python. Но .encode() не возвращает список целых чисел. Вместо этого encode() возвращает bytes объект:

Документация Python описывает объект bytes как «неизменяемую последовательность целых чисел в диапазоне 0 <= x < 256». Это кажется немного странным, учитывая, что объект encoded_string отображает символы строки “PYTHON”, а не набор целых чисел.

Но давайте примем это и посмотрим, сможем ли мы как-то выделить целые числа.

В документации на Python говорится, что bytes — это последовательность, а Глоссарий Python определяет последовательность как [a]n iterable, которая поддерживает эффективный доступ к элементам с использованием целочисленных индексов.

Итак, похоже, что вы можете индексировать объект bytes так же, как вы можете индексировать объект Python list. Давайте попробуем:

Ага!

Что происходит, когда вы конвертируете encoded_string в список?

Бинго. Похоже, что UTF-8 присваивает букву ”P” целому числу 80, ”Y” целому числу 89, ”T” целому числу 84 и так далее.

Давайте посмотрим, что произойдет, если мы закодируем строку ”🇺🇸” с помощью UTF-8:

Хм. Вы ожидали, что ”🇺🇸” будет закодировано как восемь целых чисел?

”🇺🇸” состоит из двух символов, а именно “🇺” и ”🇸”. Давайте посмотрим, как они кодируются:

Хорошо, теперь все становится более понятным. И “🇺”, и ”🇸” кодируются как четыре целых числа, и четыре целых числа, соответствующие “🇺”, появляются первыми в списке целых чисел, соответствующих ”🇺🇸”, а четыре целых числа, соответствующих ”🇸”, появляются вторыми.

Однако это вызывает вопрос.

Почему UTF-8 кодирует некоторые символы как четыре целых числа, а другие — как одно целое число?

Символ “🇺” кодируется как последовательность четырех целых чисел в UTF-8, а символ ”P” кодируется как одно целое число. Почему это?

В верхней части статьи UTF-8 в Википедии есть подсказка:

«UTF-8 способен кодировать все 1 112 064 допустимых кодовых точки символов в Unicode, используя от одного до четырех однобайтовых (8-битных) кодовых единиц. Кодовые точки с более низкими числовыми значениями, которые встречаются чаще, кодируются с использованием меньшего количества байтов».

— Википедия, "UTF-8"

Итак, это звучит так, как будто UTF-8 кодирует символы не как целые числа, а как нечто, называемое кодовой точкой Unicode. И каждая кодовая единица, по-видимому, может состоять из одного-четырех байтов.

Есть пара вопросов, на которые мы должны ответить сейчас:

  1. Что такое байт?
  2. Что такое кодовая точка Unicode?

Слово «байт» часто встречается, так что давайте продолжим и дадим ему правильное определение.

Бит – это наименьшая единица информации. Бит имеет два состояния, включено или выключено, которые обычно представляются целыми числами 0 и 1 соответственно. Байт — это последовательность из восьми битов.

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

Двоичная запись может показаться довольно экзотичной при первом знакомстве. Однако это очень похоже на обычное десятичное представление, которое вы используете для записи чисел. Разница в том, что каждая цифра может быть только 0 или 1, а значение каждого разряда в числе является степенью 2, а не степенью 10:

Поскольку байт содержит восемь битов, наибольшее число, которое можно представить одним байтом, равно 11111111 в двоичном виде или 255 в десятичном представлении.

Кодировка символов, использующая один байт для каждого символа, может кодировать максимум 256 символов, поскольку максимальное 8-битное целое число равно 255, а возможные значения начинаются с 0.

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

Ну так что ты делаешь? Разрешение кодировать символы как несколько байтов кажется разумным решением, и это именно то, что делает UTF-8.

UTF-8 — это аббревиатура от Формат преобразования Unicode — 8-битный. Опять это слово Unicode.

Согласно веб-сайту Unicode:

«Unicode предоставляет уникальный номер для каждого символа, независимо от платформы, программы и языка».

— Сайт Unicode, Что такое Unicode?

Юникод огромен. Цель Unicode — обеспечить универсальное представление для всех письменных языков. Каждому символу назначается кодовая точка — причудливое слово для целого числа с некоторой дополнительной организацией — всего существует 1 112 064 возможных кодовых точки.

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

Существуют и другие кодировки для Unicode. UTF-16 делит кодовые точки Unicode на одно или два 16-битных числа и является кодировкой по умолчанию, используемой Microsoft Windows. UTF-32 может кодировать каждую кодовую точку Unicode как одно 32-битное целое число.

Но подождите, UTF-8 кодирует символы как кодовые точки, используя от одного до четырех байтов. Итак, почему символ 🇺🇸 кодируется восемью байтами?

Помните, два символа составляют эмодзи флага США: 🇺 и 🇸. Эти символы называются региональными индикаторными символами. В стандарте Unicode есть двадцать шесть региональных индикаторов, представляющих английские буквы от A до Z. Они используются для кодирования двухбуквенных кодов стран ISO 3166–1.

Вот что Википедия говорит о символах региональных индикаторов:

Они были определены в октябре 2010 года как часть поддержки Unicode 6.0 для эмодзи в качестве альтернативы кодированию отдельных символов для флага каждой страны. Хотя они могут отображаться латинскими буквами, предполагается, что реализации могут отображать их другими способами, например, с использованием национальных флагов. В «Часто задаваемых вопросах по Unicode указано, что этот механизм следует использовать и что символы национальных флагов не будут кодироваться напрямую».

— Википедия, «Обозначение регионального индикатора»

Другими словами, символ 🇺🇸 — фактически символ флага любой страны — напрямую не поддерживается Unicode. Операционные системы, веб-браузеры и другие места, где используется цифровой текст, могут выбирать отображать пары региональных индикаторов в качестве флажков.

Давайте подведем итоги того, что мы знаем на данный момент:

  • Строки символов преобразуются в последовательности целых чисел с помощью кодировки символов, обычно UTF-8.
  • Некоторые символы кодируются как одно 8-битное целое число в UTF-8, а для других требуется два, три или четыре 8-битных целых числа.
  • Некоторые символы, такие как смайлики с флагами, не кодируются Unicode напрямую. Вместо этого они представляют собой последовательности символов Unicode и могут поддерживаться или не поддерживаться каждой платформой.

Итак, когда вы переворачиваете строку, что переворачивается? Вы меняете всю последовательность целых чисел в кодировке, или вы меняете порядок кодовых точек, или что-то другое?

Как вы на самом деле переворачиваете строку?

Можете ли вы придумать способ ответить на этот вопрос с помощью эксперимента с кодом, а не пытаться найти ответ?

Ранее вы видели, что UTF-8 кодирует строку ”PYTHON” как последовательность из шести целых чисел:

Что произойдет, если вы закодируете перестановку строки ”PYTHON”?

В этом случае порядок целых чисел в списке был обратным. Но как насчет других символов?

Ранее вы видели, что символ “🇺” закодирован как последовательность из четырех целых чисел. Что происходит, когда вы кодируете его обращение?

Хм. Порядок целых чисел в обоих списках одинаков!

Давайте попробуем поменять местами строку с флагом США:

Порядок целых чисел не меняется! Вместо этого группы из четырех целых чисел, представляющие кодовые точки Unicode для 🇺 и 🇸, меняются местами. Порядок целых чисел в каждой кодовой точке остается прежним.

Что все это значит?

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

Но является ли правильным подходом изменить порядок кодовых точек, как это делает Python? Было бы разумнее сохранить символы, представленные несколькими кодовыми точками, нетронутыми? Ответ: это зависит. Не существует канонического способа перевернуть строку, по крайней мере, насколько мне известно.

Задача: как бы вы написали функцию, которая переворачивает строку, оставляя нетронутыми символы, закодированные как последовательности кодовых точек? Можете ли вы сделать это с нуля? Есть ли пакет на вашем языке, который может сделать это за вас? Как этот пакет решил проблему?

Почему все это важно?

Из этого расследования можно извлечь несколько важных уроков.

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

Во-вторых, несмотря на широкое распространение UTF-8, все еще существует множество систем, использующих различные кодировки символов. Помните об этом при чтении текста из файла, особенно при совместном использовании из другой операционной системы или из-за границы. Будьте явными и всегда указывайте, какая кодировка используется для кодирования или декодирования текста.

Например, функция Python open() имеет параметр encoding, который указывает кодировку символов, используемую при чтении или записи текста в файл. Используйте это.

Куда вы идете отсюда?

Мы рассмотрели много вопросов, но осталось еще много вопросов. Так что запишите некоторые вопросы, которые у вас еще остались, и используйте методы расследования, которые вы видели в этой статье, чтобы попытаться ответить на них.

На создание этой статьи меня вдохновил вопрос, заданный Уиллом МакГуганом в Твиттере. Загляните в ветку Уилла, чтобы узнать о куче сумасшествия, связанного с кодировкой персонажей.

Вот некоторые вопросы, которые вы, возможно, захотите изучить:

  • Когда вы преобразуете ”🏴󠁧󠁢󠁳󠁣󠁴󠁿” в список, вы получите набор строк, начинающихся с ”\U”. Что это за строки и что они представляют?
  • Кодировка UTF-8 для ”🏴󠁧󠁢󠁳󠁣󠁴󠁿” содержит целых 28 байт информации. Чем 🏴󠁧󠁢󠁳󠁣󠁴󠁿 отличается от 🇺🇸? Какие еще флаги кодируются как 28 байт?
  • Существуют ли какие-либо смайлики с флагами, которые кодируются как одна кодовая точка?
  • Что происходит, когда вы переворачиваете 🇦🇬? Учитывая то, что вы узнали о смайликах с флагами, как вы можете объяснить перестановку? Есть ли другие флаги с подобным обращением?
  • Многие платформы поддерживают цветные эмодзи, например эмодзи с большим пальцем вверх, которые могут отображаться с разными оттенками кожи. Как кодируется один и тот же символ с разными цветами?
  • Как проверить, является ли строка, содержащая эмодзи, палиндромом?

Спасибо за прочтение! Оставайтесь любопытными там!

Подпишитесь на мою бесплатную еженедельную рассылку новостей Любопытно о коде r, чтобы каждую пятницу получать порцию любопытного контента в свой почтовый ящик.