Проблемы с использованием python метаклассов и наследования - PullRequest
0 голосов
/ 13 апреля 2020

Я работал над макетом метакласса для проекта, в котором все классы используют собственный метакласс, который загружает заданную ими конфигурацию, а также конфигурацию родителей. По сути, каждый класс определяет вложенный класс Config, который загружается в dict, тогда дети также могут определять его, и класс использует всю родительскую конфигурацию с любыми новыми значениями, перезаписанными.

Это прекрасно работает, когда я не удалите класс Config после загрузки его в dict, но теперь я пытаюсь реорганизовать и очистить пространство имен, и это вызывает проблемы. Новый (неработающий) код выглядит следующим образом:

class AbstractConfigMeta(ABCMeta):
    """Parse nested Config classes and fill a new classes config dict."""

    def __new__(mcs, name, bases, namespace):
        """Traverse the MRO backwards from object to load all Config classes.

        Any config declared in sub classes overwrites base classes.
        """
        # get any config defined in parent classes first
        config = {}
        for parent in reversed(bases):
            if hasattr(parent, "config"):
                config.update(parent.config)
        # pop Config class and add values if defined
        config_class = namespace.pop("Config", None)
        if config_class:
            # get all non-magic (i.e. user-defined) attributes
            attributes = {
                key: value
                for key, value in config_class.__dict__.items()
                if not key.startswith("__")
            }
            config.update(attributes)

        namespace["config"] = config
        return super().__new__(mcs, name, bases, namespace)

, который анализирует класс Config при использовании, но теперь не использует никакой конфигурации от родителей. Старый код, который работал, но сохранил вложенные классы после создания экземпляра:

class AbstractConfigMeta(ABCMeta):
    """Parse nested Config classes and fill a new classes config dict."""

    def __new__(mcs, name, bases, namespace):
        """Traverse the MRO backwards from object to load all Config classes.

        Any config declared in sub classes overwrites base classes.
        """
        new_class = super().__new__(mcs, name, bases, namespace)
        new_class.config = {}  # type: ignore

        for parent in reversed(new_class.__mro__):
            config_class = getattr(parent, "Config", None)
            if config_class:
                # get all non-magic attributes from each Config class
                values = {
                    key: value
                    for key, value in config_class.__dict__.items()
                    if not key.startswith("__")
                }
                new_class.config.update(values)  # type: ignore
        return new_class

Кажется, что теперь, пытаясь получить доступ к конфигурации, используя dict, созданный метаклассом, родительская конфигурация отбрасывается. Любая помощь будет принята с благодарностью.

Обновление

Проблема, как оказалось, вызвана некоторыми миксинами, которые используют вложенные классы Config, но не используют метакласс. Это было хорошо в старом блоке кода, но при переходе к получению родительской конфигурации из config config вместо вложенного класса, все, что не использует метакласс, не будет иметь этого определения, поэтому вместо этого есть класс Config, значения которого не используются.

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

class AbstractConfigMeta(ABCMeta):
    """Parse nested Config classes and fill a new classes config dict."""

    def __new__(mcs, name, bases, namespace):
        """Traverse the MRO backwards from object to load any config dicts.

        Any Config class declared in sub classes overwrites parent classes.
        """
        # pop Config class and add its attributes if defined
        config_class = namespace.pop("Config", None)
        if config_class:
            # get all non-magic (i.e. user-defined) attributes
            attributes = {
                key: value
                for key, value in config_class.__dict__.items()
                if not key.startswith("__")
            }
            if namespace.get("config"):
                warnings.warn(
                    f"A config dict and a config class are defined for {name}."
                    + " Any values in the config dict will be overwritten."
                )
            namespace["config"] = attributes

        new_class = super().__new__(mcs, name, bases, namespace)
        # get any config dicts defined in the MRO (including the current class)
        config = {}
        for parent in reversed(new_class.__mro__):
            if hasattr(parent, "config"):
                config.update(parent.config)  # type: ignore

        new_class.config = config  # type: ignore
        return new_class

1 Ответ

0 голосов
/ 13 апреля 2020

Проблема в том, что в новом коде вы взаимодействуете с классом, явно заданным bases, в то время как старый (рабочий) код выполняет итерации по __mro__.

bases приведет только к явно объявленным предкам, и любые "дедушки и бабушки" или классы в более сложной иерархии не будут посещены.

Способ go - разрешить Python сгенерируйте __mro__, фактически создав новый класс и выполнив итерацию для получения ключей конфигурации для нового класса. Атрибут config можно просто установить во вновь созданном классе - нет необходимости делать это в пространстве имен.

Не рекомендуется пытаться реплицировать Python '__mro__ - это довольно сложный алгоритм, и даже если вы будете следовать пошаговым инструкциям, чтобы все было правильно, вы просто заново изобретаете колесо.

Итак, что-то не так:

class AbstractConfigMeta(ABCMeta):
    """Parse nested Config classes and fill a new classes config dict."""

    def __new__(mcs, name, bases, namespace):
        """Traverse the MRO backwards from object to load all Config classes.

        Any config declared in sub classes overwrites base classes.
        """


        config_class = namespace.pop("Config", None)

        cls = super().__new__(mcs, name, bases, namespace)
        # get any config defined in parent classes first
        config = {}

        for parent in reversed(cls.__mro__):
            # Keep in mind this also runs for `cls` itself, so "config" can
            # also be specced as a dictionary. If you don't want that
            # to be possible, place a condition here to raise if `parent is cls and hasattr...`
            if hasattr(parent, "config"):
                config.update(parent.config)
        # pop Config class and add values if defined

        if config_class:
            # get all non-magic (i.e. user-defined) attributes
            attributes = {
                key: value
                for key, value in config_class.__dict__.items()
                if not key.startswith("__")
            }
            config.update(attributes)

        cls.config = config
        return cls
...