#python #sqlalchemy #metaclass #setattr
#python #sqlalchemy #метакласс #setattr
Вопрос:
У меня есть класс, который является потомком декларативной базы SQLAlchemy. Мне нужно написать объект-мост, который будет транслировать между декларативной базой и другой системой, которую я запускаю, но я хочу, чтобы объекту-мосту не нужно было точно знать, какие столбцы находятся в моей записи. В таком случае мне нужен способ перечислить все столбцы в записи, чтобы избежать необходимости поддерживать список атрибутов в двух лишь смутно связанных местах и одновременно минимизировать репликацию. Это означает, что я мог бы создать атрибуты столбца, а затем поддерживать отдельный список имен столбцов, но опять же, репликация является фактором — если бы я захотел изменить структуру этой вещи позже, мне пришлось бы изменить ее в обоих местах.
В таком случае я подумал о том, чтобы вставить определения столбцов в dict, а затем выполнить итерацию по dict для создания определений каждого столбца с помощью setattr, вот так:
for k,v in self.__column_dict.items():
setattr(self,k,
sqlalchemy.Column(v['column_type'],**v.get('opts',{})),
)
Теперь вот в чем загвоздка: если я вставлю этот фрагмент кода на уровне класса, self еще не определен, и я получу сообщение об ошибке (что имеет смысл). Если я вставлю это __init__
, self будет определен, но декларативная база SQLAlchemy выдает ошибку, потому что к моменту завершения определения класса в таблице нет первичного ключа (что имеет смысл, поскольку эти атрибуты не будут установлены до времени выполнения).
Итак, с учетом всего сказанного, как мне получить то, что я хочу, не используя eval, или использование eval здесь мой единственный вариант?
РЕДАКТИРОВАТЬ: я принял решение ниже, но не использовал его на 100% (хотя это было абсолютно полезно для получения мной ответа). Вот что я в итоге сделал: (обратите внимание, что DeclarativeMeta является частью sqlalchemy.ext.declarative , того же пакета, что и declarative_base)
class MyMeta(DeclarativeMeta):
def __new__(meta, classname, bases, dict_):
klass = type.__new__(meta, classname, bases, dict_)
if dict_.has_key('__classinit__'):
klass.__classinit__ = staticmethod(klass.__classinit__.im_func)
klass.__classinit__(klass, dict_)
return klass
Затем в моем производном классе,
class DerivedClass(declarative_base()):
__tablename__ = 'yaddayadda'
__metaclass__ = MyMeta
def __classinit__(klass, dict_):
klass.__column_dict = <column dict here>
for k,v in klass.__column_dict.items():
setattr(klass,k,
<fancy column stuff>
)
...<rest of class definition>...
Ответ №1:
Это можно сделать, предоставив метакласс declarative_base и в этом метаклассе
from sqlalchemy import Column, Integer
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
class MyMeta(DeclarativeMeta):
def __init__(klass, classname, bases, dict_):
for k, v in dict_.items():
if k.endswith('__column_dict'):
for name, datatype, is_pk in v:
setattr(klass, name, Column(name, datatype, primary_key=is_pk))
return DeclarativeMeta.__init__(klass, classname, bases, dict_)
Base = declarative_base(metaclass=MyMeta)
class Bob(Base):
__tablename__ = 'bob'
__column_dict = [('a', Integer, True), ('b', Integer, False)]
bob = Bob()
print bob.a
print bob.b
Комментарии:
1. Большое вам спасибо. Чтобы быть уверенным, должен ли я также вызывать super в конце для проверки на будущее? Например, вернуть super(RecordMeta, klass).__init__(classname, bases, dict_)?
2. Возвращаясь, когда я реализую это, я получаю «объект типа ‘MyDerivedObject’ не имеет атрибута ‘_MyMeta__column_dict». Похоже, что поиск column_dict привязывается к RecordMeta, как мне указать ему, где на самом деле искать?
3. Извините, я привел неаккуратный пример. Добавлена новая и полная.
4. Спасибо, довольно изящно, хотя кажется пугающим быть двусмысленным, используя «endswith» в имени атрибута.