The Metric Class Decorator

Sometimes, you will write a class that needs to compute distances. This means that it ought to have access to the metric space object and not just the points. A simple example would be a class for a metric ball like the following.

class MetricBall:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

    def __contains__(self, point):
        return self.center.dist(point) <= self.radius

This code will work just fine in many cases, but it will fail if the metric has been specified by a distance function that is not a method on the points. It will also bypass the cache.

One cumbersome solution would be to store the metric space in the ball object. This is highly redundant if there are many balls. A second solution would be to store the metric space as an attribute of the class. The problem with this solution is that it would only permit one kind of metric ball to exist at a time. A third solution would be to subclass MetricBall and then assign the metric as an attribute of the subclass. At first, this seems the most cumbersome to write, even if it resolves the main issues and corresponds precisely to what we want the class to mean, i.e., it is a metric ball in a particular metric. As this pattern is relatively standard, we provide a decorator that makes it trivial to produce subclasses this way. This is how you would write the same MetricBall class with the decorator.

@metric_class
class MetricBall:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

    def __contains__(self, point):
        dist = MetricBall.metric.dist
        return dist(self.center, point) <= self.radius

The only line of code that is different is that we are now accessing the metric via an attribute called metric that is defined on the class. To use this class, we would first define the type by combining a metric space and a metric ball as follows.

M = MetricSpace()
Ball = MetricBall(M)
ball = Ball((0,0), 1)

The point here is the decorator turns the MetricBall class into a function that returns a class. The MetricSpace is a parameter to this function.

If you define a new class this way, you may wish to assign the name of the class manually. Looking at the type of ball above, we would not see Ball, but instead MetricBall_M. If you want to also change the name, you can give a second parameter with the name when defining the Ball class as follows.

Ball = MetricBall(M, "Ball")