so - I have built a bit of a rules engine in python - but I'm fairly new to python... my engine is fairly nice to use - but adding a new rule is pretty ugly, and I'm wondering if there's a way to clean it up.
The key thing to remember is that rules have side-effects, rules can be combined with ands, ors, etc - and you only apply the side effects if the whole rule succeeded - ie the check if the rule succeeded can't be combined with perfoming the side effect.
So every rule ends up looking something like this:
def sample_rule():
def check( item ):
if item.doesnt_pass_some_condition(): return None
def action_to_perform():
item.set_some_value()
item.set_some_other_value()
return action_to_perform
return Rule(check)
which seems horribly ugly - but you don't seem to be able to do multiline lambdas or zero line lambas... I guess I'm looking for something like:
def sample_rule():
return Rule( lambda x: x.passes_condition(),
lambda x: {x.set_some_value(), x.set_some_other_value)}
but both the condition and the side effect could be multiple lines, and the side effect is often empty.
so is there a simpler pattern that i can apply that will apply to every case? (I really don't want to use the above pattern when I have exactly one line of condition and one line of side effect, and a completely different pattern in the other cases)
just out of interest, at the end you end up with something like
rule1 = sample_rule().andalso( other_rule_1().or(other_rule_2)).butnot( other_rule_3)
...
...
for thing_to_check in lots_of_things:
for rule in lots_of_rules:
if rule.apply_to( thing_to_check): break # take the first rule that applies
CodePudding user response:
Instead of defining multi-line lambdas, you could define multiple lambdas in a list and then use all lambdas in the list as required:
class Rule:
def __init__(self, checks=None, actions=None):
self.checks = checks if checks else []
self.actions = actions if actions else []
def apply_to(self, item):
if all([check(item) for check in self.checks]):
return self.actions
else:
return None
def sample_rule():
return Rule(checks=[lambda x: x.passes_condition()],
actions=[lambda x: x.set_some_value(),
lambda x: x.set_some_other_value()])
Giving checks and actions a default value of None in the class also means you don't have to define them if there are no checks/actions for a given rule; further simplifying the definition of new rules.
CodePudding user response:
I would probably define the Rule class to be something like this and define check and perform_action in subclasses:
class Rule:
def check(self, item):
# do some checking logic
pass
def perform_action(self, item):
# do some action
pass
def apply_to(self, item):
if self.check(item):
self.perform_action(item)
return True
else:
return False
class SomeSpecificRule(Rule):
def check(self, item):
# do some specific check
pass
def perform_action(self, item):
# do some specific action
pass
It's a bit verbose, but I think it's straightforward to read/maintain. Your editor should also be able to fill in the method stubs automatically, which saves some time if you're making a lot of rules. You could also use inheritance to share behavior for different kinds of rules, such as ones which all perform the same check or all do the same action.
I'm not sure how you were defining Rule.andalso, etc., before, but you could define an AndRule class like this:
class AndRule(Rule):
def __init__(self, rule_1, rule_2):
self.rule_1 = rule_1
self.rule_2 = rule_2
def check(self, item):
return self.rule_1.check(item) and self.rule_2.check(item)
def apply_to(self, item):
if self.check(item):
self.rule_1.perform_action(item)
self.rule_2.perform_action(item)
return True
else:
return False
Which means that Rule.andalso could simply be:
class Rule:
...
def andalso(self, other_rule):
return AndRule(self, other_rule)
