вторник, 12 июля 2011 г.

Переполнение стека

На первый взгляд у Питона очень простая и прозрачная работа со стеком. Каждый новый вложенный вызов функции (на самом деле исполнение code block, но кому нужны эти детали) увеличивает внутренний счетчик, каждый выход — уменьшает. Если счетчик доходит до 1000 (значение по умолчанию) — выбрасывается RuntimeError с текстом «maximin recursion depth exceeded».

Допустимая глубина регулируется sys.getrecursionlimit / sys.setrecursionlimit.

Очень простая и понятная схема, в которой тем не менее есть серьезная проблема. Рассмотрим такой код:

with zipfile.ZipFile('filename.zip', 'w') as f:
    f.writestr('file.txt', get_text_data())

Допустим, вызов get_text_data выбросил исключение. Тогда ZipFile.__exit__ должен закрыть архив, записав все нужные структуры. Это — довольно большой кусок кода с многочисленными вложенными вызовами.

А мы и так уже находимся у самого края, стек почти весь вышел. Скорее всего в таком случае ZipFile.__exit__ (который в свою очередь вызывает ZipFile.close и т.д.) вместо нормального закрытия файла сам вывалится с RuntimeError «maximin recursion depth». Обработчик ошибок сломался, породив новое исключение.

То же самое может произойти при использовании try/finally или try/except. В результате существующее поведение выглядит очень странным. На самом деле нет безопасного способа делать что-либо при переполнении стека — любое неловкое движение приведет к новому переполнению. То, как поступает Питон (выбрасывание исключения) абсолютно бесполезно и может только запутывать логику обработки ошибок. Проще, наглядней и надежней было бы просто завершать интерпретатор в аварийном режиме.

В python 3 ситауцию кардинально исправили. В случае переполнения стека все так же выбрасывается RuntimeError. Но питон гарантирует обработчикам (всему коду, который будет выполнен до выхода из frame, поймавшего исключение) запас в 50 уровней стека — а это более чем достаточно.

Глубина «добавочного стека» не регулируется. Это — принципиально. Важно дать всем третьесторонним библиотекам возможность нормально завершить свои дела. И при этом не важно, какие настройки стека выставила использующая их программа.

Если обработчики не вложились в добавочные 50 вызовов — Питон аварийно закрывается.

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

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

понедельник, 4 июля 2011 г.

GIL и обработка сигналов

Небольшое добавление к статье о GIL.

Эта часть касается только поведения Питона на posix системах. Я проводил эксперимент на Linux, но на FreeBSD и MacOS результат должен быть тем же. На Windows свои тараканы, к последующему изложению не имеющие никакого отношения.

Описание проблемы

Итак, имеем простой код:

import threading

threads = []

running = True

def f():
    while running:
        pass

for i in range(1):
    th = threading.Thread(target=f)
    threads.append(th)
    th.start()

for th in threads:
    th.join()

Что произойдет, если после запуска пользователь нажмет <Ctrl+C>? Будет послан сигнал SIGINT, который мы не умеем обрабатывать.

Отлично, добавим нужное:

import threading
import signal

threads = []

running = True

def f():
    while running:
        pass

for i in range(1):
    th = threading.Thread(target=f)
    threads.append(th)
    th.start()

def sig_handler(sig_num, frame):
    print('SIGNAL')
    global running
    running = False

signal.signal(signal.SIGINT, sig_handler)

for th in threads:
    th.join()

Всё заработало?

Зависит от версии Питона. На 3.2 действительно всё отлично.

Python 2.7 не желает вызывать зарегистрированный обработчик сигнала. print не печатается, потоки не останавливаются.

В чём же дело?

Обработка сигналов в Питоне

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

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

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

Есть еще одна особенность. Стандарт posix заявляет, что обработчик сигнала должен выполняться в главном потоке (том самом, который создается при запуске процесса). Разные операционные системы (и даже разные версии ядра linux) расширяют эти строгие рамки, но Питон в данном вопросе предельно консервативен — должен быть главный поток и всё тут!

Для этого Питон временно сбрасывает порог переключения GIL со 100 (или сколько там выставлено в sys.setcheckinterval до 1 в надежде быстро добраться до главного потока и асинхронно выполнить в нём свой питоновский код, зарегистрированный как обработчик сигнала.

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

Так вот, Питон быстро молотит, переключая GIL в стремлении добраться до главного потока. Который, в свою очередь, упёрся в Thread.join.

А в питоне этот .join реализован через threading.Condition(threading.Lock()), а не через pthread_join. Так нужно — у питона поверх pthreads есть своя необходимая логика.

В результате главный поток блокирован Lock.acquire. Смотрим, что там происходит.

Открываем файл Python/thread_pthread.h из Python 2.7 и обращаем внимание на функцию PyThread_acquire_lock:

int PyThread_acquire_lock(PyThread_type_lock lock, int waitflag) {
    int success;
    sem_t *thelock = (sem_t *)lock;
    int status, error = 0;

    do {
        if (waitflag)
            status = fix_status(sem_wait(thelock));
        else
            status = fix_status(sem_trywait(thelock));
    } while (status == EINTR); /* Retry if interrupted by a signal */

    if (waitflag) {
        CHECK_STATUS("sem_wait");
    } else if (status != EAGAIN) {
        CHECK_STATUS("sem_trywait");
    }

    return (status == 0) ? 1 : 0;
}

Очень мило: если блокирующий вызов (sem_wait или sem_trywait) возвращает EINTR (произошло прерывание по сигналу) — игнорируем это и ждём дальше.

Беда в том, что весь этот код происходит при отпущенном GIL.

То есть управление не возвращается в PyEval_EvalFrameEx, единственное место где главный поток может получить управление и переключить GIL на себя, обрабатывая питоновский код и попутно выполняя обработчик сигнала.

Еще раз.

Переключение на главный поток происходит, но не доходит до переключения GIL и выполнения питоновского кода.

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

Python 3.2

Теперь смотрим на ту же функцию в новом исполнении:

PyLockStatus
PyThread_acquire_lock_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds,
                            int intr_flag)
{
    PyLockStatus success;
    sem_t *thelock = (sem_t *)lock;
    int status, error = 0;
    struct timespec ts;

    if (microseconds > 0)
        MICROSECONDS_TO_TIMESPEC(microseconds, ts);
    do {
        if (microseconds > 0)
            status = fix_status(sem_timedwait(thelock, &ts));
        else if (microseconds == 0)
            status = fix_status(sem_trywait(thelock));
        else
            status = fix_status(sem_wait(thelock));
        /* Retry if interrupted by a signal, unless the caller wants to be
           notified.  */
    } while (!intr_flag && status == EINTR);

    /* Don't check the status if we're stopping because of an interrupt.  */
    if (!(intr_flag && status == EINTR)) {
        if (microseconds > 0) {
            if (status != ETIMEDOUT)
                CHECK_STATUS("sem_timedwait");
        }
        else if (microseconds == 0) {
            if (status != EAGAIN)
                CHECK_STATUS("sem_trywait");
        }
        else {
            CHECK_STATUS("sem_wait");
        }
    }

    if (status == 0) {
        success = PY_LOCK_ACQUIRED;
    } else if (intr_flag && status == EINTR) {
        success = PY_LOCK_INTR;
    } else {
        success = PY_LOCK_FAILURE;
    }

    return success;
}

Добавилась поддержка таймаутов (и это само по себе здорово).

И, самое главное, появился новый параметр intr_flag. Если он установлен, то функция вернет управление наверх, с указанием, что произошел вызов сигнала. Дальше питон уже разберётся, что нужно сделать переключение GIL и дать возможность главному потоку запустить обработчики сигналов.

Теперь это стало стандартным поведением. Ядро Питона везде использует intr_flag, а старый способ остался для поддержки обратной совместимости со старым Python C API. Раздражающая вещь эта обратная совместимость, но без неё никуда...

