从 Python 中的元类继承

Inheritance from metaclasses in Python

我有简单的元数据class,它将 classes 以“get_”开头的方法转换为属性:

class PropertyConvertMetaclass(type):
    def __new__(mcs, future_class_name, future_class_parents, future_class_attr):
        new_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                if name.startswith('get_'):
                    new_attr[name[4:]] = property(val)
            else:
                new_attr[name] = val
        return type.__new__(mcs, future_class_name, future_class_parents, new_attr)

假设我有 TestClass:

class TestClass():
    def __init__(self, x: int):
        self._x = x

    def get_x(self):
        print("this is property")
        return self._x

我希望它像这样工作:我创建了一些新的 class 有点继承了它们

class NewTestClass(TestClass, PropertyConvertMetaclass):
    pass

我可以像这样重用他们的两种方法:

obj = NewTestClass(8)
obj.get_x() # 8
obj.x       # 8

按照我的想法,我应该创建一个新的 class,让我们将其命名为 PropertyConvert 并使 NewTestClass 继承自它:

class PropertyConvert(metaclass=PropertyConvertMetaclass):
    pass

class NewTestClass(TestClass, PropertyConvert):
    pass

但这无济于事,我仍然无法将新的 属性 方法与 NewClassTest 一起使用。如何让 PropertyConvert 继承其兄弟的所有方法,而不在 NewClassTest 中做任何事情,仅更改 PropertyConverterMetaclass 或 PropertyConverter?我是 metaclasses 的新手,如果这个问题看起来很愚蠢,我很抱歉。

当您执行 TestClass(): 时,class 的主体在名称空间中为 运行,该名称空间成为 class __dict__。 metaclass 只是通过 __new____init__ 通知命名空间的构造。在这种情况下,您已将 TestClass 的元 class 设置为 type.

当您从 TestClass 继承时,例如。 G。对于 class NewTestClass(TestClass, PropertyConverter):,您编写的 PropertyConvertMetaclass 版本仅在 NewTestClass__dict__ 上运行。 TestClass 已在此时创建,没有任何属性,因为它的元 class 方式 type,而子 class 是空的,所以您看不到任何属性。

这里有几个可能的解决方案。更简单但由于您的任务而无法实现的是 class TestClass(metaclass=PropertyConvertMetaclass):TestClass 的所有子代都将拥有 PropertyConvertMetaclass,因此所有 getter 都将转换为属性。

另一种方法是仔细查看 PropertyConvertMetaclass.__new__ 的参数。一般情况下,你只对future_class_attr属性进行操作。但是,您也可以访问 future_class_bases。如果您想升级 PropertyConverter 的直系兄弟姐妹,这就是您所需要的:

class PropertyConvertMetaclass(type):
    def __new__(mcs, future_class_name, future_class_parents, future_class_attr):
        # The loop is the same for each base __dict__ as for future_class_attr,
        # so factor it out into a function
        def update(d):
            for name, value in d.items():
                # Don't check for dunders: dunder can't start with `get_`
                if name.startswith('get_') and callable(value):
                    prop = name[4:]
                    # Getter and setter can't be defined in separate classes
                    if 'set_' + prop in d and callable(d['set_' + prop]):
                        setter = d['set_' + prop]
                    else:
                        setter = None
                    if 'del_' + prop in d and callable(d['del_' + prop]):
                        deleter = d['del_' + prop]
                    else:
                        deleter = None
                    future_class_attr[prop] = property(getter, setter, deleter)

        update(future_class_dict)
        for base in future_class_parents:
            # Won't work well with __slots__ or custom __getattr__
            update(base.__dict__)
    return super().__new__(mcs, future_class_name, future_class_parents, future_class_attr)

这对于您的作业来说可能已经足够了,但缺乏一定的技巧。具体来说,我能看到的不足有两个:

  1. 没有超出直接基数 classes 的查找。
  2. 您不能在一个 class 中定义 getter 而在另一个中定义 setter。

