вторник, 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 поражают своим объемом, да и другие не отстают.

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

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