воскресенье, 27 марта 2011 г.

Интересная особенность использования subprocess

Когда-то для запуска процессов питон имел целый зоопарк различных popen* функций и классов, разбросанных по модулям os и popen2.

К счастью, начиная с версии 2.5 этот бардак был выброшен - остался единственный модуль subprocess, который и делает всю работу.

Статья относится только к POSIX системам - у Windows свои тараканы.

Поехали

Итак, запускаем процесс:

import subprocess

p = subprocess.Popen(args, shell=use_shell)

args может быть или строкой или списком строк, shell принимает False (значение по умолчанию) или True. Остальные многочисленные параметры сейчас не важны.

А теперь, внимание, вопрос: как это работает?

Привожу кусочек кода из subprocess.py, ответственный за подготовку параметров для вызова fork_exec (в разных версиях питона вызов подпроцесса делается чуть по разному - но сам принцип и приведенный ниже код не меняются):

if isinstance(args, types.StringTypes):
    args = [args]
else:
    args = list(args)

if shell:
    args = ["/bin/sh", "-c"] + args

if executable is None:
    executable = args[0]

Смотрим. Читаем еще раз, внимательно. До меня, например, дошло не с первой попытки.

Для рассматриваемых параметров args и shell возможны четыре комбинации, две из которых - не правильные. Рассмотрим их по порядку.

  1. args - список строк и shell=False. Всё работает отлично.
  2. shell=False, а args - строка. Следите за руками: строка превращается в список из одного элемента - этой самой строки args. Затем executable становится равным этому args. Всё ещё непонятно? Тогда пример:

    >>> p = subprocess.Popen('wc 1.fb2 -l')
    Traceback (most recent call last):
      ...
    OSError: [Errno 2] No such file or directory
    

    Файл, которого нет - это 'wc 1.fb2 -l'! Вот так, целиком, без разделения на параметры (процедура, сама по себе неоднозначная в общем виде).

    То есть использовать этот способ попросту нельзя.

  3. shell=True, args - строка. Опять всё хорошо.

  4. shell=True, args - список строк. Снова внимательно смотрим: shell запускает первый аргумент списка в качестве параметра. Остальные - в пролете.

    Т.е. ['ls', '-la'] транслируется в ['/bin/sh' '-c', 'ls', '-la']. Так уж /bin/sh устроен, что он напрочь игнорирует то, что идет после -c command. Не верите - проверьте сами. Правильная запись должна быть ['/bin/sh' '-c', 'ls -la'], что и получается когда args передаются строкой.

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

Итог

Возможны только две комбинации параметров. Используйте список если запускаете без shell или строку и shell:

p = subpocess.Popen(['ls', '-la'], shell=False)
p = subpocess.Popen('ls -la', shell=True)

Нарушение правила ведет к трудно отлавливаемым ошибкам.

В довесок - ссылка на баг в bugs.python.org. Надеюсь, к версии 3.3 что-нибудь поправят - а пока живём как живём. И такая ситуация будет сохраняться еще долго.

среда, 23 марта 2011 г.

Python 3: Импорт и юникод

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

Русские идентификаторы

Чуть меньше бросается в глаза тот факт, что идентификаторы тоже стали юникодными. Уважаемые читатели, если вы используете третий питон и недостаточно хорошо владеете английским - пишите по русски. Это выглядит гораздо лучше, чем убогое средство под названием "транслитерация". Оцените сами:

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 еще очень много времени. Достаточно, чтобы закончить работу и навести полный порядок.

Заключение

Я написал этот довольно длинный текст преследуя несколько целей:

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

  • Вторая - продемонстрировать, как работают кодировки применительно к файловой системе.

  • Третья - напомнить, что можно использовать русские буквы в идентификаторах. Комментарии излишни.

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

воскресенье, 20 марта 2011 г.

Импорт конфигурационных файлов

Конфигурацию можно хранить в различном виде: xml, yaml, ini и так далее.

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

Этот вариант имеет как достоинства, так и недостатки. Сейчас речь не о том. Рассмотрим, как именно подгружается конфигурация на примере Flask.