Что делать, если приходится писать на Python 2.x?

Нужно давать возможность выполниться питоновскому коду в главном потоке до того, как посчитали всю работу законченной и приступили к освобождению ресурсов (.join).

Т.е. следует организовать другую схему сигнализации о завершении потоков, не упирающуюся в безнадежные блокировки (и сделать её перед .join, чтобы тот гарантировано «проскочил» проблемное место).

Решения могут быть разными.

В 2.7 нет ожидания по таймауту — но есть неблокирующие способы завладеть объектом threading.Lock. И если не получилось — поспать немного. Пример смотрите в реализации threading.Condition.wait для Python 2.7.

Или задействовать «непрофильно» select.select — например, создав на каждый рабочий поток локальный socket, в который этот самый поток запишет что-нибудь, а главный поток проснется и поймет, что вся работа закончена. У select есть таймаут, и это тоже может пригодится.

В конце концов, главному потоку можно просто уходить в time.sleep(1) и затем проверять флаги, устанавливаемые работниками по завершению своего грязного дела. Одна секунда ожидания — достаточно долго, чтобы ваш компьютер не грелся до неприличных температур и достаточно мало, чтобы исполняемый процесс сумел отреагировать с не слишком большой задержкой.

Заключение

Проблема в обработке сигналов есть, и теперь вы об этом знаете.

Как вы будете её решать — дело ваше. Предупреждён — значит вооружён.

Лучший способ — перейти на python 3.2. Если это невозможно по ряду причин — остаются другие, не столь элегантные но вполне рабочие решения.

Загадочный GIL

Наверное, каждый питонист слышал про существование Global Interpreter Lock (GIL). Обычно знание предмета исчерпывается фразой: "Это - та самая гадость, которая не позволяет запустить одновременно как минимум два потока, задействовав все имеющиеся ядра современного процессора".

Высказывание отчасти верное, но совершенно неконструктивное и не покрывающее всей многогранности рассматриваемого вопроса.

Позвольте мне пройтись по теме более подробно, рассмотрев вопросы GIL и реализации Питоном многопоточности с разных сторон.

Короткое определение

Прежде всего GIL — это блокировка, которая обязательно должна быть взята перед любым обращением к Питону (а это не только исполнение питоновского кода а еще и вызовы Python C API). Строго говоря, единственные вызовы, доступные после запуска интерпретатора при незахваченном GIL — это его захват. Нарушение правила ведет к мгновенному аварийному завершению (лучший вариант) или отложенному краху программы (куда более худший и труднее отлаживаемый сценарий).

Многопоточный код на питоне

Это — самый простой уровень. Имеем обычную программу, состоящую исключительно из питоновских модулей (.py файлы) и не содержащую Python C Extensions. Пусть в ней работают два потока: главный и запущенный нами.

import threading
import time

running = True

def f(delay):
    while running:
        time.sleep(delay)

th = threading.Thread(target=f, args=(0.5,))
th.start()

for i in range(10):
    time.sleep(0.7)

running = False

th.join()

Многопоточная программа на С не должна как-то отдельно регистрировать свои потоки — достаточно вызова API (pthread_create или CreateThread) для запуска потока. Интерпретатор питона для своей работы требует ряда структур. Давайте рассмотрим их подробнее.

Структуры интерпретатора, обеспечивающие многопоточную работу

PyInterpreterState содержит глобальное состояние интерпретатора: загруженные модули modules, указатель на первый (он же главный) поток tstate_head и кучу других важных для внутренней кухни вещей.

struct PyInterpreterState {
    PyInterpreterState *next;
    PyThreadState *tstate_head;

    PyObject *modules;
    PyObject *sysdict;
    PyObject *builtins;
    PyObject *modules_reloading;

    PyObject *codec_search_path;
    PyObject *codec_search_cache;
    PyObject *codec_error_registry;
};

PyThreadState позволяет узнать, какой кадр стека (frame) исполняется и какой номер у потока с точки зрения операционной системы. Остальные атрибуты сейчас не важны.

struct PyThreadState {
    PyThreadState *next;
    PyInterpreterState *interp;

    PyFrameObject *frame;
    int recursion_depth;

    Py_tracefunc c_profilefunc;
    Py_tracefunc c_tracefunc;
    PyObject *c_profileobj;
    PyObject *c_traceobj;

    PyObject *exc_type;
    PyObject *exc_value;
    PyObject *exc_traceback;

    PyObject *dict;  /* Stores per-thread state */

    long thread_id;
};

PyFrameObject — это объект кадра стека. Питоновский объект, в отличие от первых двух структур (на это указывает PyObject_VAR_HEAD). Имеет указатель на предыдущий кадр f_back, исполняемый код f_code и последнюю выполненную в этом коде инструкцию f_lasti, указатель на свой поток f_tstate и серию из глобального, локального и встроенного пространства имен (f_globals, f_locals и f_builtins соответственно).

struct PyFrameObject {
    PyObject_VAR_HEAD
    PyFrameObject *f_back;  /* previous frame, or NULL */
    PyCodeObject *f_code;   /* code segment */
    PyObject *f_builtins;   /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;    /* global symbol table (PyDictObject) */
    PyObject *f_locals;     /* local symbol table (any mapping) */

    PyThreadState *f_tstate;
    int f_lasti;        /* Last instruction if called */
};

На самом деле членов в этих структурах поболее, и сами структуры отличаются от версии к версии (особенно заметны отличия между 2.x и 3.x) — но сейчас это не важно.

Важно понимать, что все три необходимых для исполнения структуры взаимно связаны между собой и PyThreadState_GET() возвращает указатель на текущий работающий поток:

график связывания структур

GIL

Теперь пришло время показать, как именно работает GIL. Тут есть одна тонкость: в 3.2 его реализация довольно значительно изменилась. Для начала рассмотрим «старый» GIL, используемый в Python 2.x и 3.0/3.1.

GIL переключается каждые 100 инструкций. Под инструкцией здесь понимается одна операция питоновского байткода. Возьмем простую функцию:

def f(lst, val):
    return [i for i in lst if i != val]

Применим к ней дизассемблер:

import dis
dis.dis(f)
  2           0 BUILD_LIST               0
              3 LOAD_FAST                0 (lst)
              6 GET_ITER
        >>    7 FOR_ITER                24 (to 34)
             10 STORE_FAST               2 (i)
             13 LOAD_FAST                2 (i)
             16 LOAD_FAST                1 (val)
             19 COMPARE_OP               3 (!=)
             22 POP_JUMP_IF_FALSE        7
             25 LOAD_FAST                2 (i)
             28 LIST_APPEND              2
             31 JUMP_ABSOLUTE            7
        >>   34 RETURN_VALUE

Как видите, код превратился в последовательность этих самых инструкций, исполняемых интерпретатором. Скажу сразу: никакой внятной документации по байт-коду нет, инструкции добавляются и изменяются от версии к версии. Интересующиеся должны читать файл Python/ceval.c как первоисточник для понимания того, что какая инструкция делает.

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

Сам GIL устроен как обычная не-рекурсивная блокировка. Эта же структура лежит в основе threading.Lock. Реализуется через событие CreateEvent с бубенцами на Windows и семафор sem_t на Linux.

Давайте посмотрим на кусочек исходного кода функции PyEval_EvalFrameEx, которая представляет собой цикл с очень объемным switch/case внутри, исполняющим по одной инструкции за проход.

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    PyThreadState *tstate = PyThreadState_GET();
    /* ... */
    for (;;) {
        /* ... */
        if (--_Py_Ticker < 0) {
            /* ... */
            _Py_Ticker = _Py_CheckInterval;
            if (interpreter_lock) {
                /* Give another thread a chance */
                if (PyThreadState_Swap(NULL) != tstate)
                    Py_FatalError("ceval: tstate mix-up");
                PyThread_release_lock(interpreter_lock);

                /* Other threads may run now */

                PyThread_acquire_lock(interpreter_lock, 1);
                if (PyThreadState_Swap(tstate) != NULL)
                    Py_FatalError("ceval: orphan tstate");
                /* ... */
            }
        }
    /* instruction processing */
    }
}

