如何在不使用eval()或exec()的情况下创建规则引擎?

时间:2012-01-12 00:29:57

标签: python dictionary exec eval

我的数据库中有一个简单的规则/条件表,用于为我们的某个系统生成警报。我想创建规则引擎或特定于域的语言。

此表中存储的简单规则是..(省略此处的关系)

if temp > 40 send email

请注意,会有更多这样的规则。脚本每天运行一次以评估这些规则并执行必要的操作。一开始,只有一条规则,所以我们有脚本只支持该规则。但是,我们现在需要使其更具可扩展性,以支持不同的条件/规则。我已经研究过规则引擎,但我希望以一种简单的pythonic方式实现这一点。目前,我只提出了eval / exec,我知道这不是最推荐的方法。那么,实现这个目标的最佳方法是什么?

(规则作为数据存储在数据库中,因此每个对象如“温度”,条件如“> / = ..等”,值类似于“40,50..etc”,动作如“email,sms,等等。“存储在数据库中,我检索这个以形成条件...如果temp> 50发送电子邮件,那是我的想法,然后使用exec或eval对它们进行实时代码..但不确定如果这是正确的方法)

8 个答案:

答案 0 :(得分:3)

好吧,如果您要做的是发送电子邮件,请使用email module

如果我是你,我会编写一个简单的Python脚本来处理一堆规则,可能只是在一个单独的文件中编写为简单的Python语句,然后发送电子邮件/短信/ ...用于那些需要的规则要执行的行动。

您可以使用cron

等服务每天运行一次(或其他)

例如,如果您的规则如下所示:

# Rule file: rules.py

def rule1():
    if db.getAllUsers().contains("admin"): 
        return ('email', 'no admin user in db')
    else:
        return None, None

def rule2():
    if temp > 100.0: 
        return ('sms', 'too hot in greenhouse')
    else:
        return (None, None)

...

rules = [rule1, rule2, ....]

然后您的处理脚本可能如下所示:

# Script file: engine.py

import rules
import email
...

def send_email(message, receiver):
    # function that sends an email...

def send_sms(message, receiver):
    # function that sends an sms...

actions = {'email':send_email, 'sms':send_sms, ...}    

if __name__ == '__main__':

    # Declare receiver here...

    for rule in rules.rules:
        # Does the rule return a do-able action?
        # To be really paranoid we might wrap this in a try/finally
        # in case the rules themselves have any side effects,
        # or they don't all return 2-tuples.
        act, message = rule()
        if act in actions:
            # perform the action
            actions[rule()](message, receiver) 

毫无疑问,还有其他方法可以做到这一点,例如创建用于编写规则的Pythonic DSL

答案 1 :(得分:1)

有几种方法可以实现这一目标。其他答案很有价值,我想补充两种技巧。

  • 如果您可以重写表juste,则将每个规则创建为可在需要时反序列化的pickle函数
  • 写一个大字典,其中规则作为键,函数作为值。如果您有100个最大规则,这是可管理的。只需确保使用* args和** kwargs进行非常灵活的功能。

有泡菜的例子:

首先,制作一个灵活的输入功能。

def greater_than(value, *args, **kwargs):
    return all(value > i for i in args)

然后pickle

>>> import pickle
>>> rule = pickle.dumps(greater_than)
>>> rule # store this in DB
'ctest\ngreater_than\np0\n.'

然后当你需要让你恢复业务规则时:

>>> func = pickle.loads(rule) # rule is the sring from DB
>>> func(5, 4, 3, 1)
True
>>> func(5, 6) 
False

灵活输入的目的是获得任意数量的参数:

>>> args = [1, 2, 3]
>>> func(5, *args)
True 

字典示例

将所有功能存储在一个大的映射中:

def greater_than(value, *args, **kwargs):
    return all(value > i for i in args)

RULES = {
    'if x > y': greater_than
    'other rule': other_func,
    etc
}

然后当你需要它时:

   >>> func = RULES['if x > y']
   >>> func(5, 1)
   True

答案 2 :(得分:1)

由于eahc规则的“变量”,“值”和比较运算符在数据库上,因此您可以编写一个规则类,该类将采用适当的参数(运算符,操作,值等等)和产量一个可调用的对象,它将以字典的形式接收所有相关变量,并采取适当的注册操作。

它看起来像这样,但你必须适应它才能获得适合你行动的参数:

import operator

class Rule(object):
    def __init__(self, variable_name, op, value, action):
        op_dict = {"=": operator.eq,
                   ">": operator.gt,
                   "<": operator.lt,
                   #(...)
                  }
        action_dict = {"email": email_function,
                       "log": log_function,
                       # ...
                      }
        self.variable = variable_name
        self.op = op_dict[op]
        self.value = value
        self.action = action_dict[action]
    def __call__(self, value_dict, action_parameters, k_action_parameters):
        if self.op(value_dict[self.variable], self.value):
            return self.action(*action_parameters, **k_action_parameters)
        return False

rule = Rule("temp", ">", "email")
for result in query():
     rule(result, ())

答案 3 :(得分:0)

编写解析器。见pyparsing。

或者,采用表格驱动的方法。

答案 4 :(得分:0)