import imp

d = imp.new_module('config')
d.__file__ = filename
try:
    execfile(filename, d.__dict__)
except IOError, e:
    if silent and e.errno in (errno.ENOENT, errno.EISDIR):
        return False
    e.strerror = 'Unable to load configuration file (%s)' % e.strerror
    raise

Создается объект модуля с именем 'config', в нем прописывается путь к файлу конфигурации __file__ (каждый модуль лежащий в файловой системе должен иметь этот атрибут - помогает при поиске неисправностей).

Затем следует вызов execfile в контексте модуля конфигурации. Между прочим, execfile можно заменить на более длинную конструкцию:

with open(filename) as f:
    source = f.read()
    code = compile(source, filename, 'exec')
    exec code in d.__dict__

Как видим, тоже ничего слишком сложного: читаем содержимое файла конфигурации, компилируем его в режиме 'exec' и запускаем на словаре нашего модуля.

Почти так же работает обычный импорт модуля.

Так почему же нельзя сделать

d = imp.load_source('mod_name', filename)

сократив весь код до одной строки?

Дело в первую очередь в том, что конфигурация - это не модуль в полном смысле этого слова. Хотя технически создается полноценный объект типа "модуль" с именем 'config', этот модуль не регистрируется в общем словаре модулей sys.modules.

Соответственно его нельзя получить написав import config

И, значит, конфигурация не будет путаться под ногами, закрывая собой (возможно) честный модуль с таким же именем, лежащий в python import path.

Более того, конфигурация имеет смысл только для этого самого фреймворка Flask, остальной код ее просто-напросто не должен видеть - что мы и получили.

Если хотите, модуль конфигурации - анонимный (по аналогии с анонимными функциями).

Вызов же load_source работает немного иначе. Объект модуля будет создан как:

d = sys.modules.setdefault(mod_name, imp.new_module(mod_name))

Т.е. будет взят модуль с именем mod_name из sys.modules, если не существует - будет создан новый модуль и опять же зарегистрирован в общем каталоге. Обратите внимание, load_source работает еще и как reload, если модуль с этим именем уже был загружен.

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

Flask написан очень грамотно, Armin Ronacher на такие грабли не наступает. Чего и вам желаю.

суббота, 19 марта 2011 г.

PyMOTW - справочник примеров использования

Все знают, что читать инструкцию - полезно.

К сожалению, с практической реализацией этого принципа дела обстоят несколько хуже.

У Питона - великолепная документация, правда-правда. Она подробная, написана понятным человеческим языком, снабжена множеством примеров. И, тем не менее, хорошего не бывает слишком много.

Поэтому рекомендую Python Module Of The Week (PyMOTW). Это - великолепный сборник примеров для стандартной библиотеки питона.

Существуют и другие справочные ресурсы:

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

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

Второй вариант - загляните в PyMOTW. Найдите по указателю интересующий модуль и посмотрите на примеры использования. Быть может, ситуация прояснится и всё станет ясным. Прилагающийся код подробен до занудности. Скажем, примеры для модулей re и socket поражают своим объемом, да и другие не отстают.

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

Хвалить ресурс можно очень долго. Вы просто взгляните своими глазами и убедитесь: вещь - хороша!

пятница, 4 марта 2011 г.

Юникод

Завтра, в субботу 5 марта 2011 будет завершен переход Питона с Subversion на Mercurial (hg.python.org). Очень хорошая новость: hg определенно лучше подходит для используемого техпроцесса чем svn.

Мне захотелось посмотреть: живы ли еще юниттесты?

Скорее да, чем нет. Более чем нормальная ситуация для столь ранней альфы.

Но я хочу обратить внимание на другое. Тесты стали сплошь и рядом печатать нечто вроде:

DeprecationWarning: bytes objects are so 20th-century, it's maybe time to upgrade your code to Unicode!
По моему - очень хороший совет (если, конечно, вы не провели предлагаемую операцию еще несколько лет назад).