Как создать движок правил без использования eval () или exec ()? - PullRequest
3 голосов
/ 12 января 2012

В моей базе данных есть простая таблица правил / условий, которая используется для генерации оповещений для одной из наших систем. Я хочу создать движок правил или специфичный для домена язык.

Простое правило, хранящееся в этой таблице, будет .. (здесь пропущены отношения)

if temp > 40 send email

Обратите внимание, что таких правил будет намного больше. Сценарий запускается один раз в день, чтобы оценить эти правила и выполнить необходимые действия. В начале было только одно правило, поэтому у нас был сценарий, поддерживающий только это правило. Однако теперь нам нужно сделать его более масштабируемым для поддержки различных условий / правил. Я изучил движки правил, но я надеюсь достичь этого простым питоническим способом. На данный момент я только придумал eval / exec и знаю, что это не самый рекомендуемый подход. Итак, что было бы лучшим способом сделать это ??

(Правила хранятся в виде данных в базе данных, поэтому каждый объект, например, "температура", условие, например, "> / = .. и т. Д.", Значение, подобное "40,50..etc", и действие, подобное "email, sms и т. Д." .. "хранятся в базе данных, я извлекаю это, чтобы сформировать условие ... если temp> 50 отправляет электронную почту, это была моя идея, чтобы затем использовать exec или eval для них, чтобы сделать его живым кодом .. но не уверен, что это правильный подход)

Ответы [ 8 ]

3 голосов
/ 12 января 2012

Хорошо, если вы хотите отправлять электронные письма, используйте модуль email .

На вашем месте я написал бы простой скрипт на 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 голос
/ 12 января 2012

Поскольку «переменная», «значение» и оператор сравнения для правила 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, ())
1 голос
/ 12 января 2012

Есть несколько способов добиться этого.Другие ответы полезны, и я хотел бы добавить два метода.

  • При условии, что вы можете переписать таблицу, просто создайте каждое правило в виде маринованной функции, которую можно десериализовать при необходимости
  • Напишите большой словарь с правилами в качестве ключа и функцией.Если у вас есть 100 правил макс, это можно сделать.Просто убедитесь, что вы делаете очень гибкие функции, используя * args и ** kwargs.

Пример с pickle:

Сначала создайте функцию, которая является гибкой с помощьюего ввод.

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

Затем рассол это:

>>> 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
0 голосов
/ 09 октября 2013

Вам захочется взглянуть на 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! """)

Я думаю, что использование механизма правил для этого приложения имеет смысл.Процитируем Мартина Фаулера, описав его:

Механизм правил - это обеспечение альтернативной вычислительной модели.Вместо обычной императивной модели, команды в последовательности с условными выражениями и циклами, он предоставляет список правил производства.Каждое правило имеет условие и действие - упрощенно вы можете рассматривать его как набор операторов if-then.

Использование нелинейного подхода к определенным программным проектам помогает сделать его более надежным, точным иЛегко понять.Небольшое правило, такое как «temp> 40 затем do x», гораздо проще написать как отдельное правило, чем создать полное приложение, использующее то же правило.Для оценки не требуется линейная цепочка.После того, как написано, всегда исполняется.

Другое преимущество состоит в том, что если одно правило нарушается, остальные правила работают как обычно.Как правило, это вызвало бы гораздо большую путаницу с традиционными методами программного обеспечения (императивных моделей).

Я выпустил это приложение на волю после создания его для моей нынешней компании.Я думаю, правила правил.Просто мои два цента.

0 голосов
/ 18 января 2012

Я использовал фрагмент кода @jsbuenos и сделал несколько изменений, чтобы сформировать это.По сути, мне также нужна поддержка, чтобы проверить «единицу измерения» правила для оценки состояния.Так что для поддержки различий.правила, например, если время> 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)
0 голосов
/ 15 января 2012

Я думаю, вам в основном нужны две вещи:

  • a 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

Примечание: Можно выполнить два приведенных выше кода (если их объединить). Попробуйте!

0 голосов
/ 12 января 2012

Зачем вам нужно хранить правила в базе данных?
Разве вы не можете просто сохранить данные в базе данных и поместить правила в модуль python?

Например, в файл rules.py вы могли бы:

  • Написать набор правил
  • Создать анализатор для ваших данных, который будет применять правильное правило для каждого параметра

Затем вВаше главное, вам просто нужно передать данные на ваш rules.parser(), и обо всем позаботятся.

Редактировать: Посмотрев ваш комментарий я сделал новый ответ .

0 голосов
/ 12 января 2012

Написать парсер.См. Pyparsing.

В качестве альтернативы используйте подход, основанный на таблицах.

...