Можете ли вы добавить новые операторы в синтаксис Python? - PullRequest
112 голосов
/ 18 октября 2008

Можете ли вы добавить новые операторы (например, print, raise, with) в синтаксис Python?

Скажи, чтобы позволить ..

mystatement "Something"

Или,

new_if True:
    print "example"

Не так много, если вы должны , а скорее, если это возможно (если не считать изменения кода интерпретаторов python)

Ответы [ 13 ]

134 голосов
/ 02 февраля 2012

Вы можете найти это полезным - Внутренние элементы Python: добавление нового оператора в Python , цитата здесь:


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

Все кодирование для этой статьи было выполнено с использованием новейшей ветки Py3k в зеркале 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 и не думаю, что хорошо сочетается с дзен питона).

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

Когда вы запускаете make после изменения Grammar/Grammar, обратите внимание, что программа pgen запускается для повторной генерации Include/graminit.h и Python/graminit.c, а затем несколько файлов перекомпилируются.

Изменение кода генерации AST

После того, как анализатор Python создал дерево разбора, это дерево преобразуется в AST, поскольку AST намного проще работать с на последующих этапах процесса компиляции.

Итак, мы собираемся посетить Parser/Python.asdl, который определяет структуру AST 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 уже знает, как обрабатывать эти узлы, следующие этапы процесса могут быть пропущены.

Компиляция AST в байт-код

Следующим шагом является компиляция AST в байт-код Python. Компиляция имеет промежуточный результат - CFG (Control Flow Graph), но, поскольку тот же код обрабатывает ее, я пока проигнорирую эту деталь и оставлю ее для другой статьи.

Код, который мы рассмотрим следующим: 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 -провождения исходного дерева не показывает, где оно вызывается. В этом случае остается только один вариант - 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. Как и в остальной части статьи, это было сделано в имитации функции kin compiler_while. Однако, внимательно прочитав его, помня, что виртуальная машина 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)) вместо показа ее байт-кода. Результат был менее чем обнадеживающим:

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 аналогично функциям генерации кода. Наличие таблицы символов для каждой области помогает компилятору выяснить некоторую ключевую информацию, например, какие переменные являются глобальными, а какие локальными для области.

Чтобы исправить проблему, мы должны изменить функцию symtable_visit_stmt в 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, и особенно его интерфейсом. Поэтому я нашел это упражнение очень полезным дополнением к теоретическому изучению принципов и исходного кода компилятора. Он послужит основой для будущих статей, которые углубятся в компилятор.

Ссылки

Я использовал несколько отличных ссылок для построения этой статьи. Вот они, в произвольном порядке:

  • PEP 339. Проект компилятора CPython - возможно, самая важная и полная часть официальной документации для компилятора Python. Будучи очень коротким, он мучительно показывает недостаток хорошей документации внутренних компонентов Python.
  • "Внутренние компоненты компилятора Python" - статья Томаса Ли
  • «Питон: проектирование и реализация» - презентация Гвидо ван Россума
  • Python (2.5) Виртуальная машина, экскурсия - презентация Питера Трегера

оригинальный источник

51 голосов
/ 19 октября 2008

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

Например, допустим, мы хотим ввести выражение «myprint», которое вместо печати на экране вместо записи в конкретный файл. а именно:

myprint "This gets logged to file"

будет эквивалентно

print >>open('/tmp/logfile.txt','a'), "This gets logged to file"

Существуют различные варианты того, как выполнить замену, от подстановки регулярных выражений до генерации AST, на написание собственного синтаксического анализатора в зависимости от того, насколько близко ваш синтаксис соответствует существующему python. Хорошим промежуточным подходом является использование модуля токенизатора. Это должно позволить вам добавлять новые ключевые слова, управляющие структуры и т. Д. При интерпретации источника аналогично интерпретатору python, что позволит избежать поломки грубых решений регулярных выражений. Для вышеприведенного «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")

Предостережения:

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

20 голосов
/ 19 октября 2008

Да, в некоторой степени это возможно. Существует модуль , который использует 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"
14 голосов
/ 18 октября 2008

За исключением изменения и перекомпиляции исходного кода (который возможен с открытым исходным кодом), изменение базового языка на самом деле невозможно.

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

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

12 голосов
/ 21 октября 2008

Общий ответ: вам нужно предварительно обработать ваши исходные файлы.

Более конкретный ответ: установите EasyExtend и выполните следующие шаги

i) Создать новый язычок (язык расширения)

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

Этого достаточно, чтобы определить синтаксис вашего нового оператора. Нетерминал 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 to 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, который позволяет программистам определять удобные вещи, такие как инфиксные операторы или небольшие операторы, используя просто удобное ОО программирование. Для более сложных вещей, таких как встраивание целых языков в Python с помощью построения языка, нет способа обойти полный грамматический подход.

9 голосов
/ 01 января 2011

Вот очень простой, но дурацкий способ добавления новых операторов, только в режиме интерпретации . Я использую его для маленьких однобуквенных команд для редактирования аннотаций генов, используя только 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

4 голосов
/ 18 октября 2008

Я нашел руководство по добавлению новых операторов:

https://troeger.eu/files/teaching/pythonvm08lab.pdf

По сути, чтобы добавить новые операторы, вы должны отредактировать Python/ast.c (среди прочего) и перекомпилировать двоичный файл python.

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

3 голосов
/ 20 октября 2008

Это можно сделать, используя EasyExtend :

EasyExtend (EE) - препроцессор генератор и метапрограммирование рамки написаны на чистом Python и интегрировано с CPython. Главный Целью EasyExtend является создание языков расширения, т.е. добавление Пользовательский синтаксис и семантика для Python.

2 голосов
/ 01 июля 2015

Это не совсем добавление новых операторов в синтаксис языка, но макросы являются мощным инструментом: https://github.com/lihaoyi/macropy

2 голосов
/ 08 января 2015

Некоторые вещи можно сделать с помощью декораторов. Давайте, например. Предположим, у 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())

Это довольно нечистое решение, как и здесь. Особенно поведение, когда декоратор вызывает функцию и устанавливает _ в None, является неожиданным. Для пояснения: этот декоратор эквивалентен написанию

def _(infile): ...
_ = mywith(open(...))(_) # mywith returns None.

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

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

...