Как видите, все просто: имея захваченный GIL (а поток уже им владеет перед вызовом PyEval_EvalFrameEx), каждый раз уменьшаем счетчик пока не дойдем до нуля. interpreter_lock — это наш GIL, указатель на объект блокировки. Если он есть (а есть всегда, за исключением специальных сборок питона с полностью отключенной многопоточностью), то происходит так называемое «переключение GIL».

PyThreadState_Swap сбрасывает указатель на текущий исполняемый поток (тот самый, который возвращается PyThreadState_GET) и освобождает GIL.

Затем следующей строкой пытается захватить этот GIL снова. Хитрость в том, что если работает несколько потоков одновременно, то операционная система сама будет определять, какой поток из ожидающих в PyThread_acquire_lock получит эту блокировку (остальные будут ждать следующего освобождения interpreter_lock). Современные операционные системы используют довольно замысловатые алгоритмы переключения потоков. Нам же нужно знать лишь то, что эти алгоритмы пытаются распределить время «справедливо», дав каждому поработать. Это означает, что только что освободивший GIL поток скорее всего обратно сразу же его не получит — а отдаст управление другому потоку и сам встанет в ожидание PyThread_acquire_lock.

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

  • GIL переключается даже в однопоточной программе. Формально interpreter_lock создается не сразу при старте интерпретатора. Но импорт модуля threading или, к примеру, sqlite3 создаст GIL даже без создания второго потока. На практике правильней считать, что GIL есть всегда.
  • Другими словами GIL переключается постоянно, независимо от того требует ли другой поток переключения или они все заблокированы ожиданием ввода-вывода или объектами синхронизации.
  • Потоки «соревнуются» за захват GIL. Планировщик операционной системы — очень сложно устроенная штука. Поток, интенсивно использующий операции ввода-вывода, получает более высокий приоритет чем чисто вычислительный поток. Например, первый поток читает из файла и складывает прочитанное в очередь. Второй поток получает данные из очереди и обрабатывает их. Штука в том, что считывающий поток, обладая высоким приоритетом, может класть новые данные в очередь довольно долго, прежде чем обработчик получит возможность их обрабатывать. Да, первый поток регулярно освобождает GIL — но он тут же получает его назад (приоритет выше). Эта ситуация может быть исправлена выбором правильного способа взаимодействия между потоками, но решение зачастую неочевидно и, главное, проблема трудно локализуется.
  • И, наконец, главная причина. Переключение происходит по количеству выполненных инструкций. Дело в том, что время выполнения инструкций может сильно отличаться (сравните простое сложение и создание списка на миллион элементов).

Для управления порогом переключения существуют функции:

  • sys.getcheckinterval()
  • sys.setcheckinterval(count)

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

Новый GIL

Он использует усовершенствованную схему, базирующуюся на времени. Кроме того, добавлен специальный механизм для предотвращения повторного захвата GIL.

Снова выдержка из PyEval_EvalFrameEx, на этот раз Python 3.2.

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    PyThreadState *tstate = PyThreadState_GET();
    /* ... */
    for (;;) {
        /* ... */
        if (_Py_atomic_load_relaxed(&eval_breaker)) {
            /* ... */
            if (_Py_atomic_load_relaxed(&gil_drop_request)) {
                /* Give another thread a chance */
                if (PyThreadState_Swap(NULL) != tstate)
                    Py_FatalError("ceval: tstate mix-up");
                drop_gil(tstate);

                /* Other threads may run now */

                take_gil(tstate);
                if (PyThreadState_Swap(tstate) != NULL)
                    Py_FatalError("ceval: orphan tstate");
            }
        }
    /* instruction processing */
    }
}

Как видите, внешне почти ничего не изменилось. Ушел счетчик _Py_Ticker, Появились две переменные: eval_breaker и gil_drop_request. Переключение произойдет, если обе установлены (ненулевые). Две переменные нужны потому, что один и тот же механизм используется для штатного переключения GIL и для обработки сигналов операционной системы.

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

_Py_atomic_load_relaxed — это просто макрос для атомарного чтения переменной.

Вся магия скрыта внутри функций drop_gil и take_gil, работающих в паре.

static void drop_gil(PyThreadState *tstate)
{
    if (!_Py_atomic_load_relaxed(&gil_locked))
        Py_FatalError("drop_gil: GIL is not locked");

    MUTEX_LOCK(gil_mutex);
    _Py_ANNOTATE_RWLOCK_RELEASED(&gil_locked, /*is_write=*/1);
    _Py_atomic_store_relaxed(&gil_locked, 0);
    COND_SIGNAL(gil_cond);
    MUTEX_UNLOCK(gil_mutex);

    if (_Py_atomic_load_relaxed(&gil_drop_request)) {
        MUTEX_LOCK(switch_mutex);
        /* Not switched yet => wait */
        RESET_GIL_DROP_REQUEST();
        COND_WAIT(switch_cond, switch_mutex);
        MUTEX_UNLOCK(switch_mutex);
    }
}

Наш герой теперь называется gil_locked — обычная целочисленная переменная. Используется блокировка gil_mutex в паре с условной переменной gil_cond для синхронизации доступа к GIL. gil_drop_request — запрос на переключение GIL, защищенный switch_mutex и switch_cond.

В «отпускающей» стороне нет ничего сложного: прикрываясь gil_mutex сбрасываем GIL (gil_locked) в нолик и сигналим об этом событии через gil_cond.

Затем смотрим, это была просьба о переключении от другого потока или наш поток сам попросил освободиться. Дело в том, что Питон отпускает GIL перед системными вызовами. Чтение из файла, к примеру, может занимать длительное время и совершенно не требует GIL — можно дать шанс другим потокам поработать.

Если GIL освобождается не по внешнему запросу — работа закончена. Иначе нужно дождаться, пока попросивший не захватит GIL. Таким образом форсируется переключение на другой поток.

static void take_gil(PyThreadState *tstate)
{
    MUTEX_LOCK(gil_mutex);

    while (_Py_atomic_load_relaxed(&gil_locked)) {
        int timed_out = 0;
        unsigned long saved_switchnum;

        saved_switchnum = gil_switch_number;
        COND_TIMED_WAIT(gil_cond, gil_mutex, INTERVAL, timed_out);
        /* If we timed out and no switch occurred in the meantime,
           it is time to ask the GIL-holding thread to drop it. */
        if (timed_out &&
            _Py_atomic_load_relaxed(&gil_locked) &&
            gil_switch_number == saved_switchnum) {
            SET_GIL_DROP_REQUEST();
        }
    }
    /* This mutex must be taken before modifying gil_last_holder. */
    MUTEX_LOCK(switch_mutex);
    /* We now hold the GIL */
    _Py_atomic_store_relaxed(&gil_locked, 1);
    ++gil_switch_number;
    _Py_ANNOTATE_RWLOCK_ACQUIRED(&gil_locked, /*is_write=*/1);

    COND_SIGNAL(switch_cond);
    MUTEX_UNLOCK(switch_mutex);

    if (_Py_atomic_load_relaxed(&gil_drop_request)) {
        RESET_GIL_DROP_REQUEST();
    }

    MUTEX_UNLOCK(gil_mutex);
}

Захват GIL зеркально отражает его освобождение. Сначала ждем, пока GIL не освободится. Если ждем долго (больше 5 мс по умолчанию) и при этом не произошло переключения (не важно, на нас или какой другой поток) — выставляем запрос на переключение.

