
Hi all,
I recently ran across an interesting (mis?)-feature at least in CPython that I couldn't find any specific justification for or against. The issue is that abstract *types*, while technically possible, don't behave as abstract classes.
To exemplify, say I wanted to create a metaclass which itself has ABCMeta as a metaclass, and which has some abstract classmethod defined:
import abc class Meta(type, metaclass=abc.ABCMeta):
... @abc.abstractmethod ... def foo(cls): pass ...
Now for all intents and purposes Meta *is* an abstract type:
import inspect inspect.isabstract(Meta)
True
Meta.__abstractmethods__
frozenset({'foo'})
However, nothing prevents Meta from being used as a metaclass for another class, despite it being "abstract":
class A(metaclass=Meta): pass
...
A
<class '__main__.A'>
This is simply because the check for the Py_TYPFLAGS_IS_ABSTRACT flag is implemented in object_new, which is overridden by type_new for type subclasses. type_new does not perform this check.
I'm perfectly fine if this is dismissed as too abstract or too academic to be useful, but I will mention that this came up in a real use case. The use case is in a hierarchy of metaclasses involved in a syntactic-sugary class factory framework involving creation of new classes via operators.
So I just wonder if this is a bug that should be fixed, or at the very least a feature request. The IS_ABSTRACT flag check is cheap and easy to add to type_new, in principle.
In the meantime a workaround, which doesn't seem too terrible, is simply to define something I called AbstractableType:
class AbstractableType(type):
... def __new__(mcls, name, bases, members): ... if inspect.isabstract(mcls): ... raise TypeError( ... "Can't instantiate abstract type {0} with " ... "abstract methods {1}".format( ... mcls.__name__, ', '.join(sorted(mcls.__abstractmethods__)))) ... return super(AbstractableType, mcls).__new__(mcls, name, bases, members) ...
Now create an abstract metaclass with (meta-)metaclass abc.ABCMeta:
class AbstractMeta(AbstractableType, metaclass=abc.ABCMeta):
... @abc.abstractmethod ... def foo(cls): pass ...
Creating a class with metaclass AbstractMeta fails as it should:
class A(metaclass=AbstractMeta): pass
... Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in __new__ TypeError: Can't instantiate abstract type AbstractMeta with abstract methods foo
However, AbstractMeta can be subclassed with a concrete implementation:
class ConcreteMeta(AbstractMeta):
... def foo(cls): print("Concrete method") ...
class A(metaclass=ConcreteMeta): pass
...
A.foo()
Concrete method
Thanks,
Erik

On 09/10/2014 01:59 PM, Erik Bray wrote:
--> import abc --> class Meta(type, metaclass=abc.ABCMeta): ... @abc.abstractmethod ... def foo(cls): pass ... --> class A(metaclass=Meta): pass ... --> A <class '__main__.A'>
I think this is a bug. However, if the class were:
--> class A(metaclass=Meta): ... def foo(self): ... pass ...
Then this should succeed, and I don't think your Abstractable type allows it.
-- ~Ethan~

On Wed, Sep 10, 2014 at 5:41 PM, Ethan Furman ethan@stoneleaf.us wrote:
On 09/10/2014 01:59 PM, Erik Bray wrote:
--> import abc --> class Meta(type, metaclass=abc.ABCMeta): ... @abc.abstractmethod ... def foo(cls): pass ... --> class A(metaclass=Meta): pass ... --> A <class '__main__.A'>
I think this is a bug. However, if the class were:
--> class A(metaclass=Meta): ... def foo(self): ... pass ...
Then this should succeed, and I don't think your Abstractable type allows it.
I don't necessarily agree that that should succeed. The use of an abstract meta-class is basically requiring there to be a concrete *classmethod* of the name "foo", (an unbound instancemethod wouldn't suffice). What maybe *should* work, but doesn't with this implementation is:
class A(metaclass=Meta): @classmethod def foo(cls): pass
That could be fixed reasonably easily by extending the AbstractableType.__new__ to check for classmethods in the new class's members, a la ABCMeta.__new__. I'm not sure how that would be best handled in CPython though.
Alternatively it could just be required that an abstract metaclass simply can't be used as a metaclass unless a concrete subclass is made. But using @classmethod to override abstract class methods does make some intuitive sense.
Erik

