Так уж получилось, что нам часто требуется определять одинаковые (или почти одинаковые) классы моделей SQLAlchemy для разных
MetaData
ну и, соответственно, с разными базовыми классами. Декларативно. Повсеместный copy-paste быстро надоел. Были мысли создавать второй класс путём копирования, но уж больно сложно получается: не так просто определить, где на какой глубине остановиться, а где заменить ссылки на что-то уже отзеркалированное. Гораздо проще сделать фабрику и создавать столько классов, сколько нужно. В питоне ж это просто:def create_C(Base):
class C(Base):
id = Column(Integer, primary_key=True)
# …
return C
Всё хорошо, только вот отступ лишний появляется. Кому-о, может, и мелочь, а нам он сильно не понравился. Неужели нельзя без него? Ну типа класс задекорировать чем-о так, чтобы он в фабрику превратился. Проблема в том, что тело класса выполняется сразу и только один раз, и никакими декораторами это правило не отменить. Ну да, можно отменить: всего-то с байткодом чуть поколдовать. Только уж очень гразным хак получается и переносимость между версиями под большим вопросом. Зато вот у функции тело можно выполнять когда захочешь и сколько угодно раз. Почему бы этим не воспользоваться и не превратить декоратором функцию в класс?
@create_class(Base)
def C():
id = Column(Integer, primary_key=True)
# …
return locals()
Опять что-то лишнее, теперь return locals()
. А в реальных задачах у нас появятся аргументы у функции (пространство имён с другими моделями, например — нам же надо как-то внешние ссылки да связи определять), которыми не захочется пространство имён класса засорять, так что строчка ещё усложнится.Вот бы здорово было бы вернуть локально определённые переменные автоматически. И это как раз можно сделать. Немного оптимизации (зачем нам держать трейсер весь вызов, это же приличные накладные расходы?), немного уважения к тем, кто это будет потом отлаживать дебаггером или профилировать, и получается такой декоратор:
import sys, functools, inspect
def return_locals(func):
'''Modifies decorated function to return its locals'''
@functools.wraps(func)
def wrap(*args, **kwargs):
frames = []
def tracer(frame, event, arg):
frames.append(frame)
sys.settrace(old_tracer)
if old_tracer is not None:
return old_tracer(frame, event, arg)
old_tracer = sys.gettrace()
sys.settrace(tracer)
try:
func(*args, **kwargs)
finally:
sys.settrace(old_tracer)
assert len(frames) == 1
argspec = inspect.getargspec(func)
argnames = list(argspec.args)
if argspec.varargs is not None:
argnames.append(argspec.varargs)
if argspec.keywords is not None:
argnames.append(argspec.keywords)
return {name: value
for name, value in frames.pop(0).f_locals.items()
if name not in argnames}
return wrap
Осталась мелочь, создать декоратор самой фабрики:
def create_class(*bases):
def wrapper(func):
return type(func.__name__, bases, return_locals(func)())
return wrapper
3 комментария:
Я бы ограничился первым вариантом и бил по рукам сотрудников, желающих пойти дальше.
Если исходить только из тех данных, которые я здесь привёл, то склонен с тобой согласиться. Есть только одно слабое оправдание: классов моделей бывает очень много и раздражение от лишнего отступа не такое уж и маленькое. Я постараюсь найти время, чтобы описать наши дальнейшие телодвижения, там оценка уже далеко не так очевидна.
Себе на будущее: в Python3 уже есть функция создания класса и можно поискать менее грязные решения.
Отправить комментарий