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!