On 09/10/2014 03:15 PM, Erik Bray wrote:
On Wed, Sep 10, 2014 at 5:41 PM, Ethan Furman ethan@stoneleaf.us wrote:
On 09/10/2014 01:59 PM, Erik Bray wrote:
--> import abc --> class Meta(type, metaclass=abc.ABCMeta): ... @abc.abstractmethod ... def foo(cls): pass ... --> class A(metaclass=Meta): pass ... --> A <class '__main__.A'>
I think this is a bug. However, if the class were:
--> class A(metaclass=Meta): ... def foo(self): ... pass ...
Then this should succeed, and I don't think your Abstractable type allows it.
I don't necessarily agree that that should succeed. The use of an abstract meta-class is basically requiring there to be a concrete *classmethod* of the name "foo", (an unbound instancemethod wouldn't suffice).
If that is what you want you should use `abstractclassmethod`.
What maybe *should* work, but doesn't with this implementation is:
class A(metaclass=Meta): @classmethod def foo(cls): pass
Well, take out the 'maybe' and I'm in agreement. ;)
Alternatively it could just be required that an abstract metaclass simply can't be used as a metaclass unless a concrete subclass is made.
-1
But using @classmethod to override abstract class methods does make some intuitive sense.
+1
-- ~Ethan~

On Wed, Sep 10, 2014 at 6:29 PM, Ethan Furman ethan@stoneleaf.us wrote:
On 09/10/2014 03:15 PM, Erik Bray wrote:
On Wed, Sep 10, 2014 at 5:41 PM, Ethan Furman ethan@stoneleaf.us wrote:
On 09/10/2014 01:59 PM, Erik Bray wrote:
--> import abc --> class Meta(type, metaclass=abc.ABCMeta): ... @abc.abstractmethod ... def foo(cls): pass ... --> class A(metaclass=Meta): pass ... --> A <class '__main__.A'>
I think this is a bug. However, if the class were:
--> class A(metaclass=Meta): ... def foo(self): ... pass ...
Then this should succeed, and I don't think your Abstractable type allows it.
I don't necessarily agree that that should succeed. The use of an abstract meta-class is basically requiring there to be a concrete *classmethod* of the name "foo", (an unbound instancemethod wouldn't suffice).
If that is what you want you should use `abstractclassmethod`.
That would be fine if the classmethods were being defined in a normal class. And with a little rearchitecting maybe that would be a simpler workaround for my own issues. But I still think this should work properly for methods belonging to a metaclass.
For that matter, I feel like this is a bug too:
class Foo(metaclass=abc.ABCMeta):
... @classmethod ... @abc.abstractmethod ... def my_classmethod(cls): pass ...
class FooSub(Foo):
... def my_classmethod(self): ... pass # Not actually a classmethod ...
FooSub()
<__main__.FooSub object at 0x7f5d8b8a6dd8>
Basically, FooSub does not really implement the interface expected by the Foo ABC.
This is especially deceptive considering that the way classmethod.__get__ works gives the impression (to the unwary) that the classmethod is actually a method defined on the class's metaclass:
Foo.my_classmethod
<bound method ABCMeta.my_classmethod of <class '__main__.Foo'>>
What maybe *should* work, but doesn't with this implementation is:
class A(metaclass=Meta): @classmethod def foo(cls): pass
Well, take out the 'maybe' and I'm in agreement. ;)
Alternatively it could just be required that an abstract metaclass simply can't be used as a metaclass unless a concrete subclass is made.
-1
But using @classmethod to override abstract class methods does make some intuitive sense.
+1
That's fine. I think that can be done.
Thanks, Erik

On 09/10/2014 04:00 PM, Erik Bray wrote:
On Wed, Sep 10, 2014 at 6:29 PM, Ethan Furman wrote:
If that is what you want you should use `abstractclassmethod`.
That would be fine if the classmethods were being defined in a normal class. And with a little rearchitecting maybe that would be a simpler workaround for my own issues. But I still think this should work properly for methods belonging to a metaclass.
Ah, right -- any method defined on a metaclass is a defacto class method.
For that matter, I feel like this is a bug too:
--> class Foo(metaclass=abc.ABCMeta): ... @classmethod ... @abc.abstractmethod ... def my_classmethod(cls): pass ... --> class FooSub(Foo): ... def my_classmethod(self): ... pass # Not actually a classmethod ... -> FooSub()
You'll have to search the docs, bug-tracker, and mailing lists for that one -- I do seem to remember reading about it, but don't recall where.
-- ~Ethan~
participants (2)
-
Erik Bray
-
Ethan Furman