“无法实例化包含抽象方法的抽象类”但该类不应有任何抽象方法

14 投票
4 回答
20923 浏览
提问于 2025-04-17 06:04

来看一个简单的例子:

import abc

class FooClass(object):
  __metaclass__ = abc.ABCMeta

  @abc.abstractmethod
  def FooMethod(self):
    raise NotImplementedError()


def main():
  derived_type = type('Derived', (FooClass,), {})

  def BarOverride(self):
    print 'Hello, world!'
  derived_type.FooMethod = BarOverride

  instance = derived_type()

运行 main() 会得到:

TypeError: Can't instantiate abstract class Derived with abstract methods FooMethod

(异常发生在 instance = derived_type() 这一行。)

但是 FooMethod 不应该是抽象的:我已经用 BarOverride 重写了它。那么,为什么会出现异常呢?

声明:是的,我可以使用明确的 class 语法,达到完全相同的效果。(而且更好的是,我可以让它正常工作!)但这是一个最小的测试案例,而更大的例子是动态创建类。:-) 我很好奇为什么这个不行。

编辑:为了避免其他明显的无效回答:我不想把 BarOverride 作为第三个参数传给 type:在真实的例子中,BarOverride 需要绑定到 derived_type。如果我能在创建 derived_type 之后定义 BarOverride,这样做会更简单。(如果我不能这样做,那为什么呢?)

4 个回答

3

我知道这个话题很老旧,但这个问题真的很有意思。

之所以不工作,是因为abc只能在创建类型的时候检查抽象方法,也就是说,当你运行type('Derived', (FooClass,), {})的时候。之后再用setattr设置的内容,abc就无法访问了。

所以,setattr是没用的,不过……

你提到的关于如何处理一个之前没有声明或定义的类名的问题,看起来是可以解决的:

我写了一个小的元类,让你可以用一个占位符“clazz”来访问任何最终会拥有你在类定义外编写的方法的类。

这样,你就不会再因为abc而出现TypeError了,因为你现在可以在创建类型之前定义你的方法,然后把它传递给type的字典参数。这样abc就会把它视为一个合适的方法重写。

而且,使用这个新的元类,你可以在那个方法中引用类对象。这太棒了,因为现在你可以使用super了!=P 我猜你也在担心这个……

看看这个:

import abc
import inspect

clazz = type('clazz', (object,), {})()

def clazzRef(func_obj):
    func_obj.__hasclazzref__ = True
    return func_obj

class MetaClazzRef(type):
    """Makes the clazz placeholder work.

    Checks which of your functions or methods use the decorator clazzRef
    and swaps its global reference so that "clazz" resolves to the
    desired class, that is, the one where the method is set or defined.

    """
    methods = {}
    def __new__(mcs, name, bases, dict):
        ret = super(MetaClazzRef, mcs).__new__(mcs, name, bases, dict)
        for (k,f) in dict.items():
            if getattr(f, '__hasclazzref__', False):
                if inspect.ismethod(f):
                    f = f.im_func
                if inspect.isfunction(f):
                    for (var,value) in f.func_globals.items():
                        if value is clazz:
                            f.func_globals[var] = ret
        return ret

class MetaMix(abc.ABCMeta, MetaClazzRef):
    pass

class FooClass(object):
    __metaclass__ = MetaMix
    @abc.abstractmethod
    def FooMethod(self):
        print 'Ooops...'
        #raise NotImplementedError()


def main():
    @clazzRef
    def BarOverride(self):
        print "Hello, world! I'm a %s but this method is from class %s!" % (type(self), clazz)
        super(clazz, self).FooMethod() # Now I have SUPER!!!

    derived_type = type('Derived', (FooClass,), {'FooMethod': BarOverride})

    instance = derived_type()
    instance.FooMethod()

    class derivedDerived(derived_type):
        def FooMethod(self):
            print 'I inherit from derived.'
            super(derivedDerived,self).FooMethod()

    instance = derivedDerived()
    instance.FooMethod()

main()

输出是:

Hello, world! I'm a <class 'clazz.Derived'> but this method is from class <class 'clazz.Derived'>!
Ooops...
I inherit from derived.
Hello, world! I'm a <class 'clazz.derivedDerived'> but this method is from class <class 'clazz.Derived'>!
Ooops...
3

Jochen说得对;抽象方法是在类创建的时候就确定下来的,不会因为你重新给某个属性赋值而改变。

你可以通过下面的方式手动把它从抽象方法的列表中移除:

DerivedType.__abstractmethods__ = frozenset()

或者

DerivedType.__abstractmethods__ = frozenset(
        elem for elem in DerivedType.__abstractmethods__ if elem != 'FooMethod')

还有使用 setattr 这个方法,这样它就不会再认为 FooMethod 是抽象的了。

6

因为文档是这么说的:

动态地给一个类添加抽象方法,或者在类或方法创建后试图修改它们的抽象状态,这些都是不被支持的。abstractmethod() 只会影响通过正常继承创建的子类;通过 ABC 的 register() 方法注册的“虚拟子类”不会受到影响。

元类只在定义类的时候被调用。当 abstractmethod 把一个类标记为抽象类后,这个状态是不会再改变的。

撰写回答