Exploiting Python metaclasses to forbid subclassing and where it fails
Suppose, entirely hypothetically and purely for illustration, that you want to create a class that cannot be subclassed. Python is normally a very permissive language, but as I mentioned in passing yesterday you can abuse some metaclass features to do this. Well, to more or less do this; as it happens there is a way around it because Python allows callables to be specified as your metaclass.
While there are complex approaches with a single metaclass that
has a sophisticated __new__
, it's easier to illustrate the
whole idea like this (in Python 3 for once):
class Block(type): def __new__(meta, cname, bases, cdict): raise TypeError("cannot subclass") def metacls(cname, bases, cdict): return type.__new__(Block, cname, bases, cdict) class A(metaclass=metacls): pass
This works basically by cheating; our callable metaclass gives the
newly created class a different metaclass that blocks the creation
of further classes in its __new__
. So let's try to get tricky
to get around this. Our first attempt is to give our new subclass
a different metaclass that doesn't block class creation:
class MNothing(type): pass class B(A, metaclass=MNothing): pass
This will fail:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
But wait, we don't actually need a new class here. We just need something that allows class creation but gives the new class the metaclass it needs in order to not have a metaclass conflict. As it happens we already have that:
class B(A, metaclass=metacls): pass
(note that if we didn't have metacls()
, creating it is trivial.
type(A)
will give us the metaclass class we need to shim in.)
This not only works, this is the simple and more or less ultimate
bypass for anything that tries to block __new__
because using
a callable as the metaclass splits apart what happens to a class
as it's being created from the class's metaclass class. The new
class's official post-creation metaclass gets no chance to intervene
during creation; it just winds up being the metaclass of a new class
by fiat, with nothing to say about it. Since the metaclass's
__init__
(if any) doesn't get called here, the first our Block
metaclass can possibly know about the new class is when the new
class is used for something (attribute lookup, instance creation, or
subclassing).
All of which goes to show that Python is permissive after all if you know the right tricks, even if it looks restrictive initially.
(Well, at least here.)
|
|