eregs / regulations-parser

Parser for U.S. federal regulations and other regulatory information
Creative Commons Zero v1.0 Universal
37 stars 39 forks source link

Try a decorator-based alternative to abc #326

Closed cmc333333 closed 7 years ago

cmc333333 commented 7 years ago

When removing duplicated chunks of logic, we tend to replace with declarative code (i.e. just indicating the parts that are unique to each situation). That tends to make heavy use of an abstract base class:

class SomeBase(object):
    __metaclass__ = abc.metaclass

    @abc.abstractproperty
    def SOME_CONST(self):
        raise NotImplemented()

    @abc.abstractmethod
    def some_logic(self, input_here):
        raise NotImplemented()

    def process_input(self, input_here, tail):
        return self.some_logic(input_here) + self.SOME_CONST + tail

class Example1(SomeBase):
    SOME_CONST = '1'

    def some_logic(self, input_here):
        return input_here * 2

class Example2(SomeBase):
    SOME_CONST = '2'

    def some_logic(self, input_here):
        return input_here * 4

for cls in (Example1, Example2):
    print(cls().process_input("input", "tail"))

That works okay, but includes a lot of boiler plate and isn't a great use case for classes (as every class is effectively a singleton). I'd argue there are multiple levels of indirection, too, which make reading the code more difficult.

Here, I propose an alternative based on decorators, inspired by py.test's heavy decorator use. The decorators create a little DSL, significantly altering the wrapped function (notably, changing its arguments and adding extra logic). That makes reading the logic more challenging than if it were all in-lined, but I think better than the abc situation above. To replicate the above example, we'd have:

def process_with_const(some_const):
    def decorator(fn):
        @wraps(fn)
        def wrapper(input_here, tail):
            return fn(input_here) + some_const + tail
        return wrapper
    return decorator

@process_with_const('1')
def process_with_example1(input_here):
    return input_here * 2

def example2(input_here):
    return input_here * 4
process_with_example2 = process_with_const('2')(example)

for fn in (process_with_example1, process_with_example2):
    print(fn("input", "tail"))  # note the arg count is different

What do you think? Is this worth pursuing?

EricSchles commented 7 years ago

It looks very pythonic! It seems more minimal then the previous example. So here is a good open question - is there a performance difference that might matter for non simple examples? I'm going to run some experiments.

cmc333333 commented 7 years ago

Low risk and this has been open for a bit, so I'm merging.