要解决第一个问题,您将必须遍历 class 的 MRO。由于 ,使用 __init__ 而不是 class 之前的字典,在完全构造的 class 上更容易做到这一点。我将通过在创建任何属性之前创建可用 getter 和 setter 的 table 来解决第二个问题。您还可以通过这样做使属性尊重 MRO。使用 __init__ 的唯一麻烦是您必须在 class 上调用 setattr 而不是简单地更新它的未来 __dict__.

class PropertyConvertMetaclass(type):
    def __init__(cls, class_name, class_parents, class_attr):
        getters = set()
        setters = set()
        deleters = set()

        for base in cls.__mro__:
            for name, value in base.__dict__.items():
                if name.startswith('get_') and callable(value):
                    getters.add(name[4:])
                if name.startswith('set_') and callable(value):
                    setters.add(name[4:])
                if name.startswith('del_') and callable(value):
                    deleters.add(name[4:])

        for name in getters:
            def getter(self, *args, **kwargs):
                return getattr(super(cls, self), 'get_' + name)(*args, **kwargs)
            if name in setters:
                def setter(self, *args, **kwargs):
                    return getattr(super(cls, self), 'set_' + name)(*args, **kwargs)
            else:
                setter = None
            if name in deleters:
                def deleter(self, *args, **kwargs):
                    return getattr(super(cls, self), 'del_' + name)(*args, **kwargs)
            else:
                deleter = None
            setattr(cls, name, property(getter, setter, deleter)

您在 metaclass 的 __init__ 中所做的任何事情都可以使用 class 装饰器轻松完成。主要区别在于元class 将应用于所有子 classes,而装饰器仅在使用它的地方应用。

没有什么是“不可能”的。 这是一个问题,无论多么不寻常,都可以用 metaclasses.

来解决

你的方法很好 - 你遇到的问题是,当你查看“future_class_attr”(也称为 class 正文中的名称空间)时,它只包含方法和class 当前正在定义 的属性。在您的示例中,NewTestClass 为空,“future_class_attr”也是如此。

克服这个问题的方法是检查所有基础 classes,寻找与您正在寻找的模式相匹配的方法,然后创建适当的 属性.

之前创建目标 class 正确执行此操作会很棘手 - 因为必须在所有对象的正确 mro(方法解析顺序)中进行属性搜索superclasses - 可能会有很多极端情况。 (但请注意,这并非“不可能”)

但是在 创建新的 class 之后,没有什么能阻止您这样做 。为此,您可以将 super().__new__(mcls, ...) 的 return 值分配给一个变量(顺便说一下,更喜欢使用 super().__new__ 而不是硬编码 type.__new__:这允许您的元 class 进行协作并与 collections.ABC 或 enum.Enum 结合使用)。该变量是您完成的 class 并且您可以在其上使用 dir 来检查所有属性和方法名称,已经合并所有 superclasses - 然后,只需创建您的新属性和然后用 setattr(cls_variable, property_name, property_object).

分配给新创建的 class

更好的是,编写 metaclass __init__ 而不是它的 __new__ 方法:你检索已经创建的新 class,并且可以继续自省它dir 并立即添加属性。 (尽管 你的 class 不需要它,但不要忘记调用 super().__init__(...)。)

此外,请注意,自 Python 3.6 起,如果仅在一个基地 class.

我的问题的解决方案之一是在 PropertyConvertMetaclass 中解析 parents' 指令:

class PropertyConvertMetaclass(type):
    def __new__(mcs, future_class_name, future_class_parents, future_class_attr):
        new_attr = {}
        for parent in future_class_parents:
            for name, val in parent.__dict__.items():
                if not name.startswith('__'):
                    if name.startswith('get_'):
                        new_attr[name[4:]] = property(val, parent.__dict__['set_' + name[4:]])
                new_attr[name] = val

        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                if name.startswith('get_'):
                    new_attr[name[4:]] = property(val, future_class_attr['set_'+name[4:]])
            new_attr[name] = val
        return type.__new__(mcs, future_class_name, future_class_parents, new_attr)