P-260 - Alternative(s)
Python Asteroids+Invaders on GitHub
There are surely ways to provide my
ignore
feature without using metaclasses. I can think of one. Maybe it will help me think of another.
I am pretty comfortable with the metaclass implementation of the ignore
feature, but it is rather deep in the bag of tricks. In addition, the fact that we needed to inherit, not from type
but from ABCMeta
means that it could conceivably fail in some strange way based on interaction between our class and ABCMeta. Therefore, it makes sense to look for a better alternative.
Superclass
One possibility has already been discussed. Along the way to the metaclass, I produced this test and code:
class Ignorables():
def __init__(self, ignores=[]):
for ign in ignores:
if not ign in self.__class__.__dict__:
setattr(self, ign, self.none)
def none(self, o, f):
pass
def interact_with_bar(self, _object, _fleets):
raise NotImplementedError
def interact_with_baz(self, _object, _fleets):
raise NotImplementedError
def interact_with_qux(self, _object, _fleets):
raise NotImplementedError
class Foo(Ignorables):
ignore_list = ["interact_with_bar", "interact_with_qux"]
def __init__(self):
super().__init__(self.ignore_list)
def abc(self, value):
return 40 + value
def interact_with_qux(self, thing, fleets):
fleets.append(42)
class TestIgnoring:
def test_exists(self):
fleets = [36]
assert 36 in fleets
foo = Foo()
assert foo.abc(2) == 42
foo.interact_with_bar(33, [])
with pytest.raises(NotImplementedError):
foo.interact_with_baz(33, [])
fleets = []
foo.interact_with_qux(33, fleets)
assert fleets, "should have contents unless it was erroneously ignored"
assert fleets[0] == 42
Here, we just have a standard superclass, Ignorables, with an __init__
method that takes a list of method names and sets them up to return silently in the object’s class dictionary. It works, with the disadvantage that the changes to the class dictionary happen every time an instance is created, not just when the class is created.
Covering Class
There is another way that came to mind during my studies of metaclasses last night. It is “well known” that this code:
class Mumble(Super1, Super2):
Is just syntactic sugar for
Mumble = type("Mumble", (Super1, Super2), {})
The top class in Python is type
, and it is used to create any other classes. The rules for the dictionary there at the end are that whatever is in that dictionary will be used to initialize the class dictionary of the newly created class. (I believe that some processing goes on, where they convert that dictionary to a more performant object.)
So if, just if, we were to provide our dictionary of null methods to type()
, we should get the same effect as with our metaclass, and it would be much less weird. But there’s a problem: if we create a class that way, we never get “inside” the definition and we can’t easily create its methods. Oh, we can do it, but it will be strange. Instead, what if we used type
to produce a class that a) inherits from our desired superclass, and b) overrides some required methods with our trick, and c) can be used by our real class as its parent.
Essentially, we’ll insert our ignoring class in between InvadersFlyer and our desired class.
Let’s try it.
ignore_dict = {"bar": lambda *args: None, "baz": lambda *args: None}
CoveringClass = type("FooViaType", (Base,), ignore_dict)
class FooCovered(CoveringClass):
def foo(self):
return "foo"
def baz(self):
return "baz"
class TestIgnoringMetaclass:
def test_using_covering(self):
foo = FooCovered()
assert foo.foo() == "foo"
assert foo.bar() is None
assert foo.baz() == "baz"
with pytest.raises(NotImplementedError):
foo.qux()
This works perfectly. I’m not surprised: I thought it would. But I am pleased.
Covering Class via Function
Let’s see if we can package the creation of the helper class in a function, to make things a bit more neat.
def ignore_these(klass, bases, names):
dict = {name: lambda *args: None for name in names}
cover = type("cover", bases, dict)
bases = list(klass.__bases__)
bases.insert(0, cover)
klass.__bases__ = tuple(bases)
class FooFunction(Base):
def foo(self):
return "foo"
def baz(self):
return "baz"
ignore_these(FooFunction, (Base,), ["bar", "baz"])
class TestIgnoringMetaclass:
def test_covering_function(self):
foo = FooFunction()
assert foo.foo() == "foo"
assert foo.bar() is None
assert foo.baz() == "baz"
with pytest.raises(NotImplementedError):
foo.qux()
In this one, we define our class, and then, as part of that definition, call ignore_these
on it. The ignore_these
function creates our starting dictionary, creates a class named cover
, inserts that class at the beginning of the new class’s superclasses, and sets our class’s base classes to that.
I wonder if we could just set our class’s superclass to cover
. It seems to me that we could. Yes. This works:
def ignore_these(klass, bases, names):
dict = {name: lambda *args: None for name in names}
cover = type("cover", bases, dict)
klass.__bases__ = (cover,)
Makes sense, because cover
inherits whatever bases we want, we just want it in the front of the inheritance order.
I want to do this one more way. I would like to do a decorator. That deserves a write up of its own, in the next article. Let’s sum up.
Summary
We’ve seen these ways of providing pass
methods in a given class, by providing a declared list of names:
- Metaclass
-
This one is deep in the bag of tricks, probably deeper than we should rightly go. I do think that it is correct and reasonably safe. The Bumper class in the “production” code currently uses it. Weirdly scary.
- Superclass
-
As described above, we were able to create a superclass to contain the overrides. That scheme requires the class developer to call
super().__init__
, and would probably need at least some work to make it deal properly with the Flyer inheritance hierarchy. A step forward, but it feels a bit messy to me. - Covering Class
-
Then, also above, we created a “covering class”, a new class inheriting from whatever hierarchy is needed, implementing the overrides we want for our real class. That works nicely, with the disadvantage that the developer needs to write a covering class for each real class that we need. But it was another step forward.
- Covering Class via Function
-
Finally, just above, we wrapped creation of the covering class in a function. This worked to no one’s surprise, but it does require the developer to call the covering function at the top level after defining the class. Again a step forward, but still awkward.
What we see here is a learning progression. The metaclass was a long learning process of its own, and the remaining three schemes here each took a bit of cut and try as well, more than I was willing to subject my readers to. Each one has its merits, and I feel that the covering class via function is nearly as good as the metaclass, and easier to understand.
But I thought that perhaps a decorator could be better still. We’ll discuss that next time.
See you then!