子类避免父类的元类

Posted

技术标签:

【中文标题】子类避免父类的元类【英文标题】:Subclass avoiding parent's metaclass 【发布时间】:2017-10-09 15:01:33 【问题描述】:

假设我有一个第三方库,其中元类需要我实现一些东西。但我想要一个没有的中间“抽象”子类。我该怎么做?

认为这是第三方库所具有的一个非常小的示例:

class ServingMeta(type):
    def __new__(cls, name, bases, classdict):
        if any(isinstance(b, ServingMeta) for b in bases):
            if "name" not in classdict:
                # Actual code fails for a different reason,
                # but the logic is the same.
                raise TypeError(f"Class 'name' has no 'name' attribute")
        return super().__new__(cls, name, bases, classdict)

class Serving(object, metaclass=ServingMeta):
    def shout_name(self):
        return self.name.upper()

我无法修改上面的代码。这是一个外部依赖项(我不想分叉它)。

代码就是这样使用的:

class Spam(Serving):
    name = "SPAM"

spam = Spam()
print(spam.shout_name())

但是,我碰巧有很多垃圾邮件,我想介绍一个带有常用辅助方法的基类。像这样的:

class Spam(Serving):
    def thrice(self):
        return " ".join([self.shout_name()] * 3)

class LovelySpam(Spam):
    name = "lovely spam"

class WonderfulSpam(Spam):
    name = "wonderful spam"

显然,这不起作用,并且由于预期良好的TypeError: Class 'SpamBase' has no 'name' attribute declared 而失败。第三方库是否会有一个没有元类的 SpamBase 类,我本可以将其子类化 - 但这次没有这样的运气(我已经提到了给库作者带来的不便)。

我可以把它做成一个mixin:

class SpamMixin(object):
    def thrice(self):
        return " ".join([self.shout_name()] * 3)

class LovelySpam(SpamMixin, Serving):
    name = "lovely spam"

class WonderfulSpam(SpamMixin, Serving):
    name = "wonderful spam"

但是,这让我和我的 IDE 有点畏缩,因为到处重复 SpamMixin 很快就会变得很麻烦,而且因为 object 没有 shout_name 属性(而且我不想让分析工具静音) .简而言之,我就是不喜欢这种做法。

我还能做什么?

有没有办法获得Serving 的无元类版本?我想到了这样的事情:

ServingBase = remove_metaclass(Serving)

class Spam(ServingBase, metaclass=ServingMeta):
    ...

但不知道如何实际实现 remove_metaclass 以及在任何可能的情况下(当然,它必须是可行的,需要一些内省,但它可能需要比我能施展更多的奥术魔法)。

也欢迎任何其他建议。基本上,我想让我的代码 DRY(一个基类来统治它们),并让我的 linter/代码分析图标全是绿色的。

【问题讨论】:

我认为你可以尝试使用 type 重建类型对象:ServingBase = type('ServingBase', Serving.__bases__, dict(vars(Serving))) 我认为 mixin 会使代码更简洁 @vaultah 是的,我没有想过这个。看起来是一个选择。您可以将此作为答案发布吗?我会等待其他建议,但如果没有更好的建议,我希望能够将您的建议标记为已接受的答案。至于清洁度——我个人觉得,如果用自我描述的名称(ServingBaseremove_metaclass)合理地装饰东西并得到很好的评论,那么应该没问题。 【参考方案1】:

mixin 方法是正确的方法。如果您的 IDE “畏缩”,这对该工具有影响 - 只需禁用一些在为 Python 等动态语言编码时显然是不正确的调整的“功能”。

这甚至与动态创建事物无关,它只是多重继承,该语言自始至终都支持这一点。多重继承的主要用途之一就是能够创建您需要的 mixin。

另一个基于继承的解决方法是让你的层次结构更深一层,并在你想出你的 mixin 方法之后引入元类:

class Mixin(object):
    def mimixin(self): ...

class SpamBase(Mixin, metaclass=ServingMeta):
    name = "stub"

或者只是在中间子类中添加 mixin:

class Base(metaclass=Serving Meta):
     name = "stub"
class MixedBase(Mixin, Base):
     name = "stub"
class MyLovingSpam(MixedBase):
     name = "MyLovingSpam"

如果您不想在每个类中都重复使用 mixin=-base 名称,那么您可以这样做。

仅仅为了后期混入而“删除”一个元类太过分了。真的。破碎的。正如@vaultah 在另一个答案中提到的那样,这样做的方法是动态地重新创建类,但是在中间类中这样做是你不应该做的事情。这样做是为了取悦 IDE 是你不应该做两次的事情:搞乱元类已经够难的了。删除语言自然放置的继承/类创建的东西是令人讨厌的(参见这个答案:How to make a class attribute exclusive to the super class)。另一方面,mixin 和多重继承是很自然的。

你还在吗?我告诉过你不要这样做:

现在,关于您的问题 - 与其在中间类中“抑制元类”,不如继承您在那里的元类并更改其行为 - 这样它就不会检查特别标记的约束classes - 创建一个供您使用的属性,例如 _skip_checking

class MyMeta(ServingMeta):
    def __new__(metacls, name, bases, namespace):
         if namespace.get("_skip_checking", False):
              # hardcode call to "type" metaclass:
              del namespace["_skip_checking"]
              cls = type.__new__(metacls, name, bases, namespace)
         else:
              cls = super().__new__(metacls, name, bases, namespace) 
         return cls
     # repeat for __init__ if needed.  


class Base(metaclass=MyMeta):
     _skip_checking = True
     # define mixin methods

class LoveSpam(Base):
    name = "LoveSpam"

【讨论】:

非常感谢!你已经说服了我,我会坚持使用 mixins(并要求库作者考虑拥有一个单独的基类)。我认为 IDE (PyCharm) 仍然正确地警告我可能会使用存在不确定的方法而绊倒 - 但这是需要禁用的特定警告。避免元类的技巧在其他一些情况下可能很有用,我想子类化它可能是处理精神错乱的最明智的方法。 :) 因此,在这两种情况下,我都会将您的答案标记为已接受。再次感谢您。【参考方案2】:

实际上没有直接的方法可以从 Python 类中删除元类,因为元类创建了该类。您可以尝试使用不同的元类重新创建该类,该元类没有不需要的行为。例如,您可以使用type(默认元类)。

In [6]: class Serving(metaclass=ServingMeta):
   ...:     def shout_name(self):
   ...:         return self.name.upper()
   ...: 

In [7]: ServingBase = type('ServingBase', Serving.__bases__, dict(vars(Serving)))

基本上这需要__bases__ 元组和Serving 类的命名空间,并使用它们创建一个新类ServingBase。注:这意味着ServingBase 将接收来自Serving 的所有基础和方法/属性,其中一些可能是由ServingMeta 添加的。

【讨论】:

以上是关于子类避免父类的元类的主要内容,如果未能解决你的问题,请参考以下文章

TypeError:元类冲突:派生类的元类必须是其所有基类的元类的(非严格)子类

避免使用元类继承生成的类属性

如何在我的元类中获取类的父类?

如何能避免在调用子类对象的虚函数时调用父类的虚函数呢?

为啥具有对象基础的元类会引发元类冲突?

抽象类,抽象函数