Дождавшись наконец свободного GIL, захватываем его и сигналим отдавшему потоку что передача состоялась. Естественно, все обращения защищены блокировками.

Что получилось в итоге:

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

Управление временем переключения — через sys.getswitchinterval и sys.setswitchinterval. Обратите внимание: в python 3.2 остались sys.getcheckinterval и sys.setcheckinterval, но они ни на что не влияют.

GIL и системные вызовы

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

GIL — один на всю программу. Слишком расточительно позволять потоку ждать окончания системного вызова (или любой другой операции, занимающей время и не требующей обращения к питоньим структурам), когда другие потоки простаивают в ожидании своей очереди на исполнение.

Поэтому перед вызовом такого долгоиграющего кода нужно отпустить GIL, а потом сразу же его захватить обратно:

Py_BEGIN_ALLOW_THREADS
errno = 0;
self->fd = open(name, flags, 0666);
Py_END_ALLOW_THREADS

Макросы Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS делают всю необходимую работу.

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

void f()
{
    Py_BEGIN_ALLOW_THREADS
    /* do something */
    Py_END_ALLOW_THREADS
}

void g()
{
    Py_BEGIN_ALLOW_THREADS
    f();
    Py_END_ALLOW_THREADS
}

Если очень хочется, то внутри функции можно писать Py_BLOCK_THREADS/Py_UNBLOCK_THREADS для временного получения GIL назад. Например, так:

PyObject* g()
{
    int ret;
    Py_BEGIN_ALLOW_THREADS
    ret = f();
    if (ret) {
        Py_BLOCK_THREADS
        PyErr_SetFromErrno(PyExc_IOerror);
        Py_UNBLOCK_THREADS
        return NULL;
    }
    Py_END_ALLOW_THREADS
}

Во вложенной функции испрользовать Py_BLOCK_THREADS не получится — эти макросы используют стандартные питоновские вызовы PyEval_SaveThread/PyEval_RestoreThread с сохранением структуры PyThreadState в локальной переменной _save.

Коротко говоря, следите за руками и одновременно изучайте исходники Питона. Они занятные — регулярно перечитываю перед сном.

GIL и потоки

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

Давайте посмотрим, как именно создается новый поток.

Вспомогательная структура:

struct bootstate {
    PyInterpreterState *interp;
    PyObject *func;
    PyObject *args;
    PyObject *keyw;
    PyThreadState *tstate;
};

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

Код, запускающий поток:

static PyObject *
thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
{
    PyObject *func, *args, *keyw = NULL;
    struct bootstate *boot;
    long ident;

    boot = PyMem_NEW(struct bootstate, 1);
    if (boot == NULL)
        return PyErr_NoMemory();
    boot->interp = PyThreadState_GET()->interp;
    boot->func = func; boot->args = args; boot->keyw = keyw;
    boot->tstate = _PyThreadState_Prealloc(boot->interp);
    if (boot->tstate == NULL) {
        PyMem_DEL(boot);
        return PyErr_NoMemory();
    }
    Py_INCREF(func); Py_INCREF(args); Py_XINCREF(keyw);
    PyEval_InitThreads(); /* Start the interpreter's thread-awareness */
    ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
    if (ident == -1) {
        PyErr_SetString(ThreadError, "can't start new thread");
        Py_DECREF(func); Py_DECREF(args); Py_XDECREF(keyw);
        PyThreadState_Clear(boot->tstate);
        PyMem_DEL(boot);
        return NULL;
    }
    return PyLong_FromLong(ident);
}

Ничего сложного: создаем bootstate и запускаем в новом потоке функцию t_bootstrap, которая должна закончить регистрацию PyThreadState. PyThread_start_new_thread — платформонезависимая обертка для создания потока ядра.

Сама запускаемая функция:

static void
t_bootstrap(void *boot_raw)
{
    struct bootstate *boot = (struct bootstate *) boot_raw;
    PyThreadState *tstate;
    PyObject *res;

    tstate = boot->tstate;
    tstate->thread_id = PyThread_get_thread_ident();
    _PyThreadState_Init(tstate);
    PyEval_AcquireThread(tstate);
    nb_threads++;
    res = PyEval_CallObjectWithKeywords(
        boot->func, boot->args, boot->keyw);
    if (res == NULL) {
        if (PyErr_ExceptionMatches(PyExc_SystemExit))
            PyErr_Clear();
        else {
            PyObject *file;
            PySys_WriteStderr(
                "Unhandled exception in thread started by ");
            file = PySys_GetObject("stderr");
            if (file != NULL && file != Py_None)
                PyFile_WriteObject(boot->func, file, 0);
            else
                PyObject_Print(boot->func, stderr, 0);
            PySys_WriteStderr("\n");
            PyErr_PrintEx(0);
        }
    }
    else
        Py_DECREF(res);
    Py_DECREF(boot->func); Py_DECREF(boot->args); Py_XDECREF(boot->keyw);
    PyMem_DEL(boot_raw);
    nb_threads--;
    PyThreadState_Clear(tstate);
    PyThreadState_DeleteCurrent();
    PyThread_exit_thread();
}

Код длинный, но простой. Из запущенного потока можно узнать его номер (идентификатор, используемый при обращениях к операционной системе). Осталось закончить инициализацию PyThreadState и выполнить запрашиваемую питоновскую функцию (PyEval_CallObjectWithKeywords). После окончания работы нужно почистить за собой. Если было исключение — записать его в stderr (исключения из потока не пробрасываются запустившему этот поток коду, остается только запись в поток ошибок).

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

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

Код можно было бы переписать, полностью перенеся создание PyThreadState в t_bootstrap и сократив его до:

PyThreadState *tstate = PyThreadState_New(boot->interp);
PyEval_AcquireThread(tstate);

Вообще-то в наборе функций для работы с потоками и GIL наблюдается некоторый разброд и шатание. Например, PyEval_AcquireThread захватывает GIL. PyEval_RestoreThread делает практически то же самое плюс специальную проверку на случай завершения интерпретатора (которая в правильно написанной программе не нужна, порожденные потоки должны получить сигнал о завершении раньше, чем произойдет завершение питоновского кода в главном потоке). То же самое можно сказать про пару PyEval_ReleaseThread и PyEval_SaveThread и так далее.

В оправдание сложившегося положения вещей можно сказать, что эта часть API развивалась долго, постепенно переходя из закрытой в публичную и документированную. Существующие сторонние модули чаще всего писались по принципу "работает—и ладно", использование закрытого API авторов не волновало (тем более что открытое API появлялось с некоторым опозданием). На данный момент имеем множество библиотек, которые могут поломаться, вздумай разработчики Питона разом отрубить все устаревшие части API. Поэтому процесс удаления устаревшего кода занимает как минимум 2-3 версии питона (что составляет около 4-5 лет) со строгими предупреждениями и разъяснениями. И тем не менее всегда остаются недовольные авторы, чей код «внезапно» перестал компилироваться.

Временное получение GIL

Бывает так, что нужно выполнить питоновский вызов, не зная — зарегистрирован ли поток или еще нет. Для этого существует пара PyGILState_Ensure и PyGILState_Release:

PyGILState_STATE opaque = PyGILState_Ensure();
/* do stuff */
PyGILState_Release(opaque);

Между вызовами ensure/release можно делать всё, что угодно — GIL захвачен, PyThreadState настроен. Можно, например, использовать Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS и вообще вызывать любой питоновский код. Более того, можно делать вложенные вызовы PyGILState_Ensure — главное не забывать о необходимых PyGILState_Release.

Нужно только всегда помнить об одной маленькой детали. Дело в том, что при каждом вызове PyGILState_Ensure система смотрит, был ли зарегистрирован PyThreadState для исполняемого потока. Если был — то захват происходит быстро. Иначе нужно создать и зарегистрировать новый PyThreadState. Для существующей структуры достаточно просто увеличить счетчик использования. PyGILState_Release этот счетчик уменьшает и, досчитав до нуля, удаляет зарегистрированный PyThreadState. На удаление тоже нужно время. Потери относительно небольшие, если только код не исполняется очень много раз. Иными словами, вместо создания/удаления PyThreadState в цикле:

