【问题标题】:Inheritance from metaclasses in Python从 Python 中的元类继承
【发布时间】:2022-01-18 07:49:01
【问题描述】:

我有一个简单的元类,它将以“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 NewTestClass(TestClass, PropertyConvertMetaclass):
    pass

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

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

按照我的理解,我应该创建一个新类,将其命名为 PropertyConvert 并使 NewTestClass 继承自 It:

class PropertyConvert(metaclass=PropertyConvertMetaclass):
    pass

class NewTestClass(TestClass, PropertyConvert):
    pass

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

【问题讨论】:

  • 您是否希望元类在某个时候成为一个元类?因为从它继承只会使它成为一个奇怪的对象。您有时需要metaclass=... 才能实际使用它来代替type
  • new_attr 在您的元类中最终会丢弃所有非 get_* 属性,包括 dunders。你的意思是在某个地方放一个else 吗?此外,您不需要多余的ifs:任何以get_ 开头的东西显然都不是以__ 开头的。
  • 是的,我看到这很奇怪,但不幸的是,这是我的编程课程中的一项任务。
  • 你至少需要在循环结束时调用future_class_attr.update(new_attr)...
  • 即使你做了class NewTestClass(TestClass, metaclass=PropertyConvertMetaclass): 也无济于事。由于NewTestClass 没有在其主体中定义任何方法,因此元类没有可操作的内容。 TestClass 对象是在元类发挥作用之前创建的

标签: python inheritance metaclass


【解决方案1】:

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

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

这里有几个可能的解决方案。更简单但由于您的任务而无法实现的方法是执行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. 除了直接基类之外没有查找。
  2. 您不能在一个类中定义 getter,而在另一个类中定义 setter。

要解决第一个问题,您必须遍历类的 MRO。与@jsbueno suggests 一样,使用__init__ 而不是类前字典在完全构造的类上更容易做到这一点。我将通过在创建任何属性之前制作一个可用的 getter 和 setter 表来解决第二个问题。您还可以通过这样做使属性尊重 MRO。使用 __init__ 的唯一复杂之处在于,您必须在类上调用 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)

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

【讨论】:

  • 谢谢。不过,这似乎是某种方式(我稍微编辑了我的问题)。
  • 这不是“不可能的”,您只需要内省基类,而不是只查看当前类的命名空间。
  • @jsbueno。你的权利,不寻常但并非不可能。当我在深夜回答问题时会发生这种情况。
【解决方案2】:

我的问题的一个解决方案是在 PropertyConvertMetaclass 中解析父母的字典:

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)

【讨论】:

  • 我发布了一个非常相似的答案,但使用了清理过的代码(在移动设备上没有看到你的)。将更新以避免重复
  • 查看我的更新版本。它更加通用:您现在可以在不同的类中定义 getter、setter 和 deleter,并正确生成属性。
【解决方案3】:

那里没有什么是“不可能的”。 这个问题无论多么不寻常,都可以通过元类来解决。

您的方法很好-您遇到的问题是,当您查看“future_class_attr”(也称为类主体中的命名空间)时,它仅包含当前正在定义的类的方法和属性 /em> 。在您的示例中,NewTestClass 为空,“future_class_attr”也是如此。

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

在创建目标类之前正确执行此操作会很棘手 - 因为必须以所有超类的正确 mro(方法解析顺序)进行属性搜索 - 并且可能有很多角落案例。 (但请注意,这不是“不可能的”)

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

更好的是,编写元类__init__ 而不是它的__new__ 方法:您检索已经创建的新类,并且可以继续使用dir 对其进行内省并立即添加属性。 (不要忘记打电话给super().__init__(...),即使你的班级不需要它。)

另外,请注意,从 Python 3.6 开始,如果只在基类的 __init_subclass__ 方法中实现所需的逻辑,则完全无需元类即可获得相同的结果。

【讨论】:

  • 这并不一定很棘手,因为 MRO 由 super 正确处理。
  • 另外,你可以应用一个装饰器并使用dir,这实际上会更棘手
  • 我更新了我的答案,以展示一种使用super 非常普遍地做到这一点的方法,而不必担心 MRO。谢谢指点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-11-15
  • 2016-07-19
  • 2018-03-10
  • 1970-01-01
  • 1970-01-01
  • 2018-02-08
  • 1970-01-01
相关资源
最近更新 更多