Можно ли украсить атрибут класса - PullRequest
0 голосов
/ 22 мая 2018

Как собрать информацию об атрибутах класса во время создания класса?

В Java возможно то, о чем я спрашиваю.

В Python это не так.Поправьте меня, если я ошибаюсь!

Я создаю объект sqlalchemy, который определен декларативно.

class Foo(BASE):
   id = Column(Integer, primaryKey=True, nullable=False)
   text = Column(String(100))

Я хотел бы определить класс следующим образом:

class Foo(declarative_base()):
   @persisted_property(id=true)
   id = Column(Integer, primaryKey=True, nullable=False)

   @persisted_property(mutable=True)
   text = Column(String(100))

   @persisted_property(set_once=True)
   created_by = Column(Integer)

   @classmethod
   def builder(cls, existing=None):
       return Builder(cls, existing)

Класс persisted_property / функция /?Цель состоит в том, чтобы собрать атрибуты класса.С этим знанием могут произойти следующие вещи:

  1. * * * * * * * Класс-метод будет добавлен к классу Foo, который возвращает сгенерированный FooBuilder.FooBuilder будет иметь следующие методы: set_text()->FooBuilder, set_created_by()->FooBuilder, build()->Foo

  2. (в идеале) любая попытка прямого изменения объекта Foo будет заблокирована.(как разрешить sqlalchemy работать?)

Пример поведения:

  1. Foo.builder().set_text("Foo text").set_created_by(1).build()
  2. Foo.builder(existing_foo).set_text("Foo text").set_created_by(1).build(): вызовет Exception, так как existing_foo уже имеет значение для created_by

Примечания:

  1. Добавление декоратора уровня класса отделяет определение атрибута от декорации и кажется ... неправильным
  2. Украшения на уровне класса происходят после того, как sqlalchemy делает это волшебство.(это может быть хорошо или плохо)

Альтернативы?предложения?

Ответы [ 2 ]

0 голосов
/ 06 июня 2018

Это сработало для меня:

from abc import ABCMeta, abstractmethod
from functools import partial


class BaseDecorator(object):
    __metaclass__ = ABCMeta

    def __init__(self, *args, **kwargs):
        pass

    @abstractmethod
    def decorate(self, method, obj, *args, **kwargs):
        raise NotImplementedError()

    def __call__(self, method):
        class Wrapper(object):
            def __init__(self, parent, method):
                self.method = method
                self.parent = parent

            def __call__(self, obj, *args, **kwargs):
                return self.parent.decorate(self.method, obj, *args, **kwargs)

            def __get__(self, obj, cls):
                return partial(self.__call__, obj)
        return Wrapper(self, method)


class SomeDecorator(BaseDecorator):
    def __init__(self, goto=None):
        self.goto = goto

    def decorate(self, method, obj, *args, **kwargs):
        print("method was decorated")
        return method(obj, *args, **kwargs)


class Foo(object):
    @SomeDecorator(goto='/promo/')
    def get(self, request):
        return 'response'


if __name__ == '__main__':
    foo = Foo()
    print(foo.get('/layout/'))
0 голосов
/ 22 мая 2018

Синтаксис декоратора @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 .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...