«Не могу создать экземпляр абстрактного класса ... с абстрактными методами» в классе, который не должен иметь никакого абстрактного метода - PullRequest
8 голосов
/ 11 ноября 2011

Возьмите следующий минимальный пример:

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.Это легче сделать, если я могу определить BarOverride после создания derived_type.(Если я не могу этого сделать, то почему?)

Ответы [ 4 ]

5 голосов
/ 11 ноября 2011

Поскольку в документах так сказано:

Динамическое добавление абстрактных методов в класс или попытка изменить статус абстракции метода или класса после его создания, не поддерживаются Abstractmethod () влияет только на подклассы выведено с использованием регулярного наследования; «Виртуальные подклассы» зарегистрированы на метод ABC register () не влияют.

Метакласс вызывается только когда класс определен. Когда abstractmethod помечает класс как абстрактный, этот статус не изменится позже.

3 голосов
/ 05 февраля 2015

Я знаю, что эта тема действительно старая, но ... Это действительно хороший вопрос.

Это не работает, потому что abc может проверять абстрактные методы только во время определения типов, то есть, когда работает type('Derived', (FooClass,), {}). Любой setattr, сделанный после этого, не доступен из abc

Итак, setattr не будет работать, buuut ... Ваша проблема обращения к имени класса, который не был ранее объявлен или определен, выглядит решаемой:

Я написал небольшой метакласс, который позволяет использовать заполнитель «clazz» для доступа к любому классу, который в конечном итоге получит метод, который вы пишете, вне определения класса.

Таким образом, вы больше не получите TypeError от abc, так как теперь вы можете определить свой метод ДО установления типа, а затем передать его типу в аргументе dict. Тогда abc увидит это как правильное переопределение метода.

Aaand, с новым метаклассом вы можете ссылаться на объект класса во время этого метода. И это супер, потому что теперь вы можете использовать супер! = Р Я думаю, вы тоже волновались об этом ...

Взгляните:

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 голосов
/ 11 ноября 2011

Йохен прав;абстрактные методы устанавливаются при создании класса и не изменяются только потому, что вы переназначаете атрибут.

Вы можете вручную удалить его из списка абстрактных методов, выполнив

DerivedType.__abstractmethods__ = frozenset()

или

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

, а также setattr, поэтому он все еще не думает, что FooMethod является абстрактным.

2 голосов
/ 11 ноября 2011

Ну, если вы должны сделать это таким образом, тогда вы можете просто передать фиктивный диктов {'FooMethod':None} в качестве третьего аргумента для типа. Это позволяет derived_type удовлетворить требование ABCMeta о переопределении всех абстрактных методов. Позже вы можете поставить реальное FooMethod:

def main():
  derived_type = type('Derived', (FooClass,), {'FooMethod':None})
  def BarOverride(self):
    print 'Hello, world!'
  setattr(derived_type, 'FooMethod', BarOverride)
  instance = derived_type()
...