Синтаксис декоратора @callable
действительно эксклюзивен для операторов def
и class
.Тем не менее, это просто синтаксический сахар .
Синтаксис
@name(arguments)
def functionname(...):
# ...
переводится в:
def functionname(...):
# ...
functionname = name(arguments)(functionname)
, то есть вызываемый объектвызывается @[decorator]
, и результат присваивается обратно имени функции (или имени класса, если применяется к оператору class
).
Вы всегда можете вызвать декоратор напрямую и назначить возвратзначение:
id = persisted_property(id=true)(Column(Integer, primaryKey=True, nullable=False))
Однако декораторы не имеют доступа к пространству имен, в котором создаются объекты!Тело оператора class
выполняется так, как если бы оно было функцией (хотя и с другими правилами области видимости), а результирующее локальное пространство имен используется для создания атрибутов класса.Декоратор - это просто еще один вызов функции в этом контексте, и локальное пространство имен тела класса не должно быть доступным.
Далее, я бы даже не стал строить ваш шаблон строителя .Это шаблон Java, где конфиденциальность и неизменность классов применяются в ущерб динамическим языковым шаблонам.Python не Java, не пытайтесь превратить его в Java.Например, вы не можете сделать экземпляры классов Python неизменными, это просто не то, что позволяет делать динамический язык.Более того, шаблон построителя - это решение проблемы, которой на самом деле не существует в Python, где вы можете создать свои аргументы для создания класса заранее, например, в словаре, который вы затем динамически применяете к вызову класса, тогда как Javaне имеет такой поддержки динамического вызова.
И вам не нужно использовать шаблон декоратора для пометки атрибутов схемы в любом случае .Вместо этого вам следует полагаться на собственную поддержку интроспекции в SQLAlchemy :
from sqlalchemy.inspection import inspect
class Builder:
def __init__(self, cls, existing=None, **attrs):
self.cls = cls
if existing is not None:
assert isinstance(existing, cls)
existing_attrs = {n: s.value for n, s in inspect(existing).attrs.items()}
# keyword arguments override existing attribute values
attrs = {**existing_attrs, **attrs}
self.attrs = attrs
def _create_attr_setter(self, attrname):
# create a bound attribute setter for the given attribute name
def attr_setter(self, value):
if attrname in self.attrs:
raise ValueError(f"{attrname} already has a value set")
return type(self)(self.cls, **self.attrs, **{attrname: value})
attr_setter.__name__ = f'set_{attrname}'
return attr_setter.__get__(self, type(self))
def __getattr__(self, name):
if name.startswith('set_'):
attrname = name[4:]
mapper = inspect(self.cls)
# valid SQLAlchemy descriptor name on the class?
if attrname in mapper.attrs:
return self._create_attr_setter(attrname)
raise AttributeError(name)
def build(self):
return self.cls(**self.attrs)
class BuilderMixin:
@classmethod
def builder(cls, existing=None):
return Builder(cls, existing)
, а затем просто использовать BuilderMixin
в качестве класса mixin:
>>> from sqlalchemy.ext.declarative import declarative_base
>>> from sqlalchemy import Column, Integer, String
>>> Base = declarative_base()
>>> class Foo(Base, BuilderMixin):
... __tablename__ = 'foo'
... id = Column(Integer, primary_key=True, nullable=False)
... text = Column(String(100))
... created_by = Column(Integer)
...
>>> Foo.builder().set_text('Demonstration text').set_created_by(1).build()
<__main__.Foo object at 0x10f8314a8>
>>> _.text, _.created_by
('Demonstration text', 1)
Вы можете прикрепить дополнительныеинформация для столбцов в словаре info
:
text = Column(String(100), info={'mutable': True})
, к которой ваш код компоновщика мог бы затем обращаться через маппер (например, mapper.attrs['text'].info.get('mutable', False)
).
Но опять же, вместо того, чтобы воссоздавать Javaшаблон построителя, просто создайте словарь attrs
напрямую и, самое большее, кодируйте правила изменчивости, используя гибридное свойство или события ORM .