void f() {
    int i;
    PyGILState_STATE state;
    for(i=0; i<100000; ++i) {
        /* do C block */
        state = PyGILState_Ensure();
        /* call python API */
        PyGLIState_Release(state);
    }
}

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

void f() {
    int i;
    PyGILState_STATE state, outer_state;
    outer_state = PyGILState_Ensure();
    Py_BEGIN_ALLOW_THREADS
    for(i=0; i<100000; ++i) {
        /* do C block */
        state = PyGILState_Ensure();
        /* call python API */
        PyGILState_Release(state);
    }
    Py_END_ALLOW_THREADS
    PyGILState_Release(outer_state);
}

Заключение

Я постарался как можно подробней описать, как работает GIL. Полностью покрыть эту тему невозможно даже в большой статье. Поэтому, если возникают вопросы — задавайте их или (что надежней и полезней) читайте ответ в исходном коде Питона, он простой и понятный.

Несколько слов «на общие темы».

  • GIL не уберут никогда. Или, по крайней мере, в ближайший десяток лет. Сейчас никаких работ на эту тему не ведется. Если некий гений предъявит работающую реализацию без GIL, ничего не ломающую и работающую не медленней, чем существующая версия — предложению будет открыт зеленый свет. Пока же «убрать GIL» проходит по части благих, но невыполнимых пожеланий.

  • В Java и C# никакого GIL нет. Потому что у них иначе устроен garbage collector. Если хотите, он более прогрессивный. Переделать GC Питона, не сломав обратной совместимости со всеми существующими библиотеками, использующими Python C API — невозможно. Сообщество и так уже который год лихорадит в связи с переходом на Python 3.x. Разработчики не желают выкатывать второе революционное изменение, не разобравшись с первым. Ждите Python 4.x (которого нет даже в планах) — до тех пор ничего не поменяется.

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

    • Во первых, если поток не делает вызовов Python C API — то GIL ему не нужен. Так можно держать много параллельно работающих потоков-числодробилок плюс несколько медленных питоновских потоков для управления всем хозяйством. Конечно, для этого нужно уметь писать Python C Extensions.
    • Второй способ еще лучше. Замените «поток» на «процесс». По настоящему высоконагруженная система в любом случае должна строится с учетом масштабируемости и высокой надежности. На эту тему можно говорить очень долго, но хорошая архитектура автоматически позволяет вам запускать несколько процессов на одной машине, которые общаются между собой через какую-либо систему сообщений. В качестве одного из приятных бонусов получается избавление от "проклятия GIL" — у каждого процесса он только один, но процессов много!

Надеюсь, прочитанное поможет вам понять, как GIL работает, какие у него есть неочевидные особенности. И, главное — запомнить, как он не работает никогда!

GIL и обработка сигналов

В дополнение к этой статье рекомендую прочесть про особенности обработки сигналов Питоном в posix средах.

Ссылки:

воскресенье, 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!
По моему - очень хороший совет (если, конечно, вы не провели предлагаемую операцию еще несколько лет назад).

понедельник, 28 февраля 2011 г.

Python 3.2

Как известно, недавно был релиз Python 3.2
Я не буду рассказывать о том, какие замечательные штуки в нем появились.

Занятно другое.
20 февраля 1991 был выпущен Python 0.9 - первый официальный релиз Питона. Кажется, поставлялся исключительно в виде исходников.
20 февраля 2011 вышел Python 3.2

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

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

Долгих лет моему любимому языку программирования!

воскресенье, 27 февраля 2011 г.

Питон: времена, даты и временные зоны

Сегодня поговорим о дате-времени.

При этом я не хочу отдельно останавливаться на модуле time - слишком он низкоуровневый.

Эта статья будет почти исключительно посвящена модулю datetime, предоставляющему довольно красивый и понятный интерфейс.

Давайте посмотрим на элементарный пример:

>>> from datetime import *
>>> dt = datetime.now()

Что может быть проще?

Правильно ли так писать? Ответ будет довольно неожиданным: когда как...

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

Назовем их относительным и абсолютным временем.

Относительное время

Этот тип времени никогда не пересекает границ программы, не сохраняется в базе данных и не передается по сети.

Используется для разных целей: измерения временных интервалов, общения с пользователем и так далее.

У относительного времени (naive в английской терминологии) нет информации о временной зоне (timezone). Наш простейший пример создавал именно такое время.

Большинство используемых в небрежно составленной программе времен - относительные. И это далеко не всегда корректно.

Абсолютное время

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

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

Временные зоны

Позвольте немного подробней рассказать о временных зонах.

Это наши привычные смещения относительно времени по Гринвичу. Например, в моём Киеве локальное время отличается на +2 часа (летом +3).

Объекты datetime и time могут принимать необязательный парамер по имени tzinfo.

Этот параметр должен быть или None или экземпляром класса, унаследованного от базового абстрактного класса tzinfo.

Я сейчас не хочу подробно останавливаться на том, как правильно унаследоваться от tzinfo и что нужно переопределить. Достаточно знать, что объекты временных зон существуют.

Теория работы с абсолютным временем

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

Дело в том, что у разных машин могут быть разные временные зоны, а Киевское время, например, отличается от Московского на один час. В результате одно и то же, казалось бы, время будет означать в Москве не совсем то, что оно означает в Киеве. В Москве может быть уже "завтра", когда у меня еще "сегодня".

Чтобы окончательно всё запутать, существует летнее время. Дата перехода на него отличается от страны к стране. Если в Украине летнее время начинается в последнее воскресенье марта, то в Бразилии, насколько я помню, оно заканчивается в последнее воскресенье февраля (при условии что эта дата не совпадает с их праздником Карнавала).

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

Если же во всех внешних коммуникациях используется UTC (которое не имеет летнего времени, между прочим) - всё получается однозначно.

К сожалению, об абсолютных временах и "правиле UTC" очень часто забывают при разработке.

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

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

Особенности реализации относительного и абсолютного времени в Питоне

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

По умолчанию объект datetime.datetime (всё сказанное применимо и к datetime.time) создается как относительное время, то есть без временной зоны.

Более того, существует два метода получить текущее время: datetime.now() и datetime.utcnow(). Полученные значения, конечно, различаются на действующую сейчас временную разницу. При этом вы не можете программно понять, где время в UTC а где - локальное.

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

Поэтому единственный надежный способ работы с базами данных - использовать относительное время, явно преобразуя его в UTC.

На базах данных странности не кончаются.

Дело в том, что только кажется: в Питоне есть лишь один datetime.datetime. Хотя технически все объекты класса datetime.datetime используют этот единственный тип, на самом деле концептуально абсолютные времена (с указанной зоной) никак не совместимы с относительными.

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

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

Она содержит абстрактный класс tzinfo - и ни одной его конкретной реализации.

Лишь в Python 3.2 появилась зона datetime.utc. Локальной зоны всё ещё нет.

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

При передаче этого относительного времени на другую машину получаем неопределенность, чреватую неожиданностями. Ну, дальше вы поняли...

Абсолютные времена, pytz и dateutil

Для более или менее комфортной жизни в этом сумасшедшем доме я рекомендую следующее.

Работайте с временами в базе данных только как с относительными (никуда не денешься), но храните их исключительно в UTC.

При обработке сразу же добавляйте временную зону UTC в явном виде. Для ORM процесс можно автоматизировать, унаследовавшись от существующего в вашей ORM описания колонки (поля) DateTime и добавив в преобразователи явное приведение временных зон. То же самое относится и, например, к библиотекам GUI.

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

