Шаблон проектирования для реляционных и дополнительных параметров? - PullRequest
0 голосов
/ 06 июня 2019

Я должен построить класс, который принимает серию входных данных через метод конструктора, а затем выполнить вычисление с calculate(), используя эти параметры. Хитрость здесь в том, что эти параметры могут быть доступны иногда, а другие - нет Тем не менее, существует заданное уравнение между переменными, так что недостающие из них могут быть вычислены из уравнений. Вот пример:

Я знаю, что:

a = b * c - d 
c = e/f

Я всегда рассчитываю a+b+c+d+e+f

Вот что у меня есть:

class Calculation:
  def __init__(self, **kwargs):
    for parameter, value in kwargs.items():
      setattr(self, '_'.format(parameter), value)

  @property
  def a(self):
    try:
      return self._a  
    except AttributeError:
      return self._b * self._c - self._d


  @property
  def b(self):
    try:
      return self._b  
    except AttributeError:
      return (self._a + self._d) / self._c

... // same for all a,b,c,d,e,f 

  def calculate(self):
    return sum(self.a+self.b+self.c+self.d+self.e+self.f)

затем используйте как:

  c = Calculation(e=4,f=6,b=7,d=2)
  c.calculate()

однако, в другое время могут быть другие переменные, такие как: c = расчет (b = 5, c = 6, d = 7, e = 3, f = 6) c.calculate ()

Мой вопрос: какой шаблон дизайна будет хорошим в моем случае? Пока что кажется немного избыточным сделать @property для всех переменных. Проблема, которую он должен решить, состоит в том, чтобы принять любые переменные (минимум, для которых возможен расчет) и на основе уравнения, которое у меня есть, выяснить остальное, необходимое для расчета.

Ответы [ 3 ]

1 голос
/ 06 июня 2019

Это хороший кандидат для функции getattr .Вы можете хранить ключевые аргументы непосредственно в классе и использовать этот словарь, чтобы либо вернуть известный параметр в качестве атрибута, либо вывести неопределенное значение «на лету» на основе других известных вам формул:

class Calculation:
  def __init__(self, **kwargs):
      self.params   = kwargs
      self.inferred = {
          "a"     : lambda: self.b * self.c - self.d,
          "c"     : lambda: self.e / self.f,
          "result": lambda: self.a+self.b+self.c+self.d+self.e+self.f
          }

  def __getattr__(self, name):
      if name in self.params:
          return self.params[name]
      if name in self.inferred:
          value = self.inferred[name]()
          self.params[name] = value
          return value

r = Calculation(b=1,d=3,e=45,f=9).result
print(r) # 65.0 (c->45/9->5, a->1*5-3->2)

Обратите внимание, что если у вас есть очень сложные вычисления для некоторых параметров, вы можете использовать функции класса в качестве реализации лямбда-выражений в словаре self.inferred.

Если вы 'Если вы собираетесь использовать этот шаблон для многих формул, вы можете захотеть централизовать стандартный код в базовом классе.Это сократит работу, необходимую для новых классов вычислений, до необходимости только реализации функции inferred () .:

class SmartCalc:

  def __init__(self, **kwargs):
      self.params   = kwargs

  def __getattr__(self, name):
      if name in self.params:
          return self.params[name]
      if name in self.inferred():
          value = self.inferred()[name]()
          self.params[name] = value
          return value

class Calculation(SmartCalc):

    def inferred(self):
        return {
                 "a"     : lambda: self.b * self.c - self.d,
                 "b"     : lambda: (self.a+self.d)/self.c,
                 "c"     : lambda: self.e / self.f,
                 "d"     : lambda: self.c * self.b - self.a,
                 "e"     : lambda: self.f * self.c,
                 "f"     : lambda: self.e / self.c,
                 "result": lambda: self.a+self.b+self.c+self.d+self.e+self.f
               }

Имея достаточно содержимого в inferred (), вы даже можете использовать этот подход для получения любого значения изкомбинация других:

valueF = Calculation(a=2,b=1,c=5,d=3,e=45,result=65).f
print(valueF) # 9.0

РЕДАКТИРОВАТЬ

Если вы хотите сделать это еще более сложным, вы можете улучшить getattr , чтобы позволитьспецификация зависимостей в словаре inferred ().

Например:

class SmartCalc:

    def __init__(self, **kwargs):
        self.params   = kwargs

    def __getattr__(self, name):
        if name in self.params:
            return self.params[name]
        if name in self.inferred():
            calc  = self.inferred()[name]
            if isinstance(calc,dict):
                for names,subCalc in calc.items():
                    if isinstance(names,str): names = [names]
                    if all(name in self.params for name in names):
                        calc = subCalc; break
            value = calc()
            self.params[name] = value
            return value

import math
class BodyMassIndex(SmartCalc):

    def inferred(self):
        return {
                  "heightM"      : { "heightInches":     lambda: self.heightInches * 0.0254,
                                     ("bmi","weightKg"): lambda: math.sqrt(self.weightKg/self.bmi),
                                     ("bmi","weightLb"): lambda: math.sqrt(self.weightKg/self.bmi)
                                   }, 
                  "heightInches" : lambda: self.heightM / 0.0254,
                  "weightKg"     : { "weightLb":             lambda: self.weightLb / 2.20462,
                                     ("bmi","heightM"):      lambda: self.heightM**2*self.bmi,
                                     ("bmi","heightInches"): lambda: self.heightM**2*self.bmi
                                   },
                  "weightLb"     : lambda: self.weightKg * 2.20462,
                  "bmi"          : lambda: self.weightKg / (self.heightM**2)
               }

bmi = BodyMassIndex(heightM=1.75,weightKg=130).bmi
print(bmi) # 42.44897959183673

height = BodyMassIndex(bmi=42.45,weightKg=130).heightInches
print(height) # 68.8968097135968  (1.75 Meters)

EDIT2

Аналогичный класс может быть разработан для обработки формулвыражается в виде текста.Это позволило бы использовать базовую форму решателя терминов с использованием итерационной аппроксимации Ньютона-Рафсона (по крайней мере, для полиномиальных уравнений в 1 градус):

class SmartFormula:

    def __init__(self, **kwargs):
        self.params        = kwargs
        self.moreToSolve   = True
        self.precision     = 0.000001
        self.maxIterations = 10000

    def __getattr__(self, name):
        self.resolve()
        if name in self.params: return self.params[name]

    def resolve(self):
        while self.moreToSolve:
            self.moreToSolve = False
            for formula in self.formulas():
                param = formula.split("=",1)[0].strip()
                if param in self.params: continue
                if "?" in formula:
                    self.useNewtonRaphson(param)
                    continue
                try: 
                    exec(formula,globals(),self.params)
                    self.moreToSolve = True
                except: pass

    def useNewtonRaphson(self,name):
        for formula in self.formulas():
            source,calc = [s.strip() for s in formula.split("=",1)]
            if name   not in calc: continue
            if source not in self.params: continue            
            simDict = self.params.copy()
            target  = self.params[source]
            value   = target
            try:
                for _ in range(self.maxIterations):                    
                    simDict[name] = value
                    exec(formula,globals(),simDict)
                    result        = simDict[source]
                    resultDelta   = target-result
                    value        += value*resultDelta/result/2
                    if abs(resultDelta) < self.precision/2 : 
                        self.params[name] = round(simDict[name]/self.precision)*self.precision
                        self.moreToSolve  = True
                        return

            except: continue        

При таком подходе калькулятор BodyMassIndex будет легче читать:

import math
class BodyMassIndex(SmartFormula):

    def formulas(self):
        return [
                 "heightM      = heightInches * 0.0254",
                 "heightM      = ?",  # use Newton-Raphson solver.  
                 "heightInches = ?",
                 "weightKg     = weightLb / 2.20462",
                 "weightKg     = heightM**2*bmi",
                 "weightLb     = ?",
                 "bmi          = weightKg / (heightM**2)"
               ]

Это позволяет вам получить / использовать термины, для которых формула расчета явно не указана в списке (например, heightInches вычисляется из heightM, которая вычисляется из bmi и weightKg):

height = BodyMassIndex(bmi=42.45,weightKg=130).heightInches
print(height) # 68.8968097135968  (1.75 Meters)

Примечание. Формулы выражаются в виде текста и выполняются с использованием eval (), который может быть намного медленнее, чем другое решение.Кроме того, алгоритм Ньютона-Рафсона подходит для линейных уравнений, но имеет свои ограничения для кривых, которые имеют сочетание положительных и отрицательных наклонов.Например, мне пришлось включить формулу weightKg = heightM**2*bmi, потому что для получения weightKg на основе bmi = weightKg/(heightM**2) необходимо решить уравнение y = 1/x^2, с которым Ньютон-Рафсон, похоже, не может справиться.

Вот пример использования вашей исходной задачи:

class OP(SmartFormula):

    def formulas(self):
        return [
                  "a = b * c - d",
                  "b = ?",
                  "c = e/f",
                  "d = ?",
                  "e = ?",
                  "f = ?",
                  "result = a+b+c+d+e+f"
               ]

r = OP(b=1,d=3,e=45,f=9).result
print(r) # 65.0
f = OP(a=2,c=5,d=3,e=45,result=65).f
print(f) # 9.0


class ABCD(SmartFormula):
    def formulas(self) : return ["a=b+c*d","b=?","c=?","d=?"]

    @property
    def someProperty(self): return "Found it!"

abcd = ABCD(a=5,b=2,c=3)
print(abcd.d)            # 1.0
print(abcd.someProperty) # Found it!
print(abcd.moreToSolve)  # False
1 голос
/ 08 июня 2019

[Новый ответ в дополнение к предыдущему]

Мне показалось, что мой ответ становится слишком большим, поэтому я добавляю это улучшенное решение в отдельный.

Это базовый решатель алгебры для простых уравнений, который будет выводить оператор присваивания для другого члена входного уравнения:

Например:

solveFor("d","a=b+c/d") # --> 'd=c/(a-b)'

С помощью этой функции вы можете дополнительноулучшить класс SmartFormula, пытаясь использовать алгебру, прежде чем вернуться к Ньютон-Рафсону.Это обеспечит более надежные результаты, когда уравнение достаточно простое для функции solveFor ​​().

Функция solveFor ​​() может решить уравнение для любого члена, который встречается в формуле только один раз.Он будет «понимать» вычисления до тех пор, пока решаемые компоненты связаны только с основными операциями (+, -, *, /, **).Любая группа в скобках, которая не содержит целевой термин, будет обрабатываться «как есть» без дальнейшей интерпретации.Это позволяет вам помещать сложные функции / операторы в скобки, чтобы другие термины могли быть решены даже при наличии этих специальных вычислений.

import re
from itertools import accumulate

def findGroups(expression):
    levels = list(accumulate(int(c=="(")-int(c==")") for c in expression))
    groups = "".join([c,"\n"][lv==0] for c,lv in zip(expression,levels)).split("\n")
    groups = [ g+")" for g in groups if g ]
    return sorted(groups,key=len,reverse=True)

functionMap = [("sin","asin"),("cos","acos"),("tan","atan"),("log10","10**"),("exp","log")]
functionMap += [ (b,a) for a,b in functionMap ]

def solveFor(term,equation):
    equation = equation.replace(" ","").replace("**","†")
    termIn = re.compile(f"(^|\\W){term}($|\\W)")
    if len(termIn.findall(equation)) != 1: return None
    left,right = equation.split("=",1)
    if termIn.search(right): left,right = right,left

    groups = { f"#{i}#":group for i,group in enumerate(findGroups(left)) }
    for gid,group in groups.items(): left = left.replace(group,gid)
    termGroup = next((gid for gid,group in groups.items() if termIn.search(group)),"##" )

    def moveTerms(leftSide,rightSide,oper,invOper):
        keepLeft = None
        for i,x in enumerate(leftSide.split(oper)):
            if termGroup in x or termIn.search(x):
                keepLeft  = x; continue
            x = x or "0"
            if any(op in x for op in "+-*/"): x = "("+x+")"
            rightSide = invOper[i>0].replace("{r}",rightSide).replace("{x}",x)
        return keepLeft, rightSide

    def moveFunction(leftSide,rightSide,func,invFunc):
        fn = leftSide.split("#",1)[0]
        if fn.split(".")[-1] == func:
            return leftSide[len(fn):],fn.replace(func,invFunc)
        return leftSide,rightSide

    left,right = moveTerms(left,right,"+",["{r}-{x}"]*2)
    left,right = moveTerms(left,right,"-",["{x}-{r}","{r}+{x}"])  
    left,right = moveTerms(left,right,"*",["({r})/{x}"]*2)
    left,right = moveTerms(left,right,"/",["{x}/({r})","({r})*{x}"])  
    left,right = moveTerms(left,right,"†",["log({r})/log({x})","({r})†(1/{x})"])
    for func,invFunc in functionMap:
        left,right = moveFunction(left,right,func,f"{invFunc}({right})")
    for sqrFunc in ["math.sqrt","sqrt"]:
        left,right = moveFunction(left,right,sqrFunc,f"({right})**2")

    for gid,group in groups.items(): right = right.replace(gid,group)
    if left == termGroup:
        subEquation = groups[termGroup][1:-1]+"="+right
        return solveFor(term,subEquation)
    if left != term: return None
    solution = f"{left}={right}".replace("†","**")
    # expression clen-up
    solution = re.sub(r"(?<!\w)(0\-)","-",solution)
    solution = re.sub(r"1/\(1/(\w)\)",r"\g<1>",solution)
    solution = re.sub(r"\(\(([^\(]*)\)\)",r"(\g<1>)",solution)
    solution = re.sub(r"(?<!\w)\((\w*)\)",r"\g<1>",solution)
    return solution 

Пример использования:

solveFor("x","y=(a+b)*x-(math.sin(1.5)/322)")   # 'x=(y+(math.sin(1.5)/322))/(a+b)'
solveFor("a","q=(a**2+b**2)*(c-d)**2")          # 'a=(q/(c-d)**2-b**2)**(1/2)'
solveFor("a","c=(a**2+b**2)**(1/2)")            # 'a=(c**2-b**2)**(1/2)'    
solveFor("a","x=((a+b)*c-d)*(23+y)")            # 'a=(x/(23+y)+d)/c-b'
sa = solveFor("a","y=-sin((x)-sqrt(a))")        # 'a=(x-asin(-y))**2'
sx = solveFor("x",sa)                           # 'x=a**(1/2)+asin(-y)'
sy = solveFor("y",sx)                           # 'y=-sin(x-a**(1/2))' 

Обратите внимание, что вы, вероятно, можете найти гораздо лучшие «решатели» алгебры, это простое / наивное решение.

Вот улучшенная версия класса SmartFormula, в которой для решения проблемы используется методForFor ()решение алгебры перед возвратом к приближениям Ньютона-Рафсона:

class SmartFormula:

    def __init__(self, **kwargs):
        self.params        = kwargs
        self.precision     = 0.000001
        self.maxIterations = 10000
        self._formulas     = [(f.split("=",1)[0].strip(),f) for f in self.formulas()]
        terms = set(term for _,f in self._formulas for term in re.findall(r"\w+\(?",f) )
        terms = [ term for term in terms if "(" not in term and not term.isdigit() ]
        self._formulas    += [ (term,f"{term}=solve('{term}')") for term in terms]
        self(**kwargs)

    def __getattr__(self, name):       
        if name in self.params: return self.params[name]

    def __call__(self, **kwargs):
        self.params          = kwargs
        self.moreToSolve     = True
        self.params["solve"] = lambda n: self.autoSolve(n)
        self.resolve()
        return self.params.get(self._formulas[0][0],None)

    def resolve(self):
        while self.moreToSolve:
            self.moreToSolve = False
            for param,formula in self._formulas:
                if self.params.get(param,None) is not None: continue
                try: 
                    exec(formula,globals(),self.params)
                    if self.params.get(param,None) is not None:
                        self.moreToSolve = True
                except: pass

    def autoSolve(self, name):
        for resolver in [self.algebra, self.newtonRaphson]:
            for source,formula in self._formulas:
                if self.params.get(source,None) is None:
                    continue
                if not re.search(f"(^|\\W){name}($|\\W)",formula):
                    continue
                resolver(name,source,formula)
                if self.params.get(name,None) is not None:
                    return self.params[name]

    def algebra(self, name, source, formula):
        try:    exec(solveFor(name,formula),globals(),self.params)            
        except: pass

    def newtonRaphson(self, name, source,formula):
        simDict = self.params.copy()
        target  = self.params[source]
        value   = target
        for _ in range(self.maxIterations):                    
            simDict[name] = value
            try: exec(formula,globals(),simDict)
            except: break
            result        = simDict[source]
            resultDelta   = target-result
            if abs(resultDelta) < self.precision : 
                self.params[name] = round(value/self.precision/2)*self.precision*2
                return       
            value += value*resultDelta/result/2

Это позволило классу примера (BodyMassIndex) избежать спецификации вычисления "weightKg = heightM**2*bmi", потому что решатель алгебры может понять это.Усовершенствованный класс также устраняет необходимость указывать имена терминов для автоматического решения («term =?»).

import math
class BodyMassIndex(SmartFormula):

    def formulas(self):
        return [
                 "bmi      = weightKg / (heightM**2)",
                 "heightM  = heightInches * 0.0254",
                 "weightKg = weightLb / 2.20462"
               ]

bmi = BodyMassIndex()
print("bmi",bmi(heightM=1.75,weightKg=130)) # 42.44897959183673
print("weight",bmi.weightLb)                # 286.6006 (130 Kg)

bmi(bmi=42.45,weightKg=130)
print("height",bmi.heightInches) # 68.8968097135968  (1.75 Meters)

Для исходного вопроса это просто, как может быть:

class OP(SmartFormula):

    def formulas(self):
        return [
                  "result = a+b+c+d+e+f",
                  "a = b * c - d",
                  "c = e/f"
               ]

r = OP(b=1,d=3,e=45,f=9).result
print(r) # 65.0
f = OP(a=2,c=5,d=3,e=45,result=65).f
print(f) # 9.0        

Ньютон-Рафсон не использовался ни в одном из этих вычислений, потому что алгебра решает их в приоритетном порядке, прежде чем пробовать приближения

1 голос
/ 06 июня 2019

Просто предварительно вычислите пропущенные значения в __init__ (и поскольку вы знаете, что представляют собой 5 значений, будьте явными, а не пытайтесь сжать код с помощью kwargs):

# Note: Make all 6 keyword-only arguments
def __init__(self, *, a=None, b=None, c=None, d=None, e=None, f=None):
     if a is None:
         a = b * c - d
     if c is None:
         c = e / f

     self.sum = a + b + c + d + e + f

def calculate(self):
    return self.sum
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...