В данной статье рассматриваются наиболее интересные преобразования, которые выполняет цепочка из двух транспайлеров (первый переводит код на языке Python в код на новом языке программирования 11l, а второй — код на 11l в C++), а также производится сравнение производительности с другими средствами ускорения/исполнения кода на Python (PyPy, Cython, Nuitka).
Замена "слайсов"\slices на диапазоны\ranges
Python | 11l |
|
|
s[(len)-2]
вместо просто s[-2]
нужно для исключения следующих ошибок:- Когда требуется к примеру получить предыдущий символ по
s[i-1]
, но при i = 0 такая/данная запись вместо ошибки молча вернёт последний символ строки [и я на практике сталкивался с такой ошибкой — коммит]. - Выражение
s[i:]
послеi = s.find(":")
будет работать неверно когда символ не найден в строке [вместо ‘‘часть строки начиная с первого символа:
и далее’’ будет взят последний символ строки] (и вообще, возвращать-1
функциейfind()
в Python-е я считаю также неправильно [следует возвращать null/None [а если требуется -1, то следует писать явно:i = s.find(":") ?? -1
]]). - Запись
s[-n:]
для полученияn
последних символов строки будет некорректно работать при n = 0.
Цепочки операторов сравнения
На первый взгляд выдающаяся черта языка Python, но на практике от неё легко можно отказаться/обойтись посредством оператора
in
и диапазонов:a < b < c |
b in a<..<c |
a <= b < c |
b in a..<c |
a < b <= c |
b in a<..c |
0 <= b <= 9 |
b in 0..9 |
Списковое включение (list comprehension)
Аналогично, как оказалось, можно отказаться и от другой интересной фичи Python — list comprehensions.
В то время как одни прославляют list comprehension и даже предлагают отказаться от `filter()` и `map()`, я обнаружил, что:
- Во всех местах, где мне встречалось Python's list comprehension, можно легко обойтись функциями `filter()` и `map()`.
dirs[:] = [d for d in dirs if d[0] != '.' and d != exclude_dir] dirs[:] = filter(lambda d: d[0] != '.' and d != exclude_dir, dirs) '[' + ', '.join(python_types_to_11l[ty] for ty in self.type_args) + ']' '[' + ', '.join(map(lambda ty: python_types_to_11l[ty], self.type_args)) + ']' # Nested list comprehension: matrix = [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], ] [[row[i] for row in matrix] for i in range(4)] list(map(lambda i: list(map(lambda row: row[i], matrix)), range(4)))
- `filter()` и `map()` в 11l выглядят красивее, чем в Pythonи следовательно необходимость в list comprehensions в 11l фактически отпадает [замена list comprehension на
dirs[:] = filter(lambda d: d[0] != '.' and d != exclude_dir, dirs) dirs = dirs.filter(d -> d[0] != ‘.’ & d != @exclude_dir) '[' + ', '.join(map(lambda ty: python_types_to_11l[ty], self.type_args)) + ']' ‘[’(.type_args.map(ty -> :python_types_to_11l[ty]).join(‘, ’))‘]’ outfile.write("\n".join(x[1] for x in fileslist if x[0])) outfile.write("\n".join(map(lambda x: x[1], filter(lambda x: x[0], fileslist)))) outfile.write(fileslist.filter(x -> x[0]).map(x -> x[1]).join("\n"))
filter()
и/илиmap()
выполняется в процессе преобразования Python-кода в 11l автоматически].
Преобразование цепочки if-elif-else в switch
В то время как Python не содержит оператора switch, это одна из самых красивых конструкций в языке 11l, и поэтому я решил вставлять switch автоматически:
Python | 11l |
|
|
switch (instr[i])
{
case u'[':
nesting_level++;
break;
case u']':
if (--nesting_level == 0)
goto break_;
break;
case u'‘':
ending_tags.append(u"’"_S);
break; // ‘‘
case u'’':
assert(ending_tags.pop() == u'’');
break;
}
Преобразование небольших словарей в нативный код
Рассмотрим такую строчку кода на Python:
tag = {'*':'b', '_':'u', '-':'s', '~':'i'}[prev_char()]
Скорее всего, такая форма записи не очень эффективна [с точки зрения производительности], зато очень удобна.В 11l же соответствующая данной строчке [и полученная транспайлером Python > 11l] запись не только удобная [впрочем, не настолько изящная как в Python], но и быстрая:
var tag = switch prev_char() {‘*’ {‘b’}; ‘_’ {‘u’}; ‘-’ {‘s’}; ‘~’ {‘i’}}
Приведённая строчка странслируется в:
auto tag = [&](const auto &a){return a == u'*' ? u'b'_C : a == u'_' ? u'u'_C : a == u'-' ? u's'_C : a == u'~' ? u'i'_C : throw KeyError(a);}(prev_char());
[Вызов лямбда-функции компилятор C++ встроит\inline в процессе оптимизации и останется только цепочка операторов ?/:
.]В том случае, когда производится присваивание переменной, словарь оставляется как есть:
Python |
|
11l |
|
C++ |
|
Захват\Capture внешних переменных
В Python для указания того, что переменная не является локальной, а должна быть взята снаружи [от текущей функции], используется ключевое слово nonlocal [в противном случае к примеру
found = True
будет трактоваться как создание новой локальной переменной found
, а не присваивание значения уже существующей внешней переменной].В 11l для этого используется префикс @:
Python | 11l |
|
|
auto writepos = 0;
auto write_to_pos = [..., &outfile, &writepos](const auto &pos, const auto &npos)
{
outfile.write(...);
writepos = npos;
};
Глобальные переменные
Аналогично внешним переменным, если забыть объявить глобальную переменную в Python [посредством ключевого слова global], то получится незаметный баг:
|
|
break_label_index
’.Индекс/номер текущего элемента контейнера
Я всё время забываю порядок переменных, которые возвращает Python-функция
enumerate
{сначала идёт значение, а потом индекс или наоборот}. Поведение аналога в Ruby — each.with_index
— гораздо легче запомнить: with index означает, что index идёт после value, а не перед. Но в 11l логика ещё проще для запоминания:Python | 11l |
|
|
Производительность
В качестве тестировочной используется программа преобразования пк-разметки в HTML, а в качестве исходных данных берётся исходник статьи по пк-разметке [так как эта статья на данный момент — самая большая из написанных на пк-разметке], и повторяется 10 раз, то есть получается из 48.8 килобайтной статьи файл размером 488Кб.
Вот диаграмма, показывающая во сколько раз соответствующий способ исполнения Python-кода быстрее оригинальной реализации [CPython]:
Время выполнения [время преобразования файла размером 488Кб] составило 868 мс для CPython и 38 мс для сгенерированного C++ кода [это время включает в себя полноценный [т.е. не просто работу с данными в оперативной памяти] запуск программы операционной системой и весь ввод/вывод [чтение исходного файла [.pq] и сохранение нового файла [.html] на диск]].
Я хотел ещё попробовать Shed Skin, но он не поддерживает локальные функции.
Numba использовать также не получилось (выдаёт ошибку ‘Use of unknown opcode LOAD_BUILD_CLASS’).
Вот архив с использовавшейся программой для сравнения производительности [под Windows] (требуются установленный Python 3.6 или выше и следующие Python-пакеты: pywin32, cython).
Исходник на Python и вывод транспайлеров Python > 11l и 11l > C++:
Python | Сгенерированный 11l (с ключевыми словами вместо букв) |
11l (с буквами) |
Сгенерированный C++ |
Комментарии (28)
Tyiler
28.11.2018 07:47Прочитал мельком.
Вопрос возник: если так все хорошо получилось преобразовать, то почему бы сразу не сделать C++ из Python, без прослойки 11l (думаю, что никому не нужной — еще один язык? — нее!)alextretyak Автор
28.11.2018 08:03Ну, начиналось всё с преобразователя 11l > C++. О компиляции/ускорении кода на Python я тогда вообще не думал.
И лишь спустя полгода была начата работа над Python > 11l.
Почему не над Python > C++?
Ну, во-первых, 11l гораздо ближе к Python, чем C++ к Python.
Во-вторых, не буду скрывать, проект Python > 11l задумывался с целью популяризации языка 11l.Tyiler
28.11.2018 08:16почитал прошлые ваши статьи про 11l.
там вам пытались вправить мозги. я тоже самое скажу — забейте на 11l.
Вот сделать переводчик (Транспайлер) быстрый — Python -> C++, а потом еще и вызов компиляции из коробки для него сразу, — было бы, наверно, полезно.gnomeby
28.11.2018 10:21Таких проектов уже много и так.
alextretyak Автор
28.11.2018 13:30А можете привести самые удачные?
Просто я толковых проектов не нашёл (помимо упомянутых в статье Nuitka и Shed Skin, я попробовал py14, Pythran и Py2C).gnomeby
28.11.2018 15:19Да, по сути эти проекты и так в вашей статье. Но самый удачный проект — Cython. Он конечно вводит статическую типизацию, но одновременно и самый удобный в применении и в него можно при желании скомпилить многое оптимизируя кусками, без сильных переделок, а это самое важное. Это гораздо важнее возможности транслировать любой код на питоне в Си код.
alextretyak Автор
28.11.2018 16:09Главная проблема Cython [и главное препятствие на пути его широкого распространения] с моей т.з. — аннотации типов в нём задаются по-своему, отлично от Python Type Hints (доступных начиная с Python 3.5).
А также, насколько я понял, нет достаточно удобного отладчика для Cython (кроме DDD ничего не нашёл). [Для 11l это не такая острая проблема, так как можно отлаживать Python-код перед отдачей его транспайлеру Python > 11l, а с Cython так не получится, так как код на нём написанный не совместим с Python.]gnomeby
28.11.2018 16:46Главное препятствие на пути широкого распространения любой альтернативки CPython — то что это никому не нужно. Кроме специализированных случаев: pypy, numpy, cython, когда люди готовы специально потратить время на адаптацию алгоритмов. Никто не любит ограничений любых либ и трансляторов, ибо питон любят в том числе за синтаксис, иначе проще взять Go/Swift/Rust и пилить более быстрое ПО.
Если ваш транслятор хорош и действительно транслирует обычный питон-код, тогда его элементы можно включить в сам CPython и получить ускорение. И вот от этого никто бы не отказался. Но то-то мне подсказывает, что вы также будете предоставлять ограниченный питон.
monester
28.11.2018 10:06А как у вас с безопасностью? Что будет есть обратиться за приделы списка элементов?
alextretyak Автор
28.11.2018 10:49Также как в Python: обращение за пределы массива бросает исключение IndexError.
gnomeby
28.11.2018 10:31+1Автор вы молодец, никого не слушайте и язык не бросайте. Самое главное — это самому понять почему этот язык никому не нужен, именно это и будет самым большим опытом в этом приключении. Ну и конечно навыки трансляции бесценны.
gnomeby
28.11.2018 15:34Ну и вдобавок:
* Напишите простенький веб-фреймворк на языке и на нём же запустите собственный сайт.
* Для коннекторов к базе используйте линковку Си драйвера любимой базы.
* Не забудьте про шаблонизатор
* Не забудьте про кеш
* И вам понадобится какой-нибудь FastCgi протокол
Вот когда вы это всё сделаете, может быть вы что-то ещё поймёте.
Zanak
28.11.2018 11:12А на nim вы смотрели (если размышлять о трансляции Python->промежуточный_язык->C++)? Мне, после питона, на нем показалось вполне комфортно, жаль, он пока не особо востребован в плане коммерческой разработки.
Не то, чтобы я был против еще одного языка. Пока появилась автомобильная промышленность, автомобили строили именно энтузиасты. Думаю здесь происходит нечто подобное.
Сам язык (я про 11l), его документацию, просмотрел по диагонали, и, если честно, не зацепило. Еще один язык с непривычным синтаксисом и смутными перспективами. Я подожду с его изучением.
Ktulhy
28.11.2018 14:28Поддерживаются ли классы?
alextretyak Автор
28.11.2018 14:52Да, только они называются типами (как в Go).
Коротенький пример есть в документации.
По умолчанию типы в 11l являются "типами-значениями" [а не "типами-ссылками"] и работают как структуры (struct) в C или C++.
В случае, когда тип Type ссылается сам на себя, Type заменяется на SharedPtr<Type> (см. первый пример отсюда).
В случае, когда тип Type содержит виртуальные функции, Type заменяется на std::unique_ptr<Type> (пример).
Pappageno
29.11.2018 06:30while (true) { switch (instr[i]) { case '[': nesting_level++; break; case ']': if (--nesting_level == 0) goto break_; break; } i++; ... } break_:
А вы специально используете эти скобочки, чтобы визуально увеличить кол-во C++-кода? В контексте питона(и 11l) вы не можете сказать «мне удобнее/привычней» выделять блоки «скобкой на новой строке», а не отступом.alextretyak Автор
29.11.2018 06:40Эмм… Не понял, если честно, ваш вопрос. Вы имеете в виду, почему не так:
while (true) { switch (instr[i]) { ...
Или как вы предлагаете не "увеличивать кол-во C++-кода"? Ведь фигурные скобки в этом примере необходимы и являются требованием языка C++.
В контексте питона(и 11l) вы не можете сказать «мне удобнее/привычней» выделять блоки «скобкой на новой строке», а не отступом.
Почему не могу? 11l позволяет выделять блоки как отступом так и/или фигурными скобками.Pappageno
30.11.2018 07:43Эмм… Не понял, если честно, ваш вопрос. Вы имеете в виду, почему не так:
Да.
11l позволяет выделять блоки
Это хорошо, но это не распространяется на питон, как и на привычки/видение пользователей питона, в том числе и вас(все ваши примеры без скобочек) и ЦА ваших трудов.
Вот я спрашиваю, чем обусловлен такой стиль? А мой же вопрос обусловлен древними холиварами на тему «в c++ есть скобочки, а у нас нет — наш код компактней». Вот я и думаю — это всё идёт с тех времен, либо что-то ещё.
Tanner
Это как-то подозрительно хорошо.
А имеет смысл попробовать ускорить достаточно сложный проект? Этот, например?
alextretyak Автор
На данном этапе, увы, нет.
В этом проекте слишком много чего импортируется и слишком много возможностей Python используется.
[[[А декораторы поддерживать вообще не планируется.]]]
Tanner
Жаль.
alextretyak Автор
С моей точки зрения они только запутывают [на основе опыта перевода этого кода с декоратором
@method
— код без декоратора получился более понятным].А "стандартные" декораторы (@property, @classmethod, @staticmethod), имхо, лучше поддерживать в синтаксисе языка программирования.
Впрочем, я поторопился с ответом. И если получится найти несложный способ реализации декораторов, то можно и добавить их поддержку.
gnomeby
Это с вашей точки зрения. А с точки опытного веб-разработчика, декораторы — отличный способ не сильно увеличивая код определить правила входа в конечные точки веб-приложения (CSRF проверки, разрешенные http методы, правила кеширования и пр.).
alextretyak Автор
Очень бы хотелось увидеть пример хорошего использования декораторов.
Вы не могли бы дать ссылочку на код [если он в open source, разумеется]?
Буду крайне признателен.
gnomeby
Вот прямо на главной:
flask.pocoo.org
Вот посложнее:
docs.python.org/3/library/contextlib.html
asm0dey
скажем так — я не понимаю как сделать flexget без декораторов…