使用 python 元类和继承的问题

Issues with using python metaclasses and inheritence

我一直在为一个项目开发元class布局,其中所有 classes 使用自定义元class加载他们定义的配置,以及 parents。基本上每个 class 定义一个嵌套的 Config class 加载到一个字典,然后 children 也可以定义一个 class 使用所有 parent 覆盖所有新值的配置。

当我在将 Config class 加载到 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 但现在不使用 parent 中的任何配置。有效但在实例化后保留嵌套 classes 的旧代码是:

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

现在似乎尝试使用元class创建的字典访问配置,parent配置被丢弃。任何帮助将不胜感激。

更新

问题原来是由一些使用嵌套配置 classes 但不使用元 class 的 Mixins 引起的。这在旧代码块中很好,但是当更改为从配置字典而不是嵌套的 class 获取 parent 配置时,任何不使用 metaclass 的东西都不会定义这个,所以有一个 Config class 其值未被使用。

最终工作代码,包括 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

问题在于,在新代码中,您通过 class 显式 bases 进行交互,而旧(工作)代码则在 __mro__ 上进行迭代。

bases 将仅产生显式声明的祖先,并且不会访问更复杂层次结构中的任何 "grandparents" 或 classes。

方法是允许 Python 通过实际创建新的 class 生成 __mro__,并在新的 class 上迭代检索配置密钥. config 属性只能在新创建的 class 上设置 - 无需在命名空间中设置。

不推荐尝试复制 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