Полиглоты в продакшн Питона

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

    И самое интересное: подходы, используемые при написании этой программы, будут интересны не столько академически, сколько практически — при разработке программы с использованием этих подходов разработка будет проще и удобнее (хоть поначалу и чуточку непривычно), а программа будет эффективнее, чем без них.

    Впрочем, это звучит страшнее, чем является.

    Поехали?

    __debug__


    Начнём с достаточно простого — с кодогенерации. Программисты на Python знают, что в Python нет никакого препроцессора (как, например, в C), который бы позволял создавать макросы, в зависимости от каких-то параметров генерирующие различный код на этапе compile-time. «Динамичность» Python-а, конечно, позволяет генерировать что угодно и когда угодно на runtime, но… иногда возможность влиять на compile time тоже полезна, и даже необходима.

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

    При этом они наверняка помнят, что эта директива при запуске интерпретатора Python «с оптимизацией» (т.е., ключом -O) игнорируется и интерпретатором не выполняется.

    Сравним, например,
    #!/usr/bin/python
    
    def func(x):
        assert x > 5
        return x
    
    import dis; dis.dis(func)
    
    
      4           0 LOAD_FAST                0 (x)
                  3 LOAD_CONST               1 (5)
                  6 COMPARE_OP               4 (>)
                  9 JUMP_IF_TRUE             7 (to 19)
                 12 POP_TOP
                 13 LOAD_GLOBAL              0 (AssertionError)
                 16 RAISE_VARARGS            1
            >>   19 POP_TOP
    
      5          20 LOAD_FAST                0 (x)
                 23 RETURN_VALUE
    

    и
    #!/usr/bin/python -O
    
    def func(x):
        assert x > 5
        return x
    
    import dis; dis.dis(func)
    
      5           0 LOAD_FAST                0 (x)
                  3 RETURN_VALUE
    
    .

    Что из этого следует? Что, варьируя значение оптимизации при старте генерации байт-кода, можно получить совершенно разный байт-код. Код, который по-разному ведёт себя в зависимости от того, был он запущен в режиме оптимизации или без. Что это, как не зачаток препроцессора?
    Зачем нам это может быть полезно? Для того, чтобы существенно разделять в поведении debug- и release-build-ы. Всё как «в больших языках» — куча проверок, логов и информативности в debug-build-е, быстрый и эффективный код в release-build-е. Но не мало ли для этого одной директивы assert?

    Итак, assert ведёт себя по-разному в зависимости от настройки оптимизации, которая была в момент генерации байт-кода (при этом я надеюсь, что все помнят/знают, что байт-код вовсе не обязательно перегенерировать каждый раз? Более того, его, т.е. файлы .pyc/.pyo, можно распространять без исходников, из которых они были сгенерированы).
    Из этого вполне логично следует, что генератору байт-кода доступна информация, была ли включена оптимизация на момент его запуска. А как, кстати, он умеет оптимизировать код?

    Так вот, эта самая информация доступна ему в виде константы __debug__. Обратите внимание — действительно константы. Не так что часто в абсолютно динамическом языке Python-а можно найти константы :) Но раз уж это константа, то оптимизатор таки умеет генерировать код по-разному, в зависимости от её значения:

    #!/usr/bin/python
    
    def func():
        if __debug__:
            print
    
    import dis; dis.dis(func)
    
      5           0 PRINT_NEWLINE
                  1 LOAD_CONST               0 (None)
                  4 RETURN_VALUE
    


    #!/usr/bin/python -O
    
    def func():
        if __debug__:
            print
    
    import dis; dis.dis(func)
    
      4           0 LOAD_CONST               0 (None)
                  3 RETURN_VALUE
    


    И это, и, как частный случай, оптимизация директивы assert (ну, плюс ещё удаление docstring-ов при -OO) и составляет фактически весь спектр умений оптимизатора.

    Этот оптимизатор настолько прост, что, хоть он и умеет оптимизировать генерацию кода для операторов or и and над переменными (так, что вторая часть проверки не будет выполняться в случае ненадобности), но оптимизировать генерацию кода, когда первой проверкой является константа (так, что вторая часть проверки не будет генерироваться в случае ненадобности), он уже не умеет:

    #!/usr/bin/python -O
    
    def func(x):
        if __debug__ and x > 5:
            print
    
    import dis
    dis.dis(func)
    
      4           0 LOAD_GLOBAL              0 (__debug__)
                  3 JUMP_IF_FALSE           18 (to 24)
                  6 POP_TOP
                  7 LOAD_FAST                0 (x)
                 10 LOAD_CONST               1 (5)
                 13 COMPARE_OP               4 (>)
                 16 JUMP_IF_FALSE            5 (to 24)
                 19 POP_TOP
    
      5          20 PRINT_NEWLINE
                 21 JUMP_FORWARD             1 (to 25)
            >>   24 POP_TOP
            >>   25 LOAD_CONST               0 (None)
                 28 RETURN_VALUE
    


    Обратите внимание: хоть оптимизация и была включена, хоть сгенерированный код и способен пропустить проверку на x > 5, если после проверки он выяснит, что константа __debug__ установлена в True, но саму проверку он будет делать. Поэтому, если необходимо избавиться даже от генерации какого-то кода в release-build-е, придётся оптимизировать код «по старинке» — делать вложенные проверки, сначала на значение __debug__, и только внутри неё на x > 5:

    #!/usr/bin/python -O
    
    def func(x):
        if __debug__:
            if x > 5:
                print
    
    import dis
    dis.dis(func)
    
      4           0 LOAD_CONST               0 (None)
                  3 RETURN_VALUE
    


    Кстати, тернарный оператор тоже не оптимизируется на проверку константы:

    #!/usr/bin/python -O
    
    def func(x):
        return 5 if __debug__ else 7
    
    import dis
    dis.dis(func)
    
      4           0 LOAD_GLOBAL              0 (__debug__)
                  3 JUMP_IF_FALSE            7 (to 13)
                  6 POP_TOP
                  7 LOAD_CONST               1 (5)
                 10 JUMP_FORWARD             4 (to 17)
            >>   13 POP_TOP
                 14 LOAD_CONST               2 (7)
            >>   17 RETURN_VALUE
    
    


    Вспомним, для чего мы это делаем: для того, чтобы иметь возможность в debug-build-ах иметь сколь угодно детальную отладочную информацию (например, выводящуюся в сервисные логи), которая бы при этом опускалась и не влияла на производительность release-build-ов.

    Зато теперь, когда мы точно знаем, что и как можно делать с переменной __debug__ (никаких составных логических операций, никаких тернарных операций — только чистая проверка одной переменной __debug__ на валидность), мы можем сделать, например, следующее:

    if __debug__:
        def log(msg):
            __do_some_heavy_logging(msg)
    else:
        def log(msg):
            pass
    


    Это уже что-то. Теперь мы можем понавставлять log(что-нибудь) хоть в каждую вторую строку кода — на производительность release-build-а это нисколько не повлияет.

    Ну, честно говоря, почти нисколько. Пусть теперь на release-build-е мы и не пишем каждую строчку лога посредством жутко медленного вызова процедуры __do_some_heavy_logging() печати строчки на древнем рулонном АЦПУ — но вызов каждой функции log(), пусть и пустой, в release-build-е сохранится. И генерация сообщения для него (если она тоже вычислительно нетривиальна). Но тут уже ничего не поделаешь. В Python препроцессора нет, и совсем опускать вызов log() на debug-build-ах у нас не получится. Впрочем…

    GPP: GNU Python Preprocessor


    … Впрочем, если препроцессора нет в самом Python-е, почему бы не взять его извне? Чего только не сделаешь в стремлении оптимизировать release-build.

    Слава богам, на свете существует GPP — Generic Preprocessor, General-Purpose Processor, а теперь ещё и новый backronym — GNU Python Preprocessor. Почему бы и нет?

    Особо описывать его и перечитывать man вслух неохота. Скажу только, что синтаксис его фактически повторяет синтаксис препроцессора в C: #define, #if и т.п.

    Что очень даже удобно — в Python со знака диеза начинаются комментарии. Осознаёте, что это значит?..

    Правильно. Сейчас мы будем писать программу-полиглот.

    Да. Это вполне себе полноценный полиглот. Программа на двух языках — языке Python (не обладающем препроцессором), и на языке GPP.

    При этом это программы не уровня print "Hello world", одинаково ведущей себя на 4242 различных языках и диалектах программирования. Отнюдь, наши программы будут иметь вовсе даже различные задачи. Программа на Python (1) должна будет решить ту задачу, ради которой мы её пишем. Программа на GPP (2) должна сгенерировать программы на Python (3)/(4), решающие задачу, ради которой мы её пишем.

    Причём, обратите внимания на два забавных факта. Во-первых, программа на GPP будет генерировать не одну, а как минимум (в нашем случае) две различных программы на Python. Одну (3) — соответствующую debug-build-у, другую (4) — соответствующую release-build-у. А во-вторых, (1) определённо вовсе не равно (4) (ради того, чтобы релиз-билд отличался от промежуточного разработческого в сторону повышения производительности за счёт выкидывания отладочного кода, мы всё и затеяли), и даже не обязан быть равным (3)! Но лучше, конечно, чтобы хотя бы (1) был равен (3). Иначе запутаемся. Слишком много уровней, на которых одновременно надо мыслить. Типичная проблема при написании программы-полиглота.

    Ну что, попробуем?

    Пишем исходник на GPP:

    #ifdef debug
    #define log(msg) __do_some_heavy_logging(msg)
    #else
    #define log(msg)
    #endif
    


    В общем, неплохо. Если мы вызывали GPP с параметром -Ddebug, то он в каждое место исходника, где встречался вызов функции log(), подставит вместо него вызов функции __do_some_heavy_logging(msg). По сравнению с кодом, основанным на __debug__, уже прямая выгода — меньше на один промежуточный вызов функции. Если же мы вызывали GPP без такого параметра, то каждый вызов функции log() он заменит на пустую строку…

    Ура! То, что нам надо… или нет?

    А если мы выполним этот код сразу в интерпретаторе Python-а без препроцессинга? Тогда при любом вызове функции log() интерпретатор просто не найдёт её — у нас же она определена в препроцессоре.

    А значит, надо её определить и на уровне Python-а. Причём так, чтобы не помешать препроцессору. Второй блин:

    def log(msg):
        __do_some_heavy_logging(msg)
    #ifdef debug
    #define log(msg) __do_some_heavy_logging(msg)
    #else
    #define log(msg)
    #endif
    


    Уже лучше. Без препроцессинга — создаём функцию log() и используем её каждый раз при вызове log() в коде. С препроцессингом, без переменной препроцессора debug — вызовы log() заменяем на пустую строку. С препроцессингом, с переменной препроцессора debug: вызовы log() заменяем на вызовы __do_some_heavy_logging().

    Хорошо? Уже лучше. Но есть один нюанс: у нас (1) получился не равным (3). Что, конечно, хоть и красиво, но потенциально опасно в процессе разработки. Лучше всё-таки сделать так, чтобы препроцессированный в режиме debug код был абсолютно эквивалентен коду без препроцессинга. И поэтому весь его поместить внутри проверки #ifdef debug.

    #ifdef debug
    def log(msg):
        _do_some_heavy_logging(msg)
    #else
    #define log(msg)
    #endif
    


    Вот так намного лучше. В секции #ifdef debug — только непрепроцессорный код, и поэтому программа будет себя одинаково вести и с препроцессором и переменной -Ddebug, и без препроцессора. В секции else только препроцессорный код (и поэтому он сработает только в release-build-е). Так-то. И даже Python-овой переменной __debug__ больше не надо.

    Cython


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

    Почему бы нам не попробовать написать программу, которая после прохода GPP в режиме release (без переменной -Ddebug) генерирует код на вообще другом языке программирования? Ну, или на почти другом?

    А зачем?

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

    Единственное что — программы на Cython обратно не совместимы с Python-ом. А вот программы на Python обычно являются корректными программами на Cython. С одной стороны, это упрощает задачу до почти что тривиальности. С другой — мы не пионеры, чтобы создавать себе трудности и героически решать их, мы просто хотим писать на Python эффективно.

    Итак, цель: написать модуль на Python (1'), имеющий функцию, производящую какие-то вычисления. Этот модуль должен спокойно импортироваться и использоваться самим Python-ом без какого бы то ни было препроцессинга — чтобы удобнее разрабатывать было. Кроме того, этот модуль должен являться программой на GPP (2'). Которая, в случае соответственного указания переменных GPP, должна сгенерировать программу на Cython (3'). Содержащую алгоритм, полностью аналогичный программе (1'), но при этом типизированный. Такой, что после трансляции исходника (3') в C (4') и компиляции создастся бинарная библиотека Python-а, выполняющая всё то же самое, что и в (1'), только, за счёт компилированного кода, намного быстрее.

    В качестве решаемой задачи возьмём, например, вторую задачу Project Euler: найти сумму всех чётных чисел Фибоначчи, не превышающих четырёх миллионов. Представим, что нам её надо решать как часть гораздо большей проблемы, и задачу мы по-прежнему хотим решать на Python-е. Просто именно эту функцию мы хотим оптимизировать по производительности, переписав её на Cython-е. При этом хотим сохранить возможность запускать этот же код и без обработки Cython-ом (и GPP), как обычный Python-овый код, из соображений удобства разработки.

    Желаемый процесс выглядит разработки следующим образом: мы делаем модуль Python-а, из которого мы в процессе разработки и отладки и будем импортировать вычисляющую решение нашей задачи функцию. Этот же код при сборке release-build-а будет интерпретирован посредством GPP, который сгенерирует при этом эквивалентную программу на Cython (с необходимой для данной задачи типизацией данных); далее в процессе сборки release-build-а программа на Cython будет транслирована Cython-ом в программу на C, которая будет скомпилирована в библиотеку, доступную для использования Python-овому коду. И в release-build-е вызывающий Python-овый код будет импортировать функцию уже не из Python-ового модуля, а из бинарной библиотеки, скомпилированную и очень быструю.

    Как же должна выглядеть функция, решающая вторую задачу? Спойлер, конечно… но это вторая задача, и любой разумный программист, дочитавший до этой строчки и не ускользнувший в царство Морфея, наверняка спокойно решит её сам. Поэтому не будет большой потерей для учебного процесса, если я напишу эту функцию:

    def sum_even_fibo_le(upb):
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


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

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

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

    Но это не предел. Если мы решим типизировать наши данные (при этом, к сожалению, потеряв приятную неограниченность Python-ового типа данных long), то наш код будет работать ещё быстрее.

    Что мы будем (можем) типизировать в Cython? Входные аргументы функций. Выходные результаты функций. Промежуточные переменные.

    Вот и типизируем всё. Посчитаем, что 64 bit should be enough for everybody, и все переменные приведём к unsigned long long. Заодно и познакомимся с Cython-ом, кто его не знает:

    cdef unsigned long long sum_even_fibo_le(unsigned long long upb):
        cdef unsigned long long f1, f2, res
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
        


    Код хороший, достаточно читабельный для питонистов, и вполне понятный. Он имеет только два недостатка:
    1. Он уже не является валидным кодом на Python. Впрочем, мы это решим. Препроцессором. Но…
    2. Он не работает.


    Ну не то что бы совсем не работает. Просто не решает нашу задачу. Этим кодом прекрасно создаётся, в терминологии Cython, «C function». Которую можно вызывать из другого кода на Cython или даже на C, но которую нельзя вызывать из кода на Python. Что нам не подходит.
    Придётся модифицировать эту функцию так, чтобы она осталась «Python function». С чуточку медленным вызовом (впрочем, вызов из кода на Cython можно сохранить быстрым), возвращающую PyObject (если кого-то волнуют внутренности Python-ового интерпретатора).

    cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
        cdef unsigned long long f1, f2, res
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Итак, мы имеем функцию на Python, которая решает нашу задачу пусть и медленно, зато модуль с ней удобнее отлаживать — запускать на интерпретацию без промежуточной обработки. И функцию на Cython, которую обычный интерпретатор Python уже не поймёт, но которая зато наиболее производительна:

    def sum_even_fibo_le(upb):
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    
    cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
        cdef unsigned long long f1, f2, res
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Воспользуемся нашим любимым GPP (или что, вы его ещё не полюбили?), вспомним те знания, которые мы приобрели в процессе создания первой программы-полиглота Python/GPP, и напишем для начала программу на GPP:

    #ifdef debug
    def sum_even_fibo_le(upb):
    #else
    cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
        cdef unsigned long long f1, f2, res
    #endif
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Неплохо. Но если мы попытаемся прочитать её как программу на Python, считая строки с диезами за комментарии, мы увидим, что это вовсе не полиглот. А невалидная программа на Python — сразу после строчки с def идёт строчка с cpdef. Будем думать, как же замаскировать от интерпретатора Python код на Cython.

    Предыдущий опыт подсказывает нам, что блок #ifdef debug должен содержать только код на Python, а блок #else — только препроцессорный код. Или комментарии. Но как спрятать от интерпретации Python-ом блок #else? Если мы закомментируем его — посредством GPP мы не сможем его раскомментировать. Если мы создадим какой-нибудь макрос GPP, который комментирует этот блок в debug-build и декомментирует в release-build — это будет невалидным кодом на Python, без прохода GPP.

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

    def sum_even_fibo_le(upb):
    cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
        cdef unsigned long long f1, f2, res
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    … Они находятся прямо под сигнатурой функции, в том месте, где должны находиться… docstring-и!

    Ага. Идея ясна. Попробуем поправить этот код так, чтобы Cython-овые строчки выглядели docstring-ами (или просто свободными строками) при интерпретации Python-ом. А с интерпретацией GPP — нет.

    #ifdef debug
    def sum_even_fibo_le(upb):
        """
    #else
    cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
        cdef unsigned long long f1, f2, res
    #endif
        """
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Это ещё далеко не рабочий код, это только начало. Но идея ясна: с точки зрения Python (и, что обязательно — одновременно после препроцессинга GPP без -Ddebug), код на Cython станет docstring-ом.

    Но теперь у нас возникла проблема: проход препроцессором в режиме -Ddebug генерирует пустой docstring, а проход препроцессором в release-режиме генерирует только конечный триплет двойных кавычек от docstring-а. Тупик.

    Что-то надо сделать, чтобы избавиться от триплета кавычек в препроцессинге, но сохранить его без препроцессинга?
    Или вообще — чтобы в режиме препроцессинга триплеты кавычек исчезали, а без препроцессинга оставались? Нет, конечно, можно каждый триплет обрамлять своим собственным #if debug, или что-нибудь в этом духе, но это громоздко. Вот если бы можно было в режиме препроцессинга их прятать… комментировать!

    И точно. В GPP же есть возможность создавать свои комментации для препроцессора. Вот и создадим их, многострочные, как в C: /* ... */. Триплетами кавычек мы с точки зрения Python-а будем «комментировать» (точнее, всё-таки прятать) Cython-овый код, а C-style кавычками мы с точки зрения Cython будем комментировать триплеты кавычек. А C-style кавычки, чтобы они не проявились в Python-е, спрячем внутрь Python-овых комментариев.

    #!/usr/bin/python
    
    #mode comment "/*" "*/"
    
    #ifdef debug
    def sum_even_fibo_le(upb):
    #else /*
        """ */
    cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
        cdef unsigned long long f1, f2, res
    #endif /*
        """ # */
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Всё прекрасно. Этот код генерирует всё, что надо, во всех режимах. Он позволяет одну и ту же функциональность отлаживать в Python-режиме, и одновременно он готов к компиляции Cython-ом. Он великолепен.

    Down to Earth


    … Ладно, согласен, он не великолепен. Положа руку на сердце, вынужден признать — он отстой. Его совершенно нереально поддерживать. Его можно только копипастить. Эта путаница знаков препинания совершенно неинтуитивна. Если нам понадобится указывать эту конструкцию для каждой функции, то код станет выглядеть как Дрезден после бомбёжки. В реальном продукте такого быть не должно.

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

    Нам надо сделать две препроцессорные ветки. Ветка #ifdef debug должна без препроцессинга генерировать не выполняющийся при интерпретации Python-ом код. Ветка #else должна содержать как раз работающий код. Это всё должно выглядеть достаточно понятно и читабельно, и использовать это должно быть просто.

    Следовательно, вариант с комментированием всего и вся и перекрываниями области комментирования в разных языках отметается. Он в любом случае был негоден — а что если бы где-нибудь в Python-е встретилась бы последовательность /*?

    Хорошо. Какие у нас ещё варианты?

    Препроцессорные макросы. Да, именно они. Это всё упрощает. В точности как мы делали в самом первом эксперименте с GPP и логгированием.

    Заведём макрос cython(), которым мы будем обрамлять весь код, который не предназначен для обработки Python-интерпретатором. И поместим в него всё Cython-оотносящееся. При интерпретации GPP этот макрос будет восприниматься именно как макрос; без интерпретации он будет ничего не делающей Python-овой функцией. Что-то типа такого:

    #!/usr/bin/python
    
    def cython(text):
        pass
    #ifndef debug
    #define cython(x) x
    #endif
    
    cython("cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):")
    def sum_even_fibo_le(upb):
        cython("cdef unsigned long long f1, f2, res")
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Вот, это выглядит намного проще и понятнее. Хотя ещё не закончено. Если мы пропустим этот код через препроцессор, то, на удивление, мы увидим целых две проблемы:

    #!/usr/bin/python
    
    def cython(text):
        pass
    
    "cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):"
    def sum_even_fibo_le(upb):
        "cdef unsigned long long f1
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Во-первых, хоть Cython-овый код теперь прячется препроцессором достаточно неплохо, но Python-овый заголовок функции всё ещё светится. Делать нечего — придётся в каждой функции, для которой мы собираемся создавать Cython-овый аналог, писать ещё и кусок с явной проверкой на #ifdef debug.

    Во-вторых, GPP просто так не обладает концепцией «строк». Поэтому если мы хотим макросу cython передавать аргумент именно как единственный строковый компонент, который при раскрытии должен не печатать сами кавычки, мы должны указать соответствующую настройку в GPP. Последние правки…

    #!/usr/bin/python
    
    def cython(text):
        pass
    #ifndef debug
    #mode string QQQ "\"" "\""
    #define cython(x) x
    #endif
    
    cython("cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):")
    #ifdef debug
    def sum_even_fibo_le(upb):
    #endif
        cython("cdef unsigned long long f1, f2, res")
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        return res
    


    Вот теперь всё в порядке. Такой код не страшно и в продакшн. Главное — не забывать не использовать двойные кавычки внутри аргумента cython().

    Итог


    Что же, научились мы многому. Писать программы на Python и Cython одновременно. Убирать debug-код в релиз-билде. Немножко познакомились с особенностями оптимизатора Python-а. Теперь представим, как может выглядеть наша функция в режиме all included, в окончательном коде:

    #!/usr/bin/python
    
    #include debug.py
    from debug import cython, log
    
    cython("cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):")
    #ifdef debug
    def sum_even_fibo_le(upb):
    #endif
        cython("cdef unsigned long long f1, f2, res")
        log("Calculating sum_even_fibo_le")
        assert upb > 2
        f1, f2 = 1, 2
        res = 2
        while f2 < upb:
            f2, f1 = f1 + f2, f2
            if f2 % 2 == 0:
                res += f2
        log(res)    
        return res
    


    Вот с этим уже можно жить.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      0
      Написано, конечно, хорошо, спасибо, но кат, кат!
        +1
        А разве нет? Сорри, я вроде вставлял. Сейчас будет.
          +8
          Вот она! Прекрасная статья, а не 7 предложений и 15 строк банального листинга. Спасибо вам!
            +2
            Замечательно. Нет, потрясающе.
              +2
              За статью спасибо — приятно когда на Хабре все же встречается повод немного размять мозги :)

              Мне кажется, что достойным «финальным аккордом» было бы логично выбросить GPP и сделать свой препроцессор со специальным синтаксисом конкретно под нашу задачу (выделение в коде Python и Cython блоков).
                0
                Первоначально я как раз собирался сделать мини-препроцессор, чтобы Cython-код можно было указывать просто в комментариях, наподобие cpdef unsigned long long sum_even_fibo_le(unsigned long long upb. Но, раз GPP уже существует, почему бы не воспользоваться им?
                  0
                  Тьфу. Наподобие #cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
                0
                Тьфу. Наподобие #cpdef unsigned long long sum_even_fibo_le(unsigned long long upb):
                  0
                  Спасибо за статью!

                  1. Интересует: а стоит ли овчинка выделки?
                  Сейчас я использую Питон + критические модули на C
                  Проводили ли вы сравнение?

                  2. Интересует ваша среда сборки и развёртывания. (Вижу вы используете Джанго)
                  Как у вас это организовано?
                    0
                    1. Интересует: а стоит ли овчинка выделки?
                    Сейчас я использую Питон + критические модули на C
                    Проводили ли вы сравнение?


                    Стоит. Cython транслируется именно в C (или, при cython --cplus, в C++), который затем уже компилируется тем же самым gcc/g++, с любыми настройками оптимизаций. Скачок в производительности заметный даже при использовании Python-типов данных (раза в два); при использовании C-типов он и того больше. Будет минутка — я постараюсь упомянуть выигрыш в производительности у Cython-версии функции в примере по сравнению с Python-версией.
                      0
                      Проверил.
                      Суть бенчмарка — вызов функции sum_even_fibo_le() в цикле, 1048575 раз: print(sum(sum_even_fibo_le(n) for n in xrange(1, 0xFFFFF))).
                      Python-овый код при интерпретировании: 13,07s.
                      Python-овый код без модификаций, при компиляции Cython-ом: 4,48s
                      Cython-овый код (по сути, только Cython-овые заголовки вместо Python-овых): 0,47s.
                        0
                        Точнее, всё-таки 1048574 раза :)
                      0
                      2. Интересует ваша среда сборки и развёртывания. (Вижу вы используете Джанго)
                      Как у вас это организовано?


                      Django использую, но Cython в нём — пока нет. Стыдно признаться, но я олдскулен, и мне пока для всего хватает Makefile. Правда, в ключевом проекте размер Makefile-а уже приближается к 15k, так что в какое-то ближайшее время я буду исследовать distutils. Но не раньше, чем Makefile исчерпает себя.
                    • UFO just landed and posted this here
                        0
                        Производительность компилированного Cython-кода заметно выше, чем производительность интерпретации Python-ом.
                    • UFO just landed and posted this here
                        0
                        Статья интересная, спасибо.

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

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

                          А где сейчас используется Cython?
                            0
                            В смысле, в каких отраслях/областях? Можно использовать везде, где можно использовать Python (т.е., едва ли не везде вообще, кроме разве что программирования модулей ядра :) ) — от веба до игр, движков и числодробилок.

                            Или, в смысле, в известных широким кругам проектах? wiki.cython.org/projects — самое известное из списка, пожалуй, Sage.

                          Only users with full accounts can post comments. Log in, please.