关系和可选参数的设计模式?

时间:2019-06-06 17:43:40

标签: python design-patterns

我要构建一个通过构造函数方法接受一系列输入的类,然后使用这些参数使用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 个答案:

答案 0 :(得分:1)

只需预先计算__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

答案 1 :(得分:1)

这似乎是 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词典中lambda的实现。

如果要在许多公式中使用此模式,则可能需要将样板代码集中在基类中。这将使新的计算类所需的工作减少为仅需实现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)"
               ]

这可以让您获取/使用列表中未明确说明其计算公式的术语(例如,根据heightM来计算的heightInches是根据bmi和weightKg来计算的):

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

注:公式以文本形式表示,并使用eval()执行,该方法可能比其他解决方案慢得多。此外,Newton-Raphson算法适用于线性方程,但对于正斜率和负斜率混合的曲线也有其局限性。例如,我必须包括weightKg = heightM**2*bmi公式,因为基于weightKg获得bmi = weightKg/(heightM**2)需要解决一个Newton-Raphson似乎无法处理的y = 1/x^2方程。 / em>

以下是使用您原始问题的示例:

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

答案 2 :(得分:1)

[补充上一个答案的新答案]

我觉得我的答案太大了,所以我将这一改进的解决方案添加到单独的解决方案中。

这是简单方程的基本代数求解器,它将为输入方程的不同项输出赋值语句:

例如:

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

使用此功能,可以在尝试转换为Newton-Raphson之前尝试使用代数来进一步改进SmartFormula类。当方程对于resolveFor()函数足够简单时,这将提供更可靠的结果。

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类的改进版本,该类使用solveFor()在尝试转换为牛顿-拉夫森近似值之前尝试代数求解:

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        

在这些计算中均未使用Newton-Raphson,因为代数在尝试逼近之前优先解决它们