Here is a RegexDispatcher class that dispatches its subclass methods by regular expression.
Each dispatchable method is annotated with a regular expression e.g.
def plus(self, regex: r"\+", **kwargs):
...
In this case, the annotation is called 'regex' and its value is the regular expression to match on, '\+', which is the + sign. These annotated methods are put in subclasses, not in the base class.
When the dispatch(...) method is called on a string, the class finds the method with an annotation regular expression that matches the string and calls it. Here is the class:
import inspect
import re
class RegexMethod:
def __init__(self, method, annotation):
self.method = method
self.name = self.method.__name__
self.order = inspect.getsourcelines(self.method)[1] # The line in the source file
self.regex = self.method.__annotations__[annotation]
def match(self, s):
return re.match(self.regex, s)
# Make it callable
def __call__(self, *args, **kwargs):
return self.method(*args, **kwargs)
def __str__(self):
return str.format("Line: %s, method name: %s, regex: %s" % (self.order, self.name, self.regex))
class RegexDispatcher:
def __init__(self, annotation="regex"):
self.annotation = annotation
# Collect all the methods that have an annotation that matches self.annotation
# For example, methods that have the annotation "regex", which is the default
self.dispatchMethods = [RegexMethod(m[1], self.annotation) for m in
inspect.getmembers(self, predicate=inspect.ismethod) if
(self.annotation in m[1].__annotations__)]
# Be sure to process the dispatch methods in the order they appear in the class!
# This is because the order in which you test regexes is important.
# The most specific patterns must always be tested BEFORE more general ones
# otherwise they will never match.
self.dispatchMethods.sort(key=lambda m: m.order)
# Finds the FIRST match of s against a RegexMethod in dispatchMethods, calls the RegexMethod and returns
def dispatch(self, s, **kwargs):
for m in self.dispatchMethods:
if m.match(s):
return m(self.annotation, **kwargs)
return None
To use this class, subclass it to create a class with annotated methods. By way of example, here is a simple RPNCalculator that inherits from RegexDispatcher. The methods to be dispatched are (of course) the ones with the 'regex' annotation. The parent dispatch() method is invoked in call.
from RegexDispatcher import *
import math
class RPNCalculator(RegexDispatcher):
def __init__(self):
RegexDispatcher.__init__(self)
self.stack = []
def __str__(self):
return str(self.stack)
# Make RPNCalculator objects callable
def __call__(self, expression):
# Calculate the value of expression
for t in expression.split():
self.dispatch(t, token=t)
return self.top() # return the top of the stack
# Stack management
def top(self):
return self.stack[-1] if len(self.stack) > 0 else []
def push(self, x):
return self.stack.append(float(x))
def pop(self, n=1):
return self.stack.pop() if n == 1 else [self.stack.pop() for n in range(n)]
# Handle numbers
def number(self, regex: r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?", **kwargs):
self.stack.append(float(kwargs['token']))
# Binary operators
def plus(self, regex: r"\+", **kwargs):
a, b = self.pop(2)
self.push(b + a)
def minus(self, regex: r"\-", **kwargs):
a, b = self.pop(2)
self.push(b - a)
def multiply(self, regex: r"\*", **kwargs):
a, b = self.pop(2)
self.push(b * a)
def divide(self, regex: r"\/", **kwargs):
a, b = self.pop(2)
self.push(b / a)
def pow(self, regex: r"exp", **kwargs):
a, b = self.pop(2)
self.push(a ** b)
def logN(self, regex: r"logN", **kwargs):
a, b = self.pop(2)
self.push(math.log(a,b))
# Unary operators
def neg(self, regex: r"neg", **kwargs):
self.push(-self.pop())
def sqrt(self, regex: r"sqrt", **kwargs):
self.push(math.sqrt(self.pop()))
def log2(self, regex: r"log2", **kwargs):
self.push(math.log2(self.pop()))
def log10(self, regex: r"log10", **kwargs):
self.push(math.log10(self.pop()))
def pi(self, regex: r"pi", **kwargs):
self.push(math.pi)
def e(self, regex: r"e", **kwargs):
self.push(math.e)
def deg(self, regex: r"deg", **kwargs):
self.push(math.degrees(self.pop()))
def rad(self, regex: r"rad", **kwargs):
self.push(math.radians(self.pop()))
# Whole stack operators
def cls(self, regex: r"c", **kwargs):
self.stack=[]
def sum(self, regex: r"sum", **kwargs):
self.stack=[math.fsum(self.stack)]
if __name__ == '__main__':
calc = RPNCalculator()
print(calc('2 2 exp 3 + neg'))
print(calc('c 1 2 3 4 5 sum 2 * 2 / pi'))
print(calc('pi 2 * deg'))
print(calc('2 2 logN'))
I like this solution because there are no separate lookup tables. The regular expression to match on is embedded in the method to be called as an annotation. For me, this is as it should be. It would be nice if Python allowed more flexible annotations, because I would rather put the regex annotation on the method itself rather than embed it in the method parameter list. However, this isn't possible at the moment.
For interest, take a look at the Wolfram language in which functions are polymorphic on arbitrary patterns, not just on argument types. A function that is polymorphic on a regex is a very powerful idea, but we can't get there cleanly in Python. The RegexDispatcher class is the best I could do.