Вообще-то говоря, к примеру, измерить время выполнения алгоритма можно и применяя относительное время.

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

Операции с абсолютными временами безопасны, даже если они относятся к разным временным зонам - Питон всё учитывает.

Остается вопрос, где эти временные зоны брать.

Существует прекрасная библиотека dateutil.

Она много чего умеет, неплохо расширяя стандартную datetime.

Что касается временных зон:

>>> from dateutil.tz import tzutc, tzlocal
>>> tzutc = tzutc()
>>> tzlocal = tzlocal()

Если вам требуется получить зону по имени - используйте библиотеку pytz.

>>> import pytz
>>> tzkiev = pytz.timezone('Europe/Kiev')
>>> print tzkiev
Europe/Kiev

Классы временных зон для dateutil и pytz немного отличаются списком доступных методов. Вы можете прочесть все детали в документации. Для этого изложения важно то, что они оба поддерживают интерфейс, требуемый для datetime.tzinfo.

Теперь минимальный набор операций.

Получение пресловутого текущего времени:

>>> now = datetime.now(tzlocal)
>>> print now
2011-02-25 11:52:59.887890+02:00

Преобразование в UTC:

>>> utc = now.astimezone(tzutc)
>>> print utc
2011-02-24 09:52:59.887890+00:00

Объект utc можно класть в базу данных или пересылать по сети - он преобразован с учетом смещения.

Отбросить временную зону (если, например, база данных не игнорирует эту информацию, а требует явного относительного времени):

>>> naive_utc = utc.replace(tzinfo=None)
>>> print naive_utc
2011-02-24 09:52:59.887890

Добавить временную зону к объекту, полученному из базы данных

>>> utc2 = naive_utc.replace(tzinfo=tzutc)
>>> print utc2
2011-02-24 09:52:59.887890+00:00

Преобразовать абсолютное UTC время в абсолютное локальное

>>> local_dt = utc.astimezone(tzlocal)
>>> print local_dt
2011-02-25 11:52:59.887890+02:00

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

Итоги

  • Старайтесь работать с абсолютными временами.

  • Для общения с внешним миром предпочитайте UTC.

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

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

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

    Гораздо дешевле делать "правильно" с самого начала.

Предупрежден - значит вооружен!

Удачного вам плаванья и семи футов под килем!

Ссылки

четверг, 24 февраля 2011 г.

Регрессионные тесты

Продолжаю предыдущую статью.

Всё та же несложная консольная программа, управляющая этим блогом.

И прежняя главная цель: сделать разработку стабильной.

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

Юниттесты - не универсальное решение

К сожалению, моя программка временами всё ешё глючит. Более того, когда я правлю код, относящийся к одной команде - может совершенно неожиданно сломаться другая, никаким образом (казалось бы!) с первой не связанная.

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

Здесь присутствуют несколько проблем:

  • Юниттесты должны исполнятся в виртуальном окружении.

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

    Мое приложение простое: у него нет базы данных (её заменяет yaml файл), системы распределенных сообщений и других занятных образцов инженерной мысли.

  • Юниттест всегда работает в одиночку.

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

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

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

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

    У юниттестов совершенно другая идеология. Если хотите, юниттесты скорее предназначены для тестирования API библиотек чем конечных приложений.

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

Остаётся довольно много функционала, под юниттесты не попадающего - и расширить тестовое покрытие до полного крайне неудобно по принципиальным причинам.

Маленький совет. Я стараюсь разносить покрывающийся тестами код и не тестирумый по разным модулям. Если для модуля есть хоть один тест - то покрытие для этого модуля должно стремиться к полному. Это сильно облегчает анализ и придает некоторую строгость подходу.

Ручное тестирование

Оно великолепно! Нет, кроме шуток!

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

  • Это - долго. Я печатаю довольно быстро - но не настолько же!
  • Это - утомительно. При повторении одних и тех же действий очень быстро устаю.
  • Самый минимальный прогон - два десятка команд. Более или менее полный - около сотни. И это - очень простая система!
  • Наконец, я в такие моменты ощущаю себя обезьяной. Определение "мартышкин труд" очень хорошо подходит к ручному тестированию.

В результате:

  • Ручное тестирование никогда не выполняется в полном объеме - задачка явно превышает человеческие возможности.
  • В силу предельной однообразности действий внимание притупляется, что тоже отрицательно сказывается на качестве.
  • Даже при скурпулёзном следовании бумажному тестовому плану (кто их пишет?) - процесс занимает непозволительно много времени.

Коллега Kogrom подсказывает:

Регрессионные тесты не ограничиваются только юнит-тестами.Есть ещё и регрессионные функциональные тесты, которые требуют больше ресурсов и времени, но зато более близки к ручному тестированию.

Постановка задачи: требуется регрессионный функциональный тест

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

В результате запуска регрессионных тестов ожидается простой ответ: основная функциональность всё ещё сохранилась, или продукт творческих усилий можно показывать лишь собственной кошке?

Решение

Технически задачка выглядит простой. Требуется запускать программу через subprocess.Popen, смотреть на код завершения и сравнивать перехваченные stdout и stderr с ожидаемыми.

Первый же вариант делал всё, что требуется.

Когда я написал код проверки пяти вызовов - устал. Copy-Paste очень сильно помогает в такой работе, но текста получается очень много.

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

Автоматизация - налицо, но назвать удобной эту систему можно, только сильно покривив душой.

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

Начало работы:

def run():
    folder = os.path.dirname(os.path.abspath(sys.argv[0]))
    regr_folder = os.path.join(folder, 'regr-data')
    blog_cmd = 'blog'

    exe = Executor(blog_cmd, regr_folder)
    exe.rmtree()
    exe.mkdir('.')

    project_dir = exe.full_name('sample_blog')

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

    CREATE_PROJECT = Q("INFO Create project {0!q}")
    ALREADY_IN_PROJECT = Q("ERROR Already a project: {0!q}")

    print 'init project'
    out = exe.go('init sample_blog')
    CREATE_PROJECT(project_dir) == out

    print 'init the same project'
    out = exe.go('init sample_blog', retcode=255)
    ALREADY_IN_PROJECT(project_dir) == out

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

Добавляем файл статьи и генерируем для него html:

    print "****** SIMPLE HTML *******"

    ADD = Q("INFO Add {name!q} -> {file!q}")

    HTML = Q("INFO Generate html for {name!q}")
    HTML_WARNING_NO_TEMPLATE = HTML + '\n' + Q("""
        WARNING User settings has no template specified.
        Use markdown output as html.
        """)

    TXT = 'Text of sample article'
    INNER_HTML = '<p>' + TXT + '</p>'

    print "add post"
    with exe.cd('sample_blog'):
        exe.write('article.rst', TXT)
        rst_fname = exe.full_name('article.rst')
        out = exe.go('add article.rst')
    ADD(name='article', file='article.rst') == out

    print "generate html without template"
    with exe.cd('sample_blog'):
        out = exe.go('html article.rst')
        Q(INNER_HTML) == exe.read('article.html')
    HTML_WARNING_NO_TEMPLATE(name='article', file=rst_fname) == out

Для простых случаев всё отлично. Но одним сравнением дело обходится не всегда. Нужны регулярные выражения.

Получаем разнообразную информацию о добавленном файле:

    POST_SHOW = Q("""\
        INFO Post {changed}{name!q}
            title: {title}
            link: {link}
            slug: {slug}
            labels: {labels}
            postid: {postid}
            localstamp: (?P<localstamp>.+?)$
    """)

    print 'show post article'
    with exe.cd('sample_blog'):
        out = exe.go('post article')
    ret = POST_SHOW(name='article',
              title='',
              changed='[*]', # screen * regexp spec symbol
              link='',
              slug='article',
              labels='a',
              postid='').match(out)
    localstamp = ret['localstamp']

