Заставить класс с множественным наследованием иметь определенный метакласс в Python - PullRequest
1 голос
/ 21 октября 2019

У меня есть класс ( Схема зефира ), который подклассов двух родителей:

class OptionalLinkSchema(JsonApiSchema, ModelSchema): pass
  • JsonApiSchema имеет метакласс SchemaMeta
  • ModelSchema имеет метакласс ModelSchemaMeta(SchemaMeta) (это подкласс ModelSchema).

Теперь я не хочу метакласс ModelSchemaMeta для моего классаЯ просто хочу более простой SchemaMeta. Однако, согласно Python docs , «выбран самый производный метакласс», что означает, что независимо от того, что я делаю, ModelSchemaMeta будет выбран в качестве метакласса.

Даже если япопробуйте вручную выбрать метакласс, происходит то же самое:

class OptionalLinkSchema(JsonApiSchema, ModelSchema, metaclass=SchemaMeta): pass

print(type(OptionalLinkSchema))
<class 'marshmallow_sqlalchemy.schema.ModelSchemaMeta'>

Нет ли способа переопределить поведение Python при выборе самого производного метакласса?

1 Ответ

6 голосов
/ 21 октября 2019

Перво-наперво: я должен предупредить вас, что у вас, вероятно, есть проблема "xy": если класс предназначен для работы с механизмами в его метаклассе, если они не запускаются при создании, скорее всего,класс не будет работать.

Второе: язык не будет делать это "сам по себе". И это не будет, потому что это сломало бы вещи по причине выше. Итак, есть несколько подходов, которые можно использовать, все они навязчивы и ожидают, что вы знаете, что делаете. Если ваш OptionaLinkSchema зависит от каких-либо особенностей ModelSchemaMeta, он просто сломается.

Я могу придумать 3 способа достижения эквивалента «удаления наследуемого метакласса»

Первый подход

Вы ничего не говорите по своему вопросу, но я не вижу ModelSchemaMeta в исходном дереве Зефира - это метакласс, который вы создали? Если так, то, поскольку Marshmallow использует идиому «djangoish» наличия вложенного класса Meta для объявления вещей о самом классе, вы можете использовать атрибут в этом пространстве имен Meta, чтобы пропустить свой собственный метакласс.

Если это так, добавьте код, подобный этому, в ваш метод нежелательного метакласса '__new__ (я выбираю код синтаксического анализа "meta" из самого исходного кода Marshmallow):

class ModelSchemaMeta(SchemaMeta):

    def __new__(mcs, name, bases, attrs):
        meta = attrs.get("Meta")
        if getattr(meta, "skip_model_schema_meta", False):
            # force ShemaMeta metaclass
            cls = super().__new__(SchemaMeta, name, bases, attrs)
            # manually call `__init__` because Python does not do that
            # if the value returned from `__new__` is not an instance
            # of `mcs`
            cls.__init__(name, bases, attrs)
            return cls
        # Your original ModelSchemaMeta source code goes here



class OptionalLinkSchema(...):
    ...

    class Meta:
        skip_model_schema_meta = True
        ...

Второй подход

Чрезвычайно более инвазивный способ сделать это, но это будет работать для любого дерева наследования классов, имеет код, который возьмет суперкласс с нежелательным метаклассом и создаст его клон. Тем не менее, поскольку имеется собственный метакласс, отличный от type, шансы на такую ​​работу в этом случае невелики - поскольку процесс «клонирования» не будет передавать метаклассу «прародителя» ожидаемые необработанные поля, которые он хочет преобразовать ватрибуты в финальном классе. Кроме того, Marsmallow использует реестр классов - для создания такого промежуточного клона также будет зарегистрирован клон.

Вкратце: этот подход не применим к вашему случаю, но я опишу его здесь, поскольку он может быть полезендля тех, кто задал этот вопрос:

def strip_meta(cls,):
    "builds a clone of cls, stripping the most derived metaclass it has.

    There are no warranties the resulting cls will work at all - specially
    if one or more of the metaclasses that are being kept make transformations
    on the attributes as they are declared in the class body.
    "
    new_meta = type(cls).__mro__[1:]
    return (new_meta(cls.__name__, cls.__mro__, dict(cls.__dict__)))



class OptionalLinkSchema(FirstClass, strip_meta(SecondClass)):
    ...

(Любой, кто пытается использовать это, не то, что если SecondClass сам по себе является производным от других, использующих нежелательный метакласс, стратегию необходимо изменить, чтобы рекурсивно удалить метакласстакже суперклассы)

Третий подход

Другой, более простой подход - создать вручную производный метакласс и просто жестко закодировать вызовы к желаемому метаклассу, пропустив суперкласс. Эта вещь может дать достаточно «трезвый» код, чтобы его можно было использовать на практике.

Поэтому вы не «устанавливаете произвольный метакласс» - вместо этого вы создаете допустимый метакласс, который делает то, что вам нужно.

class NewMeta(ModelSchemaMeta, SchemaMeta):
    """Order of inheritance matters: we ant to skip methods on the first 
    baseclass, by hardwiring calls to SchemaMeta. If we do it the other way around,
    `super` calls on SchemaMeta will go to ModelSchemaMeta).

    Also, if ModeSchemaMeta inherits from SchemaMeta, you can inherit from it alone

    """

    def __new__(*args, **kw):
        return SchemaMeta.__new__(*args, **kw)


    def __init__(*args, **kw):
        return SchemaMeta.__init__(*args, **kw)

    # repeat for other methos you want to use from SchemaMeta
    # also, undesired attributes can be set to "None":

    undesired_method_in_model_schema_meta = None

Отличие от первого подхода состоит в том, что ModelSchemaMeta будет деревом наследования для метакласса, а конечный метакласс будет этим новым, производным метаклассом. Но это не будет зависеть от того, что вы контролируете код ModelSchemaMeta - и, как я уже говорил, это больше "в рамках ожидаемых правил" языка, чем второй подход.

...