使用 python 元类和继承的问题

Posted

技术标签:

【中文标题】使用 python 元类和继承的问题【英文标题】:Issues with using python metaclasses and inheritence 【发布时间】:2020-07-26 10:15:25 【问题描述】:

我一直在为一个项目开发元类布局,其中所有类都使用自定义元类来加载他们定义的配置以及父类的配置。基本上每个类都定义了一个嵌套的Config 类,该类被加载到一个字典中,然后子类也可以定义一个,并且该类使用所有父配置并覆盖任何新值。

当我在将 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

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

更新

这个问题原来是由一些使用嵌套 Config 类但不使用元类的 Mixin 引起的。这在旧代码块中很好,但是当更改为从配置字典而不是嵌套类获取父配置时,任何不使用元类的东西都不会定义这个,所以会有一个不使用值的 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】:

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

bases 将只产生显式声明的祖先,并且不会访问任何“祖父母”或更复杂层次结构中的类。

要走的路是允许 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

【讨论】:

感谢您的回复!事实证明,失败的测试是由不使用元类但确实定义了 Config 类的 mixin 引起的。但是,您提出了 2 个优点,即 mro 比这里的基础更好,并且可能与定义的 dict 和类发生冲突,现在这些已得到修复。我将为任何偶然发现此问题的其他人发布带有工作代码的编辑。 使用 bases 而不是 __mro__ 会导致优先级不一致,但如果没有 mixin 元类错误,所有祖先配置仍然会被考虑。 它们将被访问,但在之前的传递中,并将结果“累积”在直接父类中。所以,是的,它确实适用于大多数情况,并且如果类配置在创建之后和创建孙子之前发生更改,则会出现一些奇怪的行为。

以上是关于使用 python 元类和继承的问题的主要内容,如果未能解决你的问题,请参考以下文章

复习打卡--0819元类和内存管理

Python 元类中的继承如何工作?

python 类和元类(metaclass)的理解和简单运用

Python学习 Day14 python 类和元类(metaclass)的理解和简单运用

旧式类、新式类和元类

参数化类和元类有啥区别(请使用 Python 中的代码示例)?