Подтвердить что ты не робот

Недостаток метакласса SqlAlchemy

Я пытаюсь внедрить часть своего собственного кода в процесс построения класса SqlAlchemy. Пытаясь понять код, меня несколько смущает реализация метакласса. Вот соответствующие фрагменты:

По умолчанию "метакласс" SqlAlchemy:

class DeclarativeMeta(type):
    def __init__(cls, classname, bases, dict_):
        if '_decl_class_registry' in cls.__dict__:
            return type.__init__(cls, classname, bases, dict_)
        else:
            _as_declarative(cls, classname, cls.__dict__)
        return type.__init__(cls, classname, bases, dict_)

    def __setattr__(cls, key, value):
        _add_attribute(cls, key, value)

declarative_base выполняется следующим образом:

def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
                     name='Base', constructor=_declarative_constructor,
                     class_registry=None,
                     metaclass=DeclarativeMeta):
     # some code which should not matter here
     return metaclass(name, bases, class_dict)

Он используется следующим образом:

Base = declarative_base()

class SomeModel(Base):
    pass

Теперь я получил свой собственный метакласс следующим образом:

class MyDeclarativeMeta(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        result = DeclarativeMeta.__init__(cls, classname, bases, dict_)
        print result
        # here I would add my custom code, which does not work
        return result

И используйте его следующим образом:

Base = declarative_base(metaclass=MyDeclarativeMeta)

Хорошо, теперь к моей проблеме:

  • print result в моем собственном классе всегда печатает None.
  • Кажется, что код работает!?
  • Почему метакласс использует __init__, а не __new__
  • declarative_base возвращает экземпляр этого класса. Не следует ли возвращать класс, имеющий атрибут __metaclass__, имеющий MyDeclarativeMeta как значение?

Так что мне интересно, почему код работает вообще. Поскольку люди SqlAlchemy, очевидно, знают, что они делают, я предполагаю, что я нахожусь на совершенно неправильном пути. Может ли кто-нибудь объяснить, что здесь происходит?

4b9b3361

Ответ 1

Прежде всего. __init__ требуется , чтобы вернуть None. Python docs говорят, что "никакое значение не может быть возвращено", но в Python "отбрасывание конца" функции без нажатия оператора return эквивалентно до return None. Поэтому явное возвращение None (либо в виде литерала, либо путем возврата значения выражения, приводящего к None) тоже не наносит вреда.

Поэтому метод __init__ DeclarativeMeta, который вы цитируете, выглядит немного странным для меня, но он не делает ничего плохого. Здесь снова добавлены некоторые комментарии, добавленные мной:

def __init__(cls, classname, bases, dict_):
    if '_decl_class_registry' in cls.__dict__:
        # return whatever type (our superclass) __init__ returns
        # __init__ must return None, so this returns None, which is okay
        return type.__init__(cls, classname, bases, dict_)
    else:
        # call _as_declarative without caring about the return value
        _as_declarative(cls, classname, cls.__dict__)
    # then return whatever type __init__ returns
    return type.__init__(cls, classname, bases, dict_)

Это может быть сжато и чисто написано как:

def __init__(cls, classname, bases, dict_):
    if '_decl_class_registry' not in cls.__dict__:
        _as_declarative(cls, classname, cls.__dict__)
    type.__init__(cls, classname, bases, dict_)

Я понятия не имею, почему разработчики SqlAlchemy считали, что нужно возвращать все type.__init__ (которые ограничены None). Возможно, это защищает от будущего, когда __init__ может что-то вернуть. Возможно, это просто для согласованности с другими методами, где основная реализация - путем отсрочки на суперкласс; обычно вы возвращаете все вызовы суперкласса, если вы не хотите его обрабатывать. Однако он, конечно же, ничего не делает.

Итак, ваша печать print result None показывает, что все работает по назначению.


Далее, давайте более подробно рассмотрим, что означает метаклассы. Метакласс - это класс класса. Как и любой класс, вы создаете экземпляры метакласса (например, классы), вызывая метакласс. Синтаксис блока классов на самом деле не является тем, что создает классы, это просто очень удобный синтаксический сахар для определения словаря, а затем передача его вызову метакласса для создания объекта класса.

Атрибут __metaclass__ - это не то, что делает магию, это просто гигантский взлом для передачи информации "Я бы хотел, чтобы этот классный блок создавал экземпляр этого метакласса вместо экземпляра type" через обратный канал, потому что нет надлежащего канала для передачи этой информации интерпретатору. 1

Это, вероятно, будет более ясным с примером. Возьмите следующий класс:

class MyClass(Look, Ma, Multiple, Inheritance):
    __metaclass__ = MyMeta

    CLASS_CONST = 'some value'

    def __init__(self, x):
        self.x = x

    def some_method(self):
        return self.x - 76

Это примерно синтаксический сахар для выполнения следующих 2:

dict_ = {}

dict_['__metaclass__'] = MyMeta
dict_['CLASS_CONST'] = 'some value'

def __init__(self, x):
    self.x = x
dict_['__init__'] = __init__

def some_method(self):
    return self.x - 76
dict_['some_method'] = some_method

metaclass = dict_.get('__metaclass__', type)
bases = (Look, Ma, Multiple, Inheritance)
classname = 'MyClass'

MyClass = metaclass(classname, bases, dict_)

Итак, "класс, имеющий атрибут __metaclass__, имеющий [метакласс] как значение" IS экземпляр метакласса! Это точно то же самое. Единственное различие заключается в том, что если вы создаете класс напрямую (путем вызова метакласса), а не с помощью блока класса и атрибута __metaclass__, то он не обязательно должен иметь __metaclass__ в качестве атрибута. 3

Этот вызов metaclass в конце точно так же, как и любой другой вызов класса. Он вызовет metaclass.__new__(classname, bases, dict_), чтобы создать объект класса, затем вызовите __init__ в результирующем объекте для его инициализации.

Метакласс по умолчанию type делает только что-нибудь интересное в __new__. И большинство применений для метаклассов, которые я видел в примерах, на самом деле просто запутанный способ реализации декораторов классов; они хотят сделать некоторую обработку, когда класс создан, и после этого не заботятся. Поэтому они используют __new__, потому что он позволяет им выполнять как до, так и после type.__new__. В результате все думают, что __new__ - это то, что вы реализуете в метаклассах.

Но вы можете иметь метод __init__; он будет вызываться в новом объекте класса после его создания. Если вам нужно добавить некоторые атрибуты в класс или записать объект класса в реестр где-нибудь, это на самом деле немного более удобное место для этого (и логически правильное место), чем __new__.


1 В Python3 это адресовано добавлением metaclass в качестве "аргумента ключевого слова" в список базового класса, а не как атрибут класса.

2 В действительности это немного усложняется из-за необходимости совместимости метакласса между построенным классом и всеми базовыми элементами, но это основная идея.

3 Не то, чтобы даже класс с метаклассом (кроме type), созданный обычным способом, обязательно должен был иметь __metaclass__ как атрибут; правильный способ проверки класса класса аналогичен проверке класса чего-либо еще; используйте cls.__class__ или примените type(cls).

Ответ 2

__init__ в версии SQLAlchemy не так, в основном. Вероятно, это было написано три года назад, когда вырезали и вставляли метакласс где-то, или, возможно, он начинался как другой метод, который позже стал __init__ и просто не был изменен. Я только что проверил 0,5, когда он был впервые написан, и он выглядит в основном тем же самым, с ненужным утверждением "return". Исправляя это сейчас, жаль, что это вас смутило.

Ответ 3

  • print result в моем собственном классе всегда печатает None.

Это потому, что конструктор ничего не возвращает:)

  • Почему метакласс использует __init__, а не __new__

Я думаю, что это потому, что SQLAlchemy необходимо хранить ссылку cls в реестре декларативного класса. В __new__ класс еще не существует (см. fooobar.com/info/153279/...).

Когда я подклассифицировал DeclarativeMeta, я фактически сделал все в __init__ после кода SQLAlchemy. В ретроспективе после прочтения вашего вопроса, мой код должен использовать __new__ вместо этого.

  • declarative_base возвращает экземпляр этого класса. Не должен ли он возвращать класс, имеющий атрибут __metaclass__ имеющий MyDeclarativeMeta как значение?

Я думаю, что Бен объяснил это очень хорошо. В любом случае, если вы хотите (не рекомендуется), вы можете пропустить вызов declarative_base() и создать свой собственный базовый класс, например.

# Almost the same as:
#   Base = declarative_base(cls=Entity, name='Base', metaclass=MyDeclarativeMeta)
# minus the _declarative_constructor.
class Base(Entity):
    __metaclass__ = MyDeclarativeMeta

    _decl_class_registry = dict()
    metadata = MetaData()

В этом случае атрибут __metaclass__ будет там. Я фактически создал свой базовый класс, как это, чтобы помочь PyCharm получить автозаполнение для вещей, определенных в Entity.