Модуль Python abc: расширение как абстрактного базового класса, так и класса, производного от исключения, приводит к неожиданному поведению

Расширение как абстрактного базового класса, так и класса, производного от «объекта», работает так, как и следовало ожидать: если вы не реализовали все абстрактные методы и свойства, вы получите ошибку.

Как ни странно, замена класса, производного от объекта, классом, расширяющим «Exception», позволяет создавать экземпляры классов, которые не реализуют все требуемые абстрактные методы и свойства.

Например:

import abc

# The superclasses
class myABC( object ):
    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def foo(self):
        pass

class myCustomException( Exception ):
    pass

class myObjectDerivedClass( object ):
    pass

# Mix them in different ways
class myConcreteClass_1(myCustomException, myABC):
    pass

class myConcreteClass_2(myObjectDerivedClass, myABC):
    pass

# Get surprising results
if __name__=='__main__':
    a = myConcreteClass_1()
    print "First instantiation done. We shouldn't get this far, but we do."
    b = myConcreteClass_2()
    print "Second instantiation done. We never reach here, which is good."

... дает ...

First instantiation done. We shouldn't get this far, but we do.
Traceback (most recent call last):
  File "C:/Users/grahamf/PycharmProjects/mss/Modules/mssdevice/sutter/sutter/test.py", line 28, in <module>
    b = myConcreteClass_2()
TypeError: Can't instantiate abstract class myConcreteClass_2 with abstract methods foo

Я знаю, что «Exception» и, следовательно, «myCustomException» не имеют атрибута «foo», так почему же мне сходит с рук создание экземпляра «myCustomException»?

РЕДАКТИРОВАТЬ: Для справки, это хакерский обходной путь, которым я в конечном итоге воспользовался. Не совсем эквивалентно, но подходит для моих целей.

# "abstract" base class
class MyBaseClass( Exception ):
    def __init__(self):
        if not hasattr(self, 'foo'):
            raise NotImplementedError("Please implement abstract property foo")


class MyConcreteClass( MyBaseClass ):
    pass

if __name__=='__main__':
    a = MyConcreteClass()
    print "We never reach here, which is good."

person Graham F.    schedule 17.07.2014    source источник


Ответы (2)


Похоже, это связано с тем, что метод __new__ для BaseException не заботится об абстрактных методах / свойствах.

Когда вы пытаетесь создать экземпляр myConcreteClass_1, он вызывает __new__ из класса Exception. Когда нужно создать экземпляр myConcreteClass_2, он вызывает __new__ из object:

>>> what.myConcreteClass_1.__new__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exceptions.Exception.__new__(): not enough arguments
>>> what.myConcreteClass_2.__new__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object.__new__(): not enough arguments

Класс Exception не предоставляет метод __new__, но является родительским, BaseException, делает:

static PyObject *
BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyBaseExceptionObject *self;

    self = (PyBaseExceptionObject *)type->tp_alloc(type, 0);
    if (!self)
        return NULL;
    /* the dict is created on the fly in PyObject_GenericSetAttr */
    self->dict = NULL;
    self->traceback = self->cause = self->context = NULL;
    self->suppress_context = 0;

    if (args) {
        self->args = args;
        Py_INCREF(args);
        return (PyObject *)self;
    }

    self->args = PyTuple_New(0);
    if (!self->args) {
        Py_DECREF(self);
        return NULL;
    }

    return (PyObject *)self;
}

Сравните это с реализацией __new__ для object:

static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    if (excess_args(args, kwds) &&
        (type->tp_init == object_init || type->tp_new != object_new)) {
        PyErr_SetString(PyExc_TypeError, "object() takes no parameters");
        return NULL;
    }

    if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
        PyObject *abstract_methods = NULL;
        PyObject *builtins;
        PyObject *sorted;
        PyObject *sorted_methods = NULL;
        PyObject *joined = NULL;
        PyObject *comma;
        _Py_static_string(comma_id, ", ");
        _Py_IDENTIFIER(sorted);

        /* Compute ", ".join(sorted(type.__abstractmethods__))
           into joined. */
        abstract_methods = type_abstractmethods(type, NULL);
        if (abstract_methods == NULL)
            goto error;
        builtins = PyEval_GetBuiltins();
        if (builtins == NULL)
            goto error;
        sorted = _PyDict_GetItemId(builtins, &PyId_sorted);
        if (sorted == NULL)
            goto error;
        sorted_methods = PyObject_CallFunctionObjArgs(sorted,
                                                      abstract_methods,
                                                      NULL);
        if (sorted_methods == NULL)
            goto error;
        comma = _PyUnicode_FromId(&comma_id);
        if (comma == NULL)
            goto error;
        joined = PyUnicode_Join(comma, sorted_methods);
        if (joined == NULL)
            goto error;

        PyErr_Format(PyExc_TypeError,
                     "Can't instantiate abstract class %s "
                     "with abstract methods %U",
                     type->tp_name,
                     joined);
    error:
        Py_XDECREF(joined);
        Py_XDECREF(sorted_methods);
        Py_XDECREF(abstract_methods);
        return NULL;
    }
    return type->tp_alloc(type, 0);
}

Как видите, object.__new__ имеет код для выдачи ошибки, когда есть абстрактные методы, которые не переопределяются, но BaseException.__new__ этого не делает.

person dano    schedule 17.07.2014
comment
Спасибо! Это интересно. Я не могу придумать обходной путь для того, что я пытаюсь сделать, что не кажется хакерским. - person Graham F.; 17.07.2014
comment
Связанные темы для людей с этой проблемой: здесь и здесь - person Graham F.; 17.07.2014

Ответ Дано точен, но обходной путь отсутствует. Вы можете воспроизвести объектный код своим собственным __new__ методом:

import abc, traceback

# The superclasses
class MyABC(abc.ABC):

    @property
    @abc.abstractmethod
    def foo(self):
        pass

class MyCustomException( Exception ):
    pass

class MyObjectDerivedClass( object ):
    pass

# Mix them in different ways
class MyConcreteClass_1(MyCustomException, MyABC):

    def __new__(cls, *args, **kwargs):
        ''' Same abstract checks than in object.__new__ '''
        res = super().__new__(cls, *args, **kwargs)
        if cls.__abstractmethods__:
            raise TypeError(f"Can't instantiate abstract class {cls.__name__} with abstract methods {','.join(sorted(cls.__abstractmethods__))}")
        return res

class MyConcreteClass_2(MyObjectDerivedClass, MyABC):
    pass

# No longer get surprising results
if __name__=='__main__':
    try:
        a = MyConcreteClass_1()
    except TypeError:
        traceback.print_exc()
    try:
        b = MyConcreteClass_2()
    except TypeError:
        traceback.print_exc()

Что дает два ожидаемых исключения.

person FLemaitre    schedule 15.11.2019