Вместо проверки на == выполняем поиск по регулярному выражению .match. Найденные регуляркой группы можно запомнить для дальнейшего использования, проверить на соответствие и так далее.

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

  • Executor (exe) - работает как пользовательская консоль. Создаёт папки и файлы, запускает команды.

  • Q, что значит query - сравнивает свой шаблон с аргументом (__eq__), умеет делать поиск по регулярному выражению (.match), работать с результатом успешного поиска по регулярке (она же regexp).

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

    Именно потому я использую нечто вроде CREATE_PROJECT(project_dir) == out для проверок.

Executor

Тривиальная обертка вокруг нескольких функций os и subprocess.

 class Executor(object):
     def __init__(self, blog_cmd, regr_folder):
         self.blog_cmd = blog_cmd
         self.regr_folder = os.path.abspath(regr_folder)
         self.cwd = [regr_folder]
         self.out = None
         self.retcode = None

     @contextlib.contextmanager
     def cd(self, arg):
         self.cwd.append(os.path.abspath(os.path.join(self.cwd[-1], arg)))
         yield
         self.cwd.pop()

     def get_cwd(self):
         return self.cwd[-1]

     def mkdir(self, arg):
         os.makedirs(os.path.join(self.cwd[-1], arg))

     def rmtree(self):
         folder = self.cwd[-1]
         if os.path.exists(folder):
             shutil.rmtree(folder)

     def write(self, fname, text):
         with open(self.full_name(fname), 'wb') as f:
             f.write(text)

     def read(self, fname):
         with open(self.full_name(fname), 'rb') as f:
             return f.read()

     def full_name(self, fname):
         return os.path.join(self.cwd[-1], fname)

     def go(self, args, retcode=0):
         """run blog with args, return stdout merged with stderr"""
         args_list = shlex.split(args)
         proc = subprocess.Popen([self.blog_cmd] + args_list,
                                 cwd=self.get_cwd(),
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.STDOUT)
         self.out, err = proc.communicate()
         self.retcode = proc.returncode
         if retcode is not None:
             if retcode != self.retcode:
                 raise RuntimeError("RETCODE %s, EXPECTED %s\n%s" %
                                    (self.retcode,
                                     retcode,
                                     self.out)
                                    )
         return self.out

Откровенно говоря, всё просто как пять копеек.

cd, mkdir, read, write говорят сами за себя.

go запускает тестируемую программу с нужными аргументами, проверяет ее код завершения и возвращает объединенный вывод стандартных потоков.

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

Query

Проверяет шаблон на соответствие строке. Проверки бывают двух типов: на равенство и на регулярное выражение. Если проверка не проходит - выбрасывается исключение.

Проверки на регулярные выражения в случае успеха возвращают объект типа Match, дающий простой доступ к найденным группам.

Для большего удобства Q наследуется от Template (смотрите "Форматирование строк") и использует NamespaceFormatter, возвращающий None для каждого неизвестного параметра.

 class Q(Template):
     """Query, used for result check"""
     NS = NamespaceFormatter(collections.defaultdict(lambda: None))

     def format(__self, *__args, **__kwargs):
         return Q(__self.NS.vformat(__self, __args, __kwargs))

     def eq(self, other, strip=True):
         if strip:
             other = other.strip()
         if str(self) != str(other):
             raise RuntimeError('Not Equals\n%s\n----------- != ------------\n%s'
                                % (self, other))

     def __eq__(self, other):
         return self.eq(other)

     def __ne__(self, other):
         return not self.eq(other)

     def match(self, test, strip=True):
         if strip:
             test = test.strip()
         match = re.match(self, test, re.M)
         if match is None:
             raise RuntimeError("%s\n doesn't match pattern\n%s" % (test, self))
         return Match(match)

     def ifind(self, test, strip=True):
         if strip:
             test = test.strip()
         return (Match(m) for m in re.finditer(self, test, re.M))

 class Match(object):
     def __init__(self, match):
         self.match = match

     def check(self, **kwargs):
         groups = self.match.groupdict()
         for name, val in kwargs:
             group = groups(name)
             Q(val) == group

     def __getitem__(self, key):
         return self.match.groupdict()[key]

Примеры наглядно демонстрируют применение класса Q, что-то дополнительно объяснять вроде бы и не нужно.

Результат работы

Вот и всё.

У меня есть очень длинный сценарий регрессионного теста:

  • Изделие работает, и делает своё дело очень хорошо.
  • Добавлять новые тесты легко и приятно.
  • Что не менее важно - сценарий легко читать.
  • Запуск регрессионного теста позволяет с большой степенью уверенности говорить о том, что работа программы не нарушена.
  • На создание инфраструктуры я потратил от силы час. Конечно, написание самих тестов (и, главное, устранение выявившихся в процессе ошибок в программе) заняло куда больше времени - но оно того стоило.
  • Теперь я запускаю эти тесты перед каждым commit, вместе с юниттестами, ожидаю завершения - и таки делаю фиксацию с чистым сердцем или, что чаще бывает, продолжаю править баги.

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

Заключение

Возникают несколько вопросов.

Почему бы не отказаться от юнит-тестирования и делать только регрессионные тесты?

Ответ сложный.

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

  • У юниттестов есть своя ниша, в которой они хороши. Помните, что я писал о библиотеках в начале этой статьи?

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

    И это может быть очень медленно.

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

Если бы регрессионные тесты выполнялись мгновенно - большая часть вопросов бы отпала. К сожалению, это не так.

Я рассмотрел пример тестов для консольного приложения. Что делать в других случаях?

Да то же самое!

Для Веба есть Mechanize, twill, Selenium. Можно виртуально ходить по ссылкам, не запуская настоящего браузера. Предоставьте эту честь роботу!

Для GUI тоже есть способ. Проще всего воспользоваться средсвами используемой вами GUI библиотеки. Запустите приложение в том же процессе и в idle цикле эмулируйте действия пользователя. Ввводите текст и посылайте сообщения о том, что нужная кнопка нажата. Заодно библиотека GUI даст возможность узнать содержимое текстовых полей и так далее. Это удобно!

И, самое главное - регрессионные тесты выполняются автоматически. Достаточно запустить скрипт и дождаться его окончания. Всю тяжелую "обезьянью" работу выполняет скурпулёзный и внимательный компьютер, который не подвержен лени.

вторник, 22 февраля 2011 г.

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

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

  • Вы используете эту операцию по много раз на дню.
  • Вы даже предпочитаете новый стиль форматирования, использующий .format вместо %.
  • Вы знаете о форматировании всё!

Я тоже так считал, пока один случай не заставил пересмотреть своё мнение.

Думаю, эти несколько рецептов по форматированию будут интересны и полезны.

Пристегнулись? От винта! Поехали!

Конструктор

Приключения начинались довольно невинно. У меня появилось немало многострочных переменных. Для их удобного задания очень хорошо подходит функция textwrap.dedent. Она убирает общие ведущие пробелы из каждой строки текста:

def test():
    # end first line with \ to avoid the empty line!
    s = '''\
    hello
      world
    '''
    print repr(s)          # prints '    hello\n      world\n    '
    print repr(dedent(s))  # prints 'hello\n  world\n'

Проблем две. Я часто забываю ставить обратный слеш в начале. И последний возврат каретки обычно тоже не нужен.

А ещё очень хочется, чтобы строки всегда были юникодом независимо от того, указал я это явно через u'some string' или нет. И временами требуется сдвинуть весь текст на несколько пробелов вправо.

class Template(unicode):
    def __new__(cls, pattern, **kwargs):
        strip = kwargs.get('strip', True)
        dedent = kwargs.get('dedent', strip)
        indent = kwargs.get('indent', 0)
        if dedent:
            pattern = _dedent(pattern)
        if strip:
            pattern = pattern.strip()
        if indent:
            prefix = ' ' * indent
            pattern = '\n'.join(prefix + line for line in pattern.splitlines())
        return super(Template, cls).__new__(cls, pattern)

    def __add__(self, other):
        return self.__class__(unicode(self) + other, strip=False)

    __iadd__ = __add__