为什么需要将规则存储到数据库中? 你不能只将数据存储在数据库中并将规则放入python模块吗?

例如,在文件rules.py中,您可以:

  • 写一套规则
  • 为您的数据编写解析器,为每个参数输入正确的规则

然后在您的主要内容中,您只需将数据传递给rules.parser(),所有内容都将得到处理。

修改:看到你的评论我已经new answer

答案 5 :(得分:0)

我认为你主要需要两件事:

  • 用于连接前端模板的Rule
  • pickle将您的规则存储在数据库中。

这就是你的主体看起来像:

import pickle

# some data loaded from your DB
data = {'temp': 60, 'wind': 150}

# entry should be provided by your front-end template
entry = {'param_name': 'temp', 'test': Test(gt, 50), 'action': send_email}

rule = Rule(**entry)
to_store = pickle.dumps(rule)
# store 'to_store' into your DB

# Let's pretend to load the previously stored rule
stored = to_store
rule = pickle.loads(stored)
rule(data)

我们的想法是从模板中获取构建规则所需的所有信息,然后只需将该规则存储在pickle中。

这可能是Rule实施:

# =======
# Actions
# =======
#
# Any callable with no arguments is an Action
# (you may need to implement this)
#

def send_email():
    print('email sent')

# ==========
# Test class
# ==========
#
# Test class is a way to call your test function.
# The real test is in self.function
#

class Test:

    def __init__(self, function, *args):
        self.function = function
        self.args = args

    def __call__(self, parameter):
        return self.function(parameter, *self.args)

# ==============
# Test functions
# ==============
#
# These are the functions that are going to be executed
#

import operator

gt = operator.gt

def more_complex_test(*args):
    pass

# ==========
# Rule class
# ==========
#
# A Rule needs to know:
#  - the parameter to test
#  - the test to perform
#      - the action to execute
#

class Rule:

    def __init__(self, param_name, test, action):
        self.param_name = param_name
        self.test = test
        self.action = action

    def __call__(self, data):   # data is a dictionary {'temp': 60, ...}
        param_value = data[self.param_name]
        if self.test(param_value):
            return self.action()
        return False

注意:可以执行上面的两段代码(如果放入togheter)。试一试!

答案 6 :(得分:0)

我使用了@jsbuenos代码片段并做了一些更改来形成这个。基本上,我还需要支持检查“度量单位”以获得评估条件的规则。所以支持差异。规则,如果时间&gt; 24小时发送电子邮件或如果温度> 40摄氏度发送电子邮件等(我可能稍后会有其他单位..)我已经包含了一个新的dict来将度量单位映射到一个计算函数,并相应地改变了该类的可调用函数。这是正确的做法吗?

import operator

class Rule(object):
    def __init__(self, variable_name, op, value, action):
      op_dict = {"=": operator.eq,
               ">": operator.gt,
               "<": operator.lt,
               #(...)
              }
      action_dict = {"email": email_function,
                   "log": log_function,
                   # ...
                  }

      eval_condition = {"hrs" :  self.raise_timeexceeded_alert,
                    "celsius" : self.raise_tempexceeded_alert,
                    #}  

      self.variable = variable_name
      self.op = op_dict[op]
      self.value = value
      self.action = action_dict[action]
      self.uom = measure      
      self.raise_alert = eval_condition[measure]

   def __call__(self, actual_value, *action_parameters):
     if self.raise_alert(actual_value,self.op,self.uom,self.threshold):
        return self.action(*action_parameters)
    return False

   def raise_timeexceeded_alert(self,timevalue, op, uom, threshold):
    #calculate time difference with respect to local timezone and return true
    # if diff is 'operator' threshold
    localtime=pytz.timezone(TIMEZONE)
    ....
    ...
    return False


   def raise_tempexceeded_alert(self,timevalue, op, uom, threshold):
     #return True if temp. is 'operator' threshold
     ....
     .....
     return False


rule = Rule("time", ">=", "24" , "hrs", "email")
args = [contact_email,message]
rule("2011-12-11 12:06:03",*args)

答案 7 :(得分:0)

您需要查看NebriOS。规则是用纯Python编写的,而不是将它们存储在数据库中。例如:

class hello(NebriOS):
    listens_to = ['temp']

    def check(self):
        return self.temp > 40

    def action(self):
        send_email ("angela@example.com","""
            Alert: Temp is now > 40! """)

我认为在此应用程序中使用规则引擎有一定的价值。引用Martin Fowler描述一个:

  

规则引擎就是提供替代计算   模型。而不是通常的命令式模型,顺序命令   通过条件和循环,它提供了生产规则列表。   每条规则都有一个条件和一个动作 - 你可以简单地思考   它作为一堆if-then语句。

对某些软件项目采用非线性方法有助于使其更加健壮,准确且易于理解。像“temp&gt; 40然后执行x”这样的小规则作为独立规则编写起来比构建使用相同规则的完整应用程序要容易得多。它不需要线性链来评估。一经编写,始终强制执行。

另一个好处是,如果一个规则发生故障,其余规则将正常运行。这通常会导致与传统软件(命令模型)技术的更多混淆。

我为现在的公司建立了这个应用程序。我认为规则是统治的。只是我的两分钱。