Воскресный батхерт-разогрев перед началом рабочей недели. Очередной рассказ про «слабую логическую связанность, составленность из груды костылей, неподдатливость расширению», то есть " питоничность".
Как вы декорируете блоки кода в Python? Ну то есть все мы знаем, что можно декорировать функцию:
@decorator
def funcname(arg):
pass
и даже декорировать класс:
@decorator
class classname:
def __init__(self):
pass
Но вот беда — как передать кусок кода в другую функцию? Вечная проблема лямбды в питоне, которая не решена до сих пор и никогда не будет решена:
>>> decorator(lambda: a = 2)
File "<stdin>", line 1
SyntaxError: lambda cannot contain assignment
Мне стыдно, но я до сих пор не подозревал, что в лямбдах нельзя использовать присваивание. Я подозреваю, что я не один такой, на самом деле, потому что лямбды почти никто не использует — в том числе по этой причине.
«Хорошо, наш язык говно, но давайте не отчаиваться, с этим что-то можно сделать» — сказал когда-то Гвидо, и предложил:
https://www.python.org/dev/peps/pep-0343/ — PEP 343 — The «with» Statement
Этот чудесный костыль позволяет вам обрамить любой блок кода процедурами инициализации и финализации. Проблема — что мне делать, если я хочу обрамить блок кода сложнее, например, чтобы он оказался в цикле? То есть
for filename in filenames:
with try_infinitely():
print(open(filename, 'r').readlines())
В данном случае код должен пытаться бесконечно открывать и читать файл, пока не сможет это сделать. На питоне это можно написать как:
for filename in filenames:
while True:
try:
print(open(filename, 'r').readlines())
except Exception as e:
print(e)
else:
break;
Но писать такую стенку каждый раз весьма утомительно. Как и заставлять юзверя объявлять вложенную функцию и передавать ее функции-декоратору, поскольку это будет нарушать обычную читаемость кода из-за того, что код вложенной функции будет выполняться позже своего объявления, в отличие от тех же лямбд JS, которые выполняются там, где определены. Чтобы была чуть более очевидна блевотность такого кода, я специально добавил цикл for filename in filenames
снаружи:
for filename in filenames:
@try_infinitely
def nestedfunc():
print(open(filename, 'r').readlines())
nestedfunc()
или
@try_infinitely
def nestedfunc():
print(open(filename, 'r').readlines())
for filename in filenames:
nestedfunc()
Последнее, к моему удивлению, работает, потому что области видимости в питоне поделены на уровне функций, а не блоков, и потому filename будет видима в любом месте функции.
А хотелось бы чего-то простого и понятного, плана:
for filename in filenames:
@try_infinitely:
print(open(filename, 'r').readlines())
Чувствуете, как код сразу стал намного проще? Единственный выход, который я пока что вижу — это городить свой DSL через
source = inspect.get_source(func)
ast.parse(source, func.__code__.co_filename, 'exec')
К сожалению, питон из коробки не позволяет кормить AST декоратору, а сам AST он нигде не хранит, потому остается только парсить сырцы.