Класс Template наследуется от unicode - значит все Template строки тоже становятся юникодными.

Необязательные настройки передаются как keyword only arguments:

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

Заодно переопределим конкатенацию, чтобы возвращала объекты нашего класса. Зачем? Пригодится! Разумное поведение при конкатенации - не обрезать получившийся результат.

Аппетит приходит во время еды

В новой нотации задания шаблонов используется понятие явного конвертера. Например, "{0!r:>10}" указывает, что для форматируемого значения нужно сначала взять repr, а затем уже к результату применять выравнивание.

Необязательный явный конвертер задается одной буквой. По умолчанию их два: r для repr и s для str. Остается еще так много незадействованных букв!

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

Или другие, форматирующие принятым в программе способом время и дату. И так далее.

Поведение стандартного форматирования можно расширить.

class Formatter(string.Formatter):
    def convert_field(self, value, conversion):
        if 'q' == conversion:
            return qname(value)
        else:
            return super(Formatter, self).convert_field(value, conversion)

_formatter = Formatter()

Создаём свой собственный форматировщик, умеющий обрабатывать явное преобразование q -> qname в дополнение к стандартным.

Изменить стандартный системный форматировщик нельзя. Зато можно переопределить метод .format для нашей строки:

class Template(unicode):
    ...

    def format(__self, *__args, **__kwargs):
        # use __self etc to don't clash with __kwargs keys
        return _formatter.vformat(__self, __args, __kwargs)

    def __format__(__self, *__args, **__kwargs):
        return __self.format(*__args, **__kwargs)

    def __call__(__self, *__args, **__kwargs):
        return __self.format(*__args, **__kwargs)

Присутствует небольшая хитрость: имена параметров начинаются с __. Это сделано для того, чтобы можно было вызывать .format(self='self value') без конфликта имён.

Заодно и переопределим __call__, чтобы вместо Template('{0!q}').format('a b') можно было писать просто Template('{0!q}')('a b'). Результатом обоих вызовов будет '"a b"'.

Часто строчки с шаблонами такие длинные, а запись .format мешает уместится в заветные 79 символов.

__format__ нужен для того, чтобы заработало выражение вроде: format(Template('{0!q}'), 'a b').

И тут Остапа понесло

Для уточнения мелких деталей я просматривал PEP 3101, описывающий спецификацию на новомодный тип форматирования. И увидел там замечательную идею: форматирование с использованием пространств имён.

Идея очень простая. При определении значения именованного параметра сначала ищем в явно заданных аргументах, а если не нашли - последовательно перебираем все переданные в конструктор форматировщика пространства имен (простые словари).

class NamespaceFormatter(Formatter):
    def __init__(self, *namespaces):
        self.namespaces = namespaces

    def get_value(self, key, args, kwargs):
        if isinstance(key, basestring):
            try:
                return kwargs[key]
            except KeyError:
                for namespace in self.namespaces:
                    try:
                        return namespace[key]
                    except KeyError:
                        pass
        return super(NamespaceFormatter, self).get_value(key, args, kwargs)

Разжевывать не буду, а приведу отрезок из теста:

    class A(object):
        def __init__(self, val):
            self.val = val

    ns1 = {'b': A(2)}
    ns2 = {'c': A(3)}
    fmt = NamespaceFormatter(ns1, ns2)
    ret = fmt.format("{a.val} {b.val} {c.val}", a=A(1))
    assert '1 2 3' == ret

Как видите, b.val берется из ns1 а c.val - из ns2.

Хорошо, а какая польза от этих пространств имен?

Очень простая: в их качестве можно задавать стандартные globals и locals.

global_name = 'global'

def test_ns():
    local_name = 'local'
    fmt = NamespaceFormatter(locals(), globals())
    ret = fmt.format("{local_name} {global_name}")
    self.assertEqual('local global', ret)

Обратите внимание: в отличие от привычного порядка locals идёт первым. Потому что имена в нём должны перекрывать имена в globals.

Немного сахара

Почти хорошо. Только приходится довольно много писать. Автоматизируем поиск пространств имён.

def auto_format(spec, **spec_kwargs):
    template = Template(spec, **spec_kwargs)
    frame = sys._getframe(1)
    fmt = NamespaceFormatter(frame.f_locals, frame.f_globals)
    return fmt.format(template)

Берём фрейм вызвавшей функции, извлекаем оттуда заветные globals и locals.

Пример:

def test_auto_format():
    local_name = 'local'
    self.assertEqual('local global',
                     auto_format("{local_name} {global_name}"))

И, наконец, позволим использовать короткие имена:

T = Template
NF = NamespaceFormatter
a = auto_format

Мы строили, строили, и наконец - построили!

Полный код (за исключением импортов и функции qname):

class Formatter(string.Formatter):
    def convert_field(self, value, conversion):
        if 'Q' == conversion:
            if value is None:
                return 'None'
            else:
                return qname(value)

        if 'q' == conversion:
            return qname(value)

        return super(Formatter, self).convert_field(value, conversion)

_formatter = Formatter()

class Template(unicode):
    def __new__(cls, pattern, **kwargs):
        strip = kwargs.get('strip', True)
        dedent = kwargs.get('dedent', strip)
        indent = kwargs.get('indent', 0)
        if dedent:
            pattern = _dedent(pattern)
        if strip:
            pattern = pattern.strip()
        if indent:
            prefix = ' ' * indent
            pattern = '\n'.join(prefix + line for line in pattern.splitlines())
        return super(Template, cls).__new__(cls, pattern)

    def format(__self, *__args, **__kwargs):
        # use __self etc to don't clash with __kwargs keys
        return _formatter.vformat(__self, __args, __kwargs)

    def __format__(__self, *__args, **__kwargs):
        return __self.format(*__args, **__kwargs)

    def __call__(__self, *__args, **__kwargs):
        return __self.format(*__args, **__kwargs)

    def __add__(self, other):
        return self.__class__(unicode(self) + other, strip=False)

    __iadd__ = __add__

class NamespaceFormatter(Formatter):
    def __init__(self, *namespaces):
        self.namespaces = namespaces

    def get_value(self, key, args, kwargs):
        if isinstance(key, basestring):
            try:
                return kwargs[key]
            except KeyError:
                for namespace in self.namespaces:
                    try:
                        return namespace[key]
                    except KeyError:
                        pass
        return super(NamespaceFormatter, self).get_value(key, args, kwargs)

def auto_format(spec, **spec_kwargs):
    template = Template(spec, **spec_kwargs)
    frame = sys._getframe(1)
    fmt = NamespaceFormatter(frame.f_locals, frame.f_globals)
    return fmt.format(template)

T = Template
NF = NamespaceFormatter
a = auto_format

Пример использования:

def info_list(self, long, indent=4):
    changed = '*' if self.changed else ''
    ret = a("{changed}{self.name!q}")
    if long:
        ret += '\n' + a("""
            title: {self.title}
            link: {self.link}
            slug: {self.slug}
            labels: {self.labels_str}
            postid: {self.postid}
            localstamp: {self.local_stamp}
            """, indent=indent)
    return ret

Ещё один пример:

USER_INFO = T("""\
    INFO User info:
        email: {email!Q}
        default blogid: {blogid!Q}
        Template:
            dir: {dir!Q}
            file: {file!Q}
    """)
ret = USER_INFO(email=email, blogid=blogid, dir=dir, file=file)

Итоги

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

Жить стало легче, жить стало веселей!

Всё вышесказанное тестировалось на Python 2.6, на "тройке" работает с одним изменением: следует везде заменить unicode и basestring на str.