SQLAlchemy позволяет выносить часть определения модели в отдельный базовый класс, который будет потом «подмешиваться» к другим. Очень удобно, когда есть какой-то повторяющийся кусок в большом количестве классов моделей. Но есть один неприятный момент: все поля должны «знать», к какой модели они принадлежат, а для этого нужно копировать объект поля. С первым уровнем SQLAlchemy хорошо справляется, так что с простым
Column
всё путём, но ведь поля могут содержать ссылки на другие объекты (например,
ForeignKey
), к которым предъявляются такие же требования. И тут авторы SQLAlchemy пошли самым простым путём: если нельзя сделать автоматически на все случаи жизни, то не пусть делает пользователь вручную. В результате код миксина должен выглядеть примерно так:
class WithParent(object):
@declared_attr
def parent_id(cls):
return Column(ForeignKey(Parent.id))
@declared_attr
def parent(cls):
return relationship(Parent)
По сути обработанные
declared_attr
свойства решают ту же проблему, которую
мы уже решали декоратором return_locals
, а именно позволить выполнять код в определении класса несколько раз. Поэтому и решение напрашивается то же, только теперь нам нужно все свойства дополнительно завернуть в задекорированный метод. Понятно, что мы не хотим делать отдельный вызов нашей фабрики на каждый дескриптор, поэтому однажды полученный результат надо закешировать:
def declared_mixin(*args):
def wrapper(func):
attrs = weakref.WeakKeyDictionary()
def create_descriptor(name):
def get_attr(cls):
if cls not in attrs:
# Call func only once per class
attrs[cls] = return_locals(func)()
return attrs[cls][name]
get_attr.__name__ = name
return declared_attr(get_attr)
dict_ = {name: create_descriptor(name)
for name in func.func_code.co_varnames}
dict_['__doc__'] = func.__doc__
return type(func.__name__, args, dict_)
if len(args)==1 and not isinstance(args[0], type):
# Short form (without args) is used
func = args[0]
args = ()
return wrapper(func)
else:
return wrapper
Теперь наш пример миксина выглядит гораздо приятнее:
@declared_mixin
def WithParent():
parent_id = Column(ForeignKey(Parent.id))
parent = relationship(Parent)
В комментариях к прошлому посту Андрей Светлов
резонно заметил, что хак слишком грязен для столь небольшого эффекта. В ситуации же с миксином полученный эффект уже больше: если в исходном варианте на одну смысловую строчку кода приходилось две строчки шума, то здесь мы от шума полностью избавились. И дело даже не в том, что строк стало меньше, зашумлённый код гораздо сложнее читать. Вопрос поиска менее грязных путей получения нужного результата остаётся открытым.