Третий питон с рождения замечательно поддерживает юникод. Собственно говоря, это одна из самых заметных его особенностей.
Русские идентификаторы
Чуть меньше бросается в глаза тот факт, что идентификаторы тоже стали юникодными. Уважаемые читатели, если вы используете третий питон и недостаточно хорошо владеете английским - пишите по русски. Это выглядит гораздо лучше, чем убогое средство под названием "транслитерация". Оцените сами:
def функция(агрумент):
коэффициент = 5
return агрумент * коэффициент
Это на самом деле здорово!
Еще один не вполне очевидный момент: имена модулей тоже могут быть в юникоде:
from . import вспомогательный_модуль
Тоже выглядит неплохо, верно? Есть только одна небольшая проблема: это не всегда работает. Вернее, на Windows возможны неприятности. И не нужно заявлять, что вопросы, касающиеся самой популярной на сегодняшний день операционной системы - никого не волнуют. Подавляющее большинство разработчиков самого Питона Windows не используют - и тем не менее Питон обязан на ней работать, и работать хорошо.
Чтобы рассказать в чем вышла загвоздка - я должен немного погрузиться в детали.
Юникод в C API
В Python 2 немалая часть Python C API принимала char *
там, где
требовалась строка. Поскольку str
и был последовательностью байт -
сложностей не возникало.
При переносе кода на Python 3 нужно было с этим что-то делать: str
стал юникодным типом, последовательностью символов.
Но в С нет удобного типа для unicode! Вернее, существует стандартный
тип
wchar_t
,
который обременен множеством проблем. Главные из них: в разных
реализациях этот тип имеет различный размер: 16 бит для
UCS-2 и 32 бита для
UCS-4. К тому же Windows (о,
снова она) не поддерживает UCS-2 в полной мере (UCS-4 не поддерживает
совсем).
Хуже всего то, что на некоторых платформах этот wchar_t
попросту не
определен.
Таким образом, использовать wchar_t
в Python C API нельзя.
Сам Питон вводит тип Py_UNICODE
для этих целей. Но и тут не все
гладко. Этот тип не входит в Limited API (PEP
384).
Кроме того, разработчики не хотели радикально заменить все char *
на
что-то другое.
Есть еще и вопрос практического удобства: ведь очень здорово писать
ret = PyObject_GetAttrString(obj, "attribute");
Для wchar_t
все гораздо сложнее, далеко не все компиляторы
поддерживают строковые юникодные константы.
В свете вышеописанных причин Python C API продолжает использовать
char *
, считая, что эти строки имеют кодировку
UTF-8 если явно не указано иное.
Т.е. прототипы функций C API выглядят как:
PyObject *
PyImport_ImportModuleLevel(char *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);
Это - импорт модуля с именем name
, которое передается как UTF-8
строка, аналог питоновской функции __import__
.
И эта функция - лишь верхушка используемого механизма. В процессе
импорта вызываются довольно много внутренних закрытых функций - и
везде используются переменные вроде char *name
в качестве имен
модулей. В кодировке UTF-8, еще раз напомню.
А ведь имя модуля транслируется в путь к файлу! А кодировака файловой
системы может отличаться от UTF-8. Счастливые пользователи Linux
давно об этом забыли - в подавляющем большинстве систем по умолчанию
как кодировка пользователя (переменная окружения LANG
) так и
файловой системы установлены в UTF-8 и проблем нет совсем. Но в
общем случае это не всегда так.
Кодировки по умолчанию
Чуть-чуть о кодировках. Для определения используемых по умолчанию
кодировок в питоне существуют три функции: sys.getdefaultencoding
,
sys.getfilesystemencoding
и locale.getpreferredencoding
.
-
sys.getdefaultencoding()
- кодировка по умолчанию, используемая в питоновских исходниках. Для третьего питона всегда равна UTF-8. Это - та самая кодировка, которую можно перекрыть написав в начале файла# -*- encoding: utf-8 -*-
-
sys.getfilesystemencoding()
- кодировка файловой системы. Например, дляf = open('path/to/file', 'r')
значение
'path/to/file'
имеет типstr
(юникод). Лежащая в основе функция из clib имеет прототипint open(const char *pathname, int flags, mode_t mode);
Значит,
'path/to/file'
должен быть преобразован вchar *
используя кодировкуsys.getfilesystemencoding()
. Конечно, в Python C API есть специальные функции для этого. -
locale.getpreferredencoding()
- предпочтительная для пользователя кодировка. Она устанавливается в региональных настройках и к файловой системе прямого отношения не имеет.
Теперь снова вспомним нашу горячо любимую Windows.
locale.getpreferredencoding()
возвращает 'cp1251'
- Windows
настроена на русский язык. Кодировка для консоли
(sys.stdout.encoding
) другая, это 'cp866'
- что добавляет сумбура
в и без того запутанную проблему. Ну да ладно, не будем отвлекаться.
sys.getfilesystemencoding()
возвращает 'mbcs'
. И вот здесь
начинаются основные чудеса. Обратите внимание, mbcs - это не
cp1251. Равно как и не cp1252 или какая другая кодировка. mbcs -
это нечто совершенно особенное!
Multibyte character set (кодировка MBCS)
При преобразовании mbcs -> unicode используется кодировка из
locale.getpreferredencoding()
, преобразование однозначное и проблем
не вызывает.
Для обратного преобразования unicode -> mbcs тоже используется
locale.getpreferredencoding()
(cp1251 в нашем случае). Но
cp1251 не может описать любой юникодный символ. А mbcs - хитрый и
коварный. Если для символа не существует точного преобразования -
используется ближайший похожий по начертанию.
Это непросто понять без примера. Давайте возьмем французское слово comédie и попробуем преобразовать его в mbcs, имея руский язык cp1251 в настройках по умолчанию.
Возьмем Python 3.1:
>>> b = b'com\xc3\xa9die'
>>> s = b.decode('utf8')
>>> s.encode('mbcs')
b'comedie'
Посмотрите, какая прелесть! Для символа é в русской раскладке cp1251 нет подходящего аналога. Но ведь английская буква e так похожа: нужно лишь убрать умляут (англ. umlaut, французы зовут этот знак accent aigu). Так и получили преобразование comédie -> comedie без единой ошибки.
А теперь представьте, что это - имя файла. Результат будет следующим: файл на диске есть, и так как в Windows файловая система юникодная, имя файла будет записано правильно, по французски. Но преобразование unicode -> mbcs даст несколько другое имя, которого на диске нет.
В результате получается изумительная по своей красоте ситуация:
f = open('comédie', 'r')
будет говорить, что файла нет - а на самом деле вот же он, красавец!
Справедливости ради нужно упомянуть, что в Python 3.2 поведение mbcs
немного поменялось, и 'comédie'.encode('mbcs')
вызовет
UnicodeEncodeError
. Дело в том, что mbcs стал использовать режим
strict по умолчанию. Чтобы повторить функциональность 3.1 следует
указывать режим replace: 'comédie'.encode('mbcs', 'replace')
Юникодная файловая система
С mbcs мы разобрались и выяснили, что для работы с файловой системой эта кодировка в общем случае непригодна. Т.е. если я буду использовать русские имена файлов на русской Windows - всё будет хорошо. Но открыть этот файл у американца или голландца не выйдет. Что же делать?
В Windows помимо open есть еще и функция
FILE *_wfopen(const wchar_t *filename, const wchar_t *mode);
которая принимает wchar_t *
и позволяет использовать оригинальное
юникодное имя файла без всяких преобразований. Существует целый набор,
начинающийся с _w
- на все случаи жизни.
Значит, нужно делать следующее: для Windows использовать юникодные
версии функций работы с файлами, а для всех остальных операционных
систем применять .encode(sys.getfilesystemencoding())
.
Реализация модуля io
начиная с версии 3.1 так и поступает.
И снова импорт русских названий
Всё отлично за одним маленьким исключением - механизм импорта не
использует io
! Исторически сложилось так, что имя импортируемого
модуля довольно быстро преобразовывается в
sys.getfilesystemencoding()
(с возможными ошибками и потерями, о
которых я писал выше) и в таком виде пронизывает весь очень непростой
и громоздкий код, чтобы попасть в функции стандартной библиотеки C.
Добавьте к этому довольно большой объем платформозависимого кода (на Маке все работает совсем не так, как на Linux) и проблему обратной совместимости (даже после объявления части API устаревшей она должна поддерживаться как минимум в двух следующих выпусках) - и вы сможете представить сложность и объемность задачи.
Так вот, после трехлетнего труда (с небольшими перерывами, естественно - это же добровольный некоммерческий Open Source) Victor Stinner завершил требуемое нелегкое преобразование. Довольно незаметный, но очень важный шаг!
Файловые пути стали храниться в PyObject*
(на самом деле это,
конечно, str
- PyUnicodeObject
), работающая с ними часть C API
имеет суффикс Object. Например:
PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);
Сравните с PyImport_ImportModuleLevel
. Все функции из старого API
стали тонкими обертками над новыми вариантами. Так,
PyImport_ImportModuleLevel
создает PyObject
из name
и вызывает
PyImport_ImportModuleLevelObject
.
Эти старые функции оставлены для сохранения обратной совместимости, сам Питон их уже не использует.
Если быть честным, именно Windows поддержка чуть-чуть не готова - но до выхода Python 3.3 еще очень много времени. Достаточно, чтобы закончить работу и навести полный порядок.
Заключение
Я написал этот довольно длинный текст преследуя несколько целей:
-
Пожалуй, главная из них - показать, насколько порой незначительные внешне изменения способны перевернуть внутреннюю реализацию, и как нелегко их проделать не сломав того, что уже отлично работает пятнадцать лет.
-
Вторая - продемонстрировать, как работают кодировки применительно к файловой системе.
-
Третья - напомнить, что можно использовать русские буквы в идентификаторах. Комментарии излишни.
-
И, наконец, очень хотелось отметить завершение отлично выполненной работы, которая делает Питон немного лучше.
Опечатка в строке "Хуже всего то, что на некоторых платформах этот whar_t попросту не определен." в слове whar_t.
ОтветитьУдалитьСпасибо, исправил.
ОтветитьУдалитьВ питоне-двойке попытка открыть файл как f = open(u'comédie', 'r') не делает преобразование имени файла в mbcs и использует нативный юникодный API.
ОтветитьУдалитьДа, все верно. Собственно говоря, в тройке тоже используется юникодный Windows API. Проблемы были непродолжительное время и только для 3.0.
ОтветитьУдалитьЯ использовал пример с open как иллюстрацию для import, где все довольно запущено (было).
К тому же очень хотелось рассказать, какая прелесть этот mbcs - а то большинство народу и не знает.
ага, приколы с mbcs вылазят если пытаться работать в двойке на винде не используя юникодное апи. так например os.listdir('.') и os.listdir(u'.') будут давать разные результаты на русской винде для умляутов, точно как ты и рассказывал.
ОтветитьУдалитья даже это показывал разработчиками hg чтобы показать, что надо использовать юникодное апи для работы с файловой системой, но был отправлен в сад, потому что у них на линуксах и так все чудесно. ну и флаг им в руки.
hg вообще ненавидит юникод.
ОтветитьУдалитьНасколько помню, были проблемы даже с commit messages.
Заглянул в код, чтобы убедиться - так нет, вроде бы починили. Файлы - так и остались. Быть может, и до них дело когда-нибудь дойдет.
Насколько я понимаю, http://mercurial.selenic.com/wiki/EncodingStrategy - описание текущего состояния дел.
Спасибо, хорошая статья. Подписался на блог.
ОтветитьУдалитьКушайте на здоровье.
ОтветитьУдалитьУмляут во французском называется trema, но и это не он. Это accent aigu. (Это я чтобы в хорошей статье не было ошибок.)
ОтветитьУдалитьСпасибо.
ОтветитьУдалитьЯ лишь знаю, что по английски этот знак называется umlaut.
Поправил.
Еще бы в GAE появился третий питон, а то помнится именно ералаш с русским языком отвратил меня и от того и от другого.
ОтветитьУдалитьНа самом деле статья об именах файлов. Обычно программисты (особенно серверные) имеют дело лишь с английскими названиями.
ОтветитьУдалить