简体   繁体   English

使用 python 元类和继承的问题

[英]Issues with using python metaclasses and inheritence

I've been working on a metaclass layout for a project whereby all classes use a custom metaclass that loads config they define, along with that of parents.我一直在为一个项目研究元类布局,其中所有类都使用一个自定义元类来加载他们定义的配置以及父类的配置。 Basically each class defines a nested Config class which is loaded to a dict, then children can also define one and the class uses all parent config with any new values overwritten.基本上每个 class 定义一个嵌套的Config class 加载到一个字典,然后孩子也可以定义一个 class 使用所有父配置并覆盖任何新值。

It works nicely when I don't remove the Config class after loading it to a dict, but now I am trying to refactor and clear up the namespace and it causes issues.当我在将其加载到字典后不删除 Config class 时,它工作得很好,但现在我正在尝试重构和清理命名空间,它会导致问题。 The new (broken) code is as follows:新的(坏掉的)代码如下:

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)

Which parses the Config class when used but now does not use any config from parents.它在使用时解析Config class 但现在不使用来自父母的任何配置。 The old code which worked but kept the nested classes after instantiation is:在实例化后有效但保留嵌套类的旧代码是:

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

It seems by now trying to access the config using the dict created by the metaclass, parent config is discarded.现在似乎尝试使用元类创建的字典访问配置,父配置被丢弃。 Any help would be much appreciated.任何帮助将非常感激。

Update更新

The issue turned out to be caused by some Mixins that use nested Config classes but don't use the metaclass.这个问题原来是由一些使用嵌套 Config 类但不使用元类的 Mixins 引起的。 This was fine in the old code block, but when changing to getting parent config from the config dict instead of the nested class, anything not using the metaclass will not have this defined, so instead has a Config class whose values are not used.这在旧代码块中很好,但是当更改为从配置字典而不是嵌套的 class 获取父配置时,任何不使用元类的东西都不会定义这个,因此有一个未使用值的配置 class。

Final working code including fixes and covering edge cases suggested by jsbueno:最终的工作代码,包括 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

The problem is that in the new code you are interacting over the class explicit bases , while the old (working) code iterates over __mro__ .问题在于,在新代码中,您通过 class 显式bases进行交互,而旧(工作)代码迭代__mro__

bases will yield only the explicit declared ancestors, and any "grandparents" or classes in a more complex hierarchy will not be visited. bases只会产生显式声明的祖先,并且不会访问任何“祖父母”或更复杂层次结构中的类。

The way to go is to allow Python generate the __mro__ by actually creating your new class, and iterating to retrieve your config keys on the new class. The way to go is to allow Python generate the __mro__ by actually creating your new class, and iterating to retrieve your config keys on the new class. The config attribute can just be set on the newly created class - no need to do that in the namespace. config属性可以只在新创建的 class 上设置 - 无需在命名空间中执行此操作。

What is not recommended is to try to replicate Python's __mro__ - it is a rather complex algorithm, and even if you follow step by step to get it right, you will be just reinventing the wheel.不建议尝试复制 Python 的__mro__ ——这是一个相当复杂的算法,即使你一步一步地做对了,你也只是在重新发明轮子。

So, something along:所以,有一些东西:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM