Динамические переменные из файла параметров JSON

Динамические переменные из файла параметров JSON
Динамические переменные из файла параметров JSON - disruptxn @ Unsplash

Я хочу назначить переменные Python, импортированные из файла JSON. На этот вопрос был интересный ответ с использованием classmethod, но я не смог заставить его работать, а комментировать мне запрещено....

Итак, давайте рассмотрим очень простой пример: Я хочу оценить z = x^2+y^2, но я хочу иметь возможность определить x и y в JSON-файле. Мой json-файл (params.json) может выглядеть следующим образом:

{
    "x":3,
    "y":2
}

Затем я мог бы загрузить файл и сгенерировать динамические переменные:

with open("params.json", "r") as read_file:
    params = json.load(read_file)

for k, v in params.items():
    vars()[k] = v
    
z = x^2+y^2

Это работает, но кажется опасным динамически генерировать переменные. Есть ли стандартный/более разумный способ сделать это?

Это во многом зависит от ваших целей безопасности и от того, какой пользовательский интерфейс вы хотите предложить автору этих выражений.

Загрузка переменных в локальную область работает, поскольку Python — очень динамичный язык. Однако существует риск того, что переменные могут переопределить существующие объекты, что нарушит ваш код — что, если, например, есть переменная с именем len?

Поэтому обычно безопаснее избегать запуска пользовательского ввода в контексте Python. Вместо:

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

У Python есть инструменты, которые помогут в этом. Мы можем анализировать строки как код Python с помощью модуля ast. Это возвращает структуру данных, представляющую синтаксис, и ничего не выполняет (хотя синтаксический анализатор не обязательно защищен от вредоносных входных данных). Мы можем взять структуру данных, пройтись по ней и выполнить ее в соответствии с определенными нами правилами, например, разрешая переменные только из словаря. Пример кода для Python 3.10:

import ast

def interpret(code: str, variables: dict) -> dict:
  module: ast.Module = ast.parse(code, mode='exec')
  for statement in module.body:
    _interpret_statement(statement, variables)
  return variables

def _interpret_statement(statement: ast.stmt, variables: dict) -> None:
  match statement:
    case ast.Assign(targets=[ast.Name(id=name)], value=value):
      variables[name] = _interpret_expr(value, variables)
      return

    case other:
      raise InterpreterError("Syntax not supported", other)

def _interpret_expr(expr: ast.expr, variables: dict) -> Any:
  match expr:
    case ast.BinOp(left=left_ast, op=op, right=right_ast):
      left = _interpret_expr(left_ast, variables)
      right = _interpret_expr(right_ast, variables)
      return _interpret_binop(left, op, right)

    case ast.Name(id=name):
      return variables[name]

    case ast.Constant(value=(int(value) | float(value))):
      return value

    case other:
      raise InterpreterError("Syntax not supported", other)
    

def _interpret_binop(left: Any, op: ast.operator, right: Any) -> Any:
  match op:
    case ast.Add(): return left + right
    case ast.Sub(): return left - right
    case ast.Mult(): return left * right
    case ast.Div(): return left / right
    case ast.Pow(): return left**right
    case other:
      raise InterpreterError(
        "Operator not supported",
        ast.BinOp(ast.Name("_"), other, ast.Name("_")))

class InterpreterError(Exception):
  def __init__(self, msg: str, code: Optional[ast.AST] = None) -> None:
    super().__init__(msg, code)
    self._msg = msg
    self._code = code

  def __str__(self):
    if self._code:
      return f"{self._msg}: {ast.unparse(self._code)}"
    return self._msg

Затем это можно использовать для интерпретации команд, возвращая словарь со всеми переменными:

>>> interpret("z = x**2+y**2", {"x": 3, "y": 2})
{'x': 3, 'y': 2, 'z': 13}

Хотя это позволяет вам интерпретировать код Python так, как вы хотите (вы контролируете семантику), вы по-прежнему ограничены синтаксисом Python. Например, вы должны использовать оператор ** для возведения в степень, а не ^ xor-оператор Python.

Если вам нужен собственный синтаксис, вам, вероятно, придется написать собственный синтаксический анализатор. Существует множество алгоритмов синтаксического анализа и генераторов синтаксических анализаторов, но я неравнодушен к написанному от руки «рекурсивному спуску». Обычно это включает в себя написание рекурсивных функций вида parse(Position) -> Optional[tuple[Position, Value]], которые постепенно потребляют ввод. Я написал пример синтаксического анализатора и интерпретатора , используя эту стратегию, и ранее сравнивал различные подходы к синтаксическому анализу в ответе о реализации языков запросов в программе Python.


LetsCodeIt, 5 января 2023 г., 20:58