Можете ли вы добавить новые операторы в синтаксис Python?
можете ли вы добавить новые операторы (например,print
, raise
, with
) к синтаксису Python?
скажем, разрешить..
mystatement "Something"
или
new_if True:
print "example"
не так много, если вы должны, но скорее, если это возможно (за исключением изменения кода интерпретаторов python)
13 ответов
вы можете найти это полезным - Python internals: добавление нового оператора в Python, цитируемый здесь:
эта статья является попыткой лучше понять, как работает интерфейс Python. Просто чтение документации и исходного кода может быть немного скучным, поэтому я беру практический подход здесь: я собираюсь добавить until
оператор для Python.
все кодирование для этой статьи было сделано против передовой ветви Py3k в the зеркало репозитория Python Mercurial.
на until
сообщении
некоторые языки, такие как Ruby, имеют until
заявление, которое является дополнением к while
(until num == 0
эквивалентно while num != 0
). В Ruby, я могу написать:
num = 3
until num == 0 do
puts num
num -= 1
end
и он будет печатать:
3
2
1
Итак, я хочу добавить аналогичную возможность для Python. То есть уметь писать:
num = 3
until num == 0:
print(num)
num -= 1
язык-адвокатура отступление
эта статья не пытается предложить добавление until
оператор для Python. Хотя я думаю, что такое утверждение сделало бы некоторый код более ясным, и эта статья показывает, насколько легко его добавить, я полностью уважаю философию минимализма Python. Все, что я пытаюсь сделать здесь, на самом деле, это получить некоторое представление о внутренней работе Python.
изменение грамматики
Python использует пользовательский генератор имени парсер pgen
. Это синтаксический анализатор LL(1), который преобразует исходный код Python в дерево синтаксического анализа. Входным сигналом генератора синтаксического анализатора является файл Grammar/Grammar
[1]. Это простой текстовый файл, который определяет грамматику Python.
[1]: с этого момента ссылки на файлы в источнике Python даются относительно корня дерева источника, который является каталогом, в котором вы запускаете configure и make для сборки Python.
две модификации должны быть сделаны в файле грамматики. Первый-добавить определение для until
заявление. Я нашел, где while
оператор был определен (while_stmt
), и добавил until_stmt
ниже [2]:
compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite
[2]: это демонстрирует общую технику, которую я использую при изменении исходного кода, с которым я не знаком:работа по подобию. Этот принцип не решит всех ваших проблем, но он определенно может облегчить процесс. С все, что нужно сделать для while
также должно быть сделано для until
, это служит довольно хорошим руководством.
обратите внимание, что я решил исключить else
пункт из моего определения until
, просто чтобы сделать его немного другим (и потому, что, честно говоря, мне не нравится else
предложение циклов и не думаю, что оно хорошо сочетается с Дзен Python).
второе изменение заключается в изменении правила для compound_stmt
включить until_stmt
, как вы можете видеть в фрагмент выше. Это сразу после while_stmt
снова.
при выполнении make
после изменения Grammar/Grammar
обратите внимание, что pgen
программа запускается для повторного создания Include/graminit.h
и Python/graminit.c
, а затем несколько файлов повторно компилируются.
изменение кода поколения AST
после того, как синтаксический анализатор Python создал дерево синтаксического анализа, это дерево преобразуется в AST, так как ASTs гораздо проще работать с на последующих этапах процесс компиляции.
Итак, мы собираемся посетить Parser/Python.asdl
который определяет структуру ASTs Python и добавляет узел AST для нашего нового until
заявление, снова прямо под while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
если вы сейчас запустите make
обратите внимание, что перед компиляцией кучу файлов, Parser/asdl_c.py
запускается для генерации кода C из файла определения AST. Это (как Grammar/Grammar
) - еще один пример исходного кода Python с использованием мини-языка (другими словами, DSL) для упрощение программирования. Также обратите внимание, что с Parser/asdl_c.py
это скрипт Python, это своего рода загрузки - чтобы построить Python с нуля, Python уже должен быть доступен.
пока Parser/asdl_c.py
сгенерировал код для управления нашим недавно определенным узлом AST (в файлы Include/Python-ast.h
и Python/Python-ast.c
), мы все еще должны написать код, который преобразует соответствующий узел дерева синтаксического анализа в него вручную. Это делается в файле Python/ast.c
. Есть функция с именем ast_for_stmt
преобразует разберите узлы дерева для операторов в узлы AST. Опять же, руководствуясь нашим старым другом while
, мы прыгаем прямо в большой switch
для обработки составных операторов и добавления предложения для until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
теперь мы должны реализовать ast_for_until_stmt
. Вот это:
static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
/* until_stmt: 'until' test ':' suite */
REQ(n, until_stmt);
if (NCH(n) == 4) {
expr_ty expression;
asdl_seq *suite_seq;
expression = ast_for_expr(c, CHILD(n, 1));
if (!expression)
return NULL;
suite_seq = ast_for_suite(c, CHILD(n, 3));
if (!suite_seq)
return NULL;
return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
}
PyErr_Format(PyExc_SystemError,
"wrong number of tokens for 'until' statement: %d",
NCH(n));
return NULL;
}
опять же, это было закодировано, внимательно глядя на эквивалент ast_for_while_stmt
, с той разницей, что для until
я решил не поддерживать else
предложения. Как и ожидалось, AST создается рекурсивно, используя другие функции создания AST, такие как ast_for_expr
для выражения условия и ast_for_suite
на теле until
заявление. Наконец, новый узел с именем Until
возвращается.
обратите внимание, что мы обращаемся к узлу дерева синтаксического анализа n
использование некоторых макросов, таких как NCH
и CHILD
. Это стоит понять - их код находится в Include/node.h
.
отступление: АСТ композиция
я решил создать новый тип AST для until
заявление, но на самом деле в этом нет необходимости. Я мог бы сохранить некоторую работу и реализовать новую функциональность, используя состав существующих узлов AST, так как:
until condition:
# do stuff
аналогично:
while not condition:
# do stuff
вместо создания Until
узел ast_for_until_stmt
, я мог бы создать Not
узел с While
узел, как ребенок. Поскольку компилятор AST уже знает, как обрабатывать эти узлы, следующие шаги процесса могут быть пропущенный.
компиляция ASTs в байт-код
следующим шагом является компиляция AST в байт-код Python. Компиляция имеет промежуточный результат, который является CFG (график потока управления), но поскольку тот же код обрабатывает его, я пока проигнорирую эту деталь и оставлю ее для другой статьи.
код, который мы рассмотрим далее-это Python/compile.c
. Следуя примеру while
, мы находим функцию compiler_visit_stmt
, который отвечает за составление отчетности в байткод. Мы добавляем предложение для Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
если вам интересно, что Until_kind
есть, это константа (на самом деле значение _stmt_kind
перечисление) автоматически генерируется из файла определения AST в Include/Python-ast.h
. Во всяком случае, мы называем compiler_until
которого, конечно, до сих пор не существует. Я займусь этим через минуту.
если вы любопытны, как я, Вы заметите, что compiler_visit_stmt
свойственно. Нет количества grep
-ping дерево источника показывает, где он называется. В этом случае остается только один вариант-c macro-fu. Действительно, короткое расследование приводит нас к VISIT
макрос определен в Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
он используется для вызова compiler_visit_stmt
на compiler_body
. Однако вернемся к нашему делу...
как и было обещано, вот compiler_until
:
static int
compiler_until(struct compiler *c, stmt_ty s)
{
basicblock *loop, *end, *anchor = NULL;
int constant = expr_constant(s->v.Until.test);
if (constant == 1) {
return 1;
}
loop = compiler_new_block(c);
end = compiler_new_block(c);
if (constant == -1) {
anchor = compiler_new_block(c);
if (anchor == NULL)
return 0;
}
if (loop == NULL || end == NULL)
return 0;
ADDOP_JREL(c, SETUP_LOOP, end);
compiler_use_next_block(c, loop);
if (!compiler_push_fblock(c, LOOP, loop))
return 0;
if (constant == -1) {
VISIT(c, expr, s->v.Until.test);
ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
}
VISIT_SEQ(c, stmt, s->v.Until.body);
ADDOP_JABS(c, JUMP_ABSOLUTE, loop);
if (constant == -1) {
compiler_use_next_block(c, anchor);
ADDOP(c, POP_BLOCK);
}
compiler_pop_fblock(c, LOOP, loop);
compiler_use_next_block(c, end);
return 1;
}
я должен признаться: этот код не был написан на основе глубокого понимания байт-кода Python. Как и остальная часть статьи, это было сделано в подражание из рода . Однако, внимательно прочитав его, имея в виду, что виртуальная машина Python основана на стеке, и заглянув в документацию dis
модуль, который имеет список байт-кодов Python с описаниями, можно понять, что происходит.
все, мы закончили... Не так ли?
после внесения всех изменений и работает make
, мы можем запустить недавно скомпилированный Python и попробовать наш новый until
заявление:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
вуаля, все работает! Давайте посмотрим байт-код, созданный для нового оператора с помощью dis
модуль следующим образом:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
вот результат:
4 0 SETUP_LOOP 36 (to 39)
>> 3 LOAD_FAST 0 (num)
6 LOAD_CONST 1 (0)
9 COMPARE_OP 2 (==)
12 POP_JUMP_IF_TRUE 38
5 15 LOAD_NAME 0 (print)
18 LOAD_FAST 0 (num)
21 CALL_FUNCTION 1
24 POP_TOP
6 25 LOAD_FAST 0 (num)
28 LOAD_CONST 2 (1)
31 INPLACE_SUBTRACT
32 STORE_FAST 0 (num)
35 JUMP_ABSOLUTE 3
>> 38 POP_BLOCK
>> 39 LOAD_CONST 0 (None)
42 RETURN_VALUE
самая интересная операция-номер 12: Если условие истинно, мы переходим к после цикла. Это правильная семантика для until
. Если прыжок не выполняется, тело цикла продолжает работать, пока оно не вернется к состоянию при работе 35.
чувствуя себя хорошо о моем изменении, я попытался запустить функцию (выполнение myfoo(3)
) вместо того, чтобы показывать свой байткод. Результат был не слишком обнадеживающим:--111-->
Traceback (most recent call last):
File "zy.py", line 9, in
myfoo(3)
File "zy.py", line 5, in myfoo
print(num)
SystemError: no locals when loading 'print'
Вау... это не может быть хорошо. Так что же пошло не так?
случай отсутствующей таблицы символов
одним из шагов компилятора Python при компиляции AST является создание таблицы символов для кода, который он компилирует. Вызов PySymtable_Build
на PyAST_Compile
вызовы в модуль таблицы символов (Python/symtable.c
), который выполняет AST аналогично функциям генерации кода. Наличие таблицы символов для каждой области помогает компилятору вычислить некоторую ключевую информацию, например, какие переменные являются глобальными, а какие локальными для области.
чтобы устранить проблему, мы должны изменить на Python/symtable.c
добавление кода для обработки until
заявления, после аналогичного кода для while
заявления [3]:
case While_kind:
VISIT(st, expr, s->v.While.test);
VISIT_SEQ(st, stmt, s->v.While.body);
if (s->v.While.orelse)
VISIT_SEQ(st, stmt, s->v.While.orelse);
break;
case Until_kind:
VISIT(st, expr, s->v.Until.test);
VISIT_SEQ(st, stmt, s->v.Until.body);
break;
[3]: кстати, без этого кода есть предупреждение компилятора для Python/symtable.c
. Компилятор замечает, что Until_kind
значение перечисления не обрабатывается в инструкции switch symtable_visit_stmt
и жалуется. Всегда важно проверять наличие предупреждений компилятора!
и теперь мы действительно сделали. Компиляция источника после этого изменения делает выполнение myfoo(3)
работать ожидаемый.
вывод
в этой статье я продемонстрировал, как добавить новый оператор в Python. Несмотря на то, что для этого потребовалось довольно много работы в коде компилятора Python, это изменение было несложно реализовать, потому что я использовал аналогичный и существующий оператор в качестве руководства.
компилятор Python-это сложный кусок программного обеспечения, и я не утверждаю, что являюсь экспертом в этом. Тем не менее, меня действительно интересуют внутренности Python, и особенно его передний конец. Поэтому я нашел это упражнение очень полезным компаньоном для теоретического изучения принципов компилятора и исходного кода. Это послужит основой для будущих статей, которые будут углубляться в компилятор.
ссылки
я использовал несколько отличных ссылок для построения этой статьи. Вот они, в определенном порядке:--111-->
- PEP 339: дизайн компилятора CPython - наверное, самый важный и всеобъемлющий кусок официальный документация для компилятора Python. Будучи очень коротким, он болезненно показывает нехватку хорошей документации внутренних частей Python.
- "компилятор Python Internals" - статья Томаса Ли
- "Python: проектирование и реализация" - презентация Гвидо ван Россума
- виртуальная машина Python (2.5), экскурсия-презентация Питера Tröger
один из способов сделать такие вещи - предварительно обработать источник и изменить его, переведя добавленный оператор на python. Есть различные проблемы, которые этот подход принесет, и я бы не рекомендовал его для общего использования, но для экспериментов с языком или метапрограммирования конкретного назначения он может быть полезен.
например, предположим, что мы хотим ввести оператор "myprint", который вместо печати на экране вместо входа в определенный файл. т. е.:
myprint "This gets logged to file"
было бы эквивалентно
print >>open('/tmp/logfile.txt','a'), "This gets logged to file"
существуют различные варианты того, как сделать замену, от замены регулярных выражений до создания AST, чтобы написать свой собственный парсер в зависимости от того, насколько близко ваш синтаксис соответствует существующему python. Хорошим промежуточным подходом является использование модуля tokenizer. Это должно позволить вам добавлять новые ключевые слова, структуры управления и т. д. при интерпретации источника аналогично интерпретатору python, что позволит избежать поломки сырые решения regex вызовут. Для вышеуказанного "myprint" вы можете написать следующий код преобразования:
import tokenize
LOGFILE = '/tmp/log.txt'
def translate(readline):
for type, name,_,_,_ in tokenize.generate_tokens(readline):
if type ==tokenize.NAME and name =='myprint':
yield tokenize.NAME, 'print'
yield tokenize.OP, '>>'
yield tokenize.NAME, "open"
yield tokenize.OP, "("
yield tokenize.STRING, repr(LOGFILE)
yield tokenize.OP, ","
yield tokenize.STRING, "'a'"
yield tokenize.OP, ")"
yield tokenize.OP, ","
else:
yield type,name
(это делает myprint эффективно ключевым словом, поэтому использование в качестве переменной в другом месте, вероятно, вызовет проблемы)
проблема в том, как использовать его, чтобы ваш код можно было использовать из python. Один из способов-написать собственную функцию импорта и использовать ее для загрузки кода, написанного на вашем языке. т. е.:
import new
def myimport(filename):
mod = new.module(filename)
f=open(filename)
data = tokenize.untokenize(translate(f.readline))
exec data in mod.__dict__
return mod
для этого требуется однако вы обрабатываете свой настроенный код иначе, чем обычные модули python. т. е. "some_mod = myimport("some_mod.py")
" вместо "import some_mod
"
еще одно довольно аккуратное (хотя и хакерское) решение-создать пользовательскую кодировку (см. PEP 263) как этой рецепт демонстрирует. Вы можете реализовать это как:
import codecs, cStringIO, encodings
from encodings import utf_8
class StreamReader(utf_8.StreamReader):
def __init__(self, *args, **kwargs):
codecs.StreamReader.__init__(self, *args, **kwargs)
data = tokenize.untokenize(translate(self.stream.readline))
self.stream = cStringIO.StringIO(data)
def search_function(s):
if s!='mylang': return None
utf8=encodings.search_function('utf8') # Assume utf8 encoding
return codecs.CodecInfo(
name='mylang',
encode = utf8.encode,
decode = utf8.decode,
incrementalencoder=utf8.incrementalencoder,
incrementaldecoder=utf8.incrementaldecoder,
streamreader=StreamReader,
streamwriter=utf8.streamwriter)
codecs.register(search_function)
теперь после запуска этого кода (например. вы можете поместить его в свой .pythonrc или site.py) любой код, начинающийся с комментария "# coding: mylang", будет автоматически переводится через вышеуказанный этап предварительной обработки. например.
# coding: mylang
myprint "this gets logged to file"
for i in range(10):
myprint "so does this : ", i, "times"
myprint ("works fine" "with arbitrary" + " syntax"
"and line continuations")
предостережения:
есть проблемы с подходом препроцессора, как вы, вероятно, будете знакомы с, если вы работали с препроцессором C. Основной из них-отладка. Все, что видит python, это предварительно обработанный файл, который означает, что текст, напечатанный в трассировке стека и т. д., будет ссылаться на это. Если вы выполнили значительный перевод, это может сильно отличаться от исходного текста. Этот пример выше не меняет номера строк и т. д., Поэтому не будет слишком отличаться, но чем больше вы его измените, тем сложнее будет выяснить.
да, в какой-то степени это возможно. Есть модуль там, что использует sys.settrace()
для реализации goto
и comefrom
"ключевые слова":
from goto import goto, label
for i in range(1, 10):
for j in range(1, 20):
print i, j
if j == 3:
goto .end # breaking out from nested loop
label .end
print "Finished"
не хватает изменения и перекомпиляции исходного кода (который is возможно с открытым исходным кодом), изменение базового языка на самом деле невозможно.
даже если вы перекомпилируете источник, это не будет python, просто ваша взломанная измененная версия, в которую вам нужно быть очень осторожным, чтобы не вводить ошибки.
однако я не уверен, почему вы этого хотите. Объектно-ориентированные функции Python позволяют довольно просто достичь аналогичных результатов с помощью язык как он есть.
общий ответ: вам необходимо предварительно обработать исходные файлы.
более конкретный ответ: установить EasyExtend, и пройти следующие шаги
i) создайте новый langlet (язык расширения )
import EasyExtend
EasyExtend.new_langlet("mystmts", prompt = "my> ", source_ext = "mypy")
без дополнительной спецификации куча файлов будет создана под EasyExtend/langlets/mystmts/ .
ii) откройте mystmts/parsedef/Grammar.ext и добавить следующие строки
small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | exec_stmt | assert_stmt | my_stmt )
my_stmt: 'mystatement' expr
этого достаточно, чтобы определение синтаксиса оператора new. Не-терминал small_stmt является частью грамматики Python, и это место, где подключается новый оператор. Парсер теперь распознает новый оператор, т. е. исходный файл, содержащий его, будет проанализирован. Компилятор отклонит его, потому что он все еще должен быть преобразован в действительный Python.
iii) теперь нужно добавить семантику утверждения. Для этого нужно отредактировать msytmts/langlet.py и добавьте узел my_stmt посетитель.
def call_my_stmt(expression):
"defines behaviour for my_stmt"
print "my stmt called with", expression
class LangletTransformer(Transformer):
@transform
def my_stmt(self, node):
_expr = find_node(node, symbol.expr)
return any_stmt(CST_CallFunc("call_my_stmt", [_expr]))
__publish__ = ["call_my_stmt"]
iv) cd в langlets / mystmts и введите
python run_mystmts.py
теперь сеанс должен быть запущен, и вновь определенный оператор может быть использован:
__________________________________________________________________________________
mystmts
On Python 2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bit (Intel)]
__________________________________________________________________________________
my> mystatement 40+2
my stmt called with 42
довольно много шагов, чтобы прийти к тривиальному утверждению, не так ли? Еще нет API, который позволяет определять простые вещи, не заботясь о грамматиках. Но EE очень надежен по модулю некоторых ошибок. Поэтому появление API, позволяющего программистам определять такие удобные вещи, как операторы infix или небольшие операторы, использующие только удобное Программирование OO. Для более сложных вещей, таких как встраивание целых языков в Python с помощью построения ланглета, нет способа обойти полный грамматический подход.
вот очень простой, но дерьмовый способ добавления новых операторов,только в режиме интерпретации. Я использую его для маленьких 1-буквенных команд для редактирования аннотаций генов, используя только sys.displayhook, но чтобы я мог ответить на этот вопрос, я добавил sys.excepthook для синтаксических ошибок, а также. Последнее действительно уродливо, извлекая необработанный код из буфера readline. Преимущество в том, что таким образом тривиально легко добавлять новые утверждения.
jcomeau@intrepid:~/$ cat demo.py; ./demo.py
#!/usr/bin/python -i
'load everything needed under "package", such as package.common.normalize()'
import os, sys, readline, traceback
if __name__ == '__main__':
class t:
@staticmethod
def localfunction(*args):
print 'this is a test'
if args:
print 'ignoring %s' % repr(args)
def displayhook(whatever):
if hasattr(whatever, 'localfunction'):
return whatever.localfunction()
else:
print whatever
def excepthook(exctype, value, tb):
if exctype is SyntaxError:
index = readline.get_current_history_length()
item = readline.get_history_item(index)
command = item.split()
print 'command:', command
if len(command[0]) == 1:
try:
eval(command[0]).localfunction(*command[1:])
except:
traceback.print_exception(exctype, value, tb)
else:
traceback.print_exception(exctype, value, tb)
sys.displayhook = displayhook
sys.excepthook = excepthook
>>> t
this is a test
>>> t t
command: ['t', 't']
this is a test
ignoring ('t',)
>>> ^D
Я нашел руководство по добавлению новых операторов, преобразованных из PDF в HTML с помощью Google:
в основном, чтобы добавить новые операторы, вы должны изменить Python/ast.c
(среди прочего) и перекомпилировать двоичный файл python.
пока это возможно, не надо. Вы можете достичь почти всего с помощью функций и классов (которые не требует, чтобы люди перекомпилировали python только для запуска вашего скрипта..)
Это можно сделать с помощью EasyExtend:
EasyExtend (EE) является препроцессором генератор и метапрограммирование фреймворк, написанный на чистом Python и интегрирован с с CPython. Сеть целью EasyExtend является создание языков расширения, т. е. добавление пользовательский синтаксис и семантика для Python.
Не без изменения интерпретатора. Я знаю, что многие языки за последние несколько лет были описаны как "расширяемые", но не так, как вы описываете. Вы расширяете Python, добавляя функции и классы.
существует язык, основанный на python под названием программное обеспечение С которым вы можете делать такие вещи. Он не разрабатывался некоторое время, но функции, которые вы просили работают С последней версией.
некоторые вещи можно сделать с оформителями. Предположим, например, что Python не имел with
заявление. Затем мы могли бы реализовать подобное поведение следующим образом:
# ====== Implementation of "mywith" decorator ======
def mywith(stream):
def decorator(function):
try: function(stream)
finally: stream.close()
return decorator
# ====== Using the decorator ======
@mywith(open("test.py","r"))
def _(infile):
for l in infile.readlines():
print(">>", l.rstrip())
это довольно нечистое решение, однако, как сделано здесь. Особенно поведение, когда декоратор вызывает функцию и устанавливает _
to None
неожиданное. Для уточнения: этот декоратор эквивалентен написанию
def _(infile): ...
_ = mywith(open(...))(_) # mywith returns None.
и декораторы, как правило, должны изменить, не выполнять функции.
я использовал такой метод Перед в скрипте, где мне пришлось временно установить рабочий каталог для нескольких функций.
Это не совсем добавление новых операторов в синтаксис языка, но макросы являются мощным инструментом:https://github.com/lihaoyi/macropy
десять лет назад вы не могли, и я сомневаюсь, что это изменилось. Однако тогда было не так сложно изменить синтаксис, если вы были готовы перекомпилировать python, и я сомневаюсь, что это изменилось.