expression.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """规则库内表达式的确定性求值器。
  2. 规则库把公式写成字符串,这里用白名单 AST 求值,既能保证「公式不由大模型
  3. 生成」,又能让公式与参数分离、可版本化。不引入 eval 的开放执行面。
  4. """
  5. from __future__ import annotations
  6. import ast
  7. import math
  8. from typing import Any, Mapping
  9. class ExpressionError(ValueError):
  10. """表达式非法或引用了未提供的参数。"""
  11. _ALLOWED_FUNCS = {
  12. "max": max,
  13. "min": min,
  14. "sum": sum,
  15. "abs": abs,
  16. "round": round,
  17. "ceil": math.ceil,
  18. "floor": math.floor,
  19. "len": len,
  20. }
  21. _ALLOWED_NODES = (
  22. ast.Expression, ast.BinOp, ast.UnaryOp, ast.Constant, ast.Name, ast.Load,
  23. ast.Call, ast.List, ast.Tuple, ast.Set, ast.Compare, ast.IfExp, ast.BoolOp,
  24. ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow, ast.USub, ast.UAdd,
  25. ast.Gt, ast.GtE, ast.Lt, ast.LtE, ast.Eq, ast.NotEq, ast.And, ast.Or, ast.Not,
  26. ast.LtE, ast.keyword,
  27. )
  28. # 表达式规模上限:规则库里的公式都是短式,超限说明输入异常
  29. _MAX_LENGTH = 500
  30. _MAX_POWER = 8
  31. def evaluate(expression: str, params: Mapping[str, Any]) -> float:
  32. """按 params 求值;参数缺失抛 ExpressionError,由调用方转"依据不足"。"""
  33. if not isinstance(expression, str) or not expression.strip():
  34. raise ExpressionError("表达式为空")
  35. if len(expression) > _MAX_LENGTH:
  36. raise ExpressionError("表达式过长")
  37. try:
  38. tree = ast.parse(expression, mode="eval")
  39. except SyntaxError as exc:
  40. raise ExpressionError(f"表达式语法错误:{exc.msg}") from exc
  41. for node in ast.walk(tree):
  42. if not isinstance(node, _ALLOWED_NODES):
  43. raise ExpressionError(f"表达式含不允许的语法:{type(node).__name__}")
  44. if isinstance(node, ast.Call):
  45. if not isinstance(node.func, ast.Name) or node.func.id not in _ALLOWED_FUNCS:
  46. name = getattr(node.func, "id", type(node.func).__name__)
  47. raise ExpressionError(f"表达式含不允许的函数:{name}")
  48. if isinstance(node, ast.Constant) and isinstance(node.value, str):
  49. raise ExpressionError("表达式不允许字符串常量")
  50. if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Pow):
  51. exponent = getattr(node.right, "value", None)
  52. if not isinstance(exponent, (int, float)) or abs(exponent) > _MAX_POWER:
  53. raise ExpressionError("幂次超出允许范围")
  54. try:
  55. value = _eval_node(tree.body, params)
  56. except ExpressionError:
  57. raise
  58. except (TypeError, ValueError, ZeroDivisionError, KeyError) as exc:
  59. raise ExpressionError(f"表达式求值失败:{exc}") from exc
  60. if isinstance(value, bool) or not isinstance(value, (int, float)):
  61. raise ExpressionError("表达式未返回数值")
  62. return float(value)
  63. def try_evaluate(expression: str, params: Mapping[str, Any]) -> tuple[float | None, str | None]:
  64. """求值失败不抛异常,返回 (值, 失败原因),供"依据不足"分支使用。"""
  65. try:
  66. return evaluate(expression, params), None
  67. except ExpressionError as exc:
  68. return None, str(exc)
  69. def missing_params(expression: str, params: Mapping[str, Any]) -> list[str]:
  70. """列出表达式引用但 params 中缺失的变量名(按在表达式中出现的先后顺序)。"""
  71. try:
  72. tree = ast.parse(expression, mode="eval")
  73. except SyntaxError:
  74. return []
  75. names: list[str] = []
  76. for node in sorted(ast.walk(tree), key=lambda item: (getattr(item, "lineno", 0),
  77. getattr(item, "col_offset", 0))):
  78. if isinstance(node, ast.Name) and node.id not in _ALLOWED_FUNCS:
  79. if node.id not in params and node.id not in names:
  80. names.append(node.id)
  81. return names
  82. def _eval_node(node: ast.AST, params: Mapping[str, Any]) -> Any:
  83. if isinstance(node, ast.Constant):
  84. return node.value
  85. if isinstance(node, ast.Name):
  86. if node.id in _ALLOWED_FUNCS:
  87. return _ALLOWED_FUNCS[node.id]
  88. if node.id not in params:
  89. raise ExpressionError(f"缺少参数:{node.id}")
  90. return params[node.id]
  91. if isinstance(node, ast.Call):
  92. func = _eval_node(node.func, params)
  93. if not callable(func):
  94. raise ExpressionError("调用目标不是函数")
  95. args = [_eval_node(arg, params) for arg in node.args]
  96. return func(*args)
  97. if isinstance(node, ast.List):
  98. return [_eval_node(item, params) for item in node.elts]
  99. if isinstance(node, ast.Tuple):
  100. return tuple(_eval_node(item, params) for item in node.elts)
  101. if isinstance(node, ast.Set):
  102. return {_eval_node(item, params) for item in node.elts}
  103. if isinstance(node, ast.UnaryOp):
  104. operand = _eval_node(node.operand, params)
  105. if isinstance(node.op, ast.USub):
  106. return -operand
  107. if isinstance(node.op, ast.UAdd):
  108. return +operand
  109. if isinstance(node.op, ast.Not):
  110. return not operand
  111. raise ExpressionError("不支持的一元运算符")
  112. if isinstance(node, ast.BinOp):
  113. left = _eval_node(node.left, params)
  114. right = _eval_node(node.right, params)
  115. return _apply_binop(node.op, left, right)
  116. if isinstance(node, ast.BoolOp):
  117. values = [_eval_node(value, params) for value in node.values]
  118. return all(values) if isinstance(node.op, ast.And) else any(values)
  119. if isinstance(node, ast.Compare):
  120. left = _eval_node(node.left, params)
  121. for op, comparator in zip(node.ops, node.comparators):
  122. right = _eval_node(comparator, params)
  123. if not _compare(op, left, right):
  124. return False
  125. left = right
  126. return True
  127. if isinstance(node, ast.IfExp):
  128. return _eval_node(node.body, params) if _eval_node(node.test, params) else _eval_node(node.orelse, params)
  129. raise ExpressionError(f"不支持的语法:{type(node).__name__}")
  130. def _apply_binop(op: ast.AST, left: Any, right: Any) -> Any:
  131. if isinstance(op, ast.Add):
  132. return left + right
  133. if isinstance(op, ast.Sub):
  134. return left - right
  135. if isinstance(op, ast.Mult):
  136. return left * right
  137. if isinstance(op, ast.Div):
  138. return left / right
  139. if isinstance(op, ast.Mod):
  140. return left % right
  141. if isinstance(op, ast.Pow):
  142. return left ** right
  143. raise ExpressionError(f"不支持的运算符:{type(op).__name__}")
  144. def _compare(op: ast.AST, left: Any, right: Any) -> bool:
  145. if isinstance(op, ast.Gt):
  146. return left > right
  147. if isinstance(op, ast.GtE):
  148. return left >= right
  149. if isinstance(op, ast.Lt):
  150. return left < right
  151. if isinstance(op, ast.LtE):
  152. return left <= right
  153. if isinstance(op, ast.Eq):
  154. return left == right
  155. if isinstance(op, ast.NotEq):
  156. return left != right
  157. raise ExpressionError(f"不支持的比较符:{type(op).__name__}")