使用 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
我一直在为一个项目开发元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