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



我要构建一个通过构造函数方法接受一系列输入的类,然后使用这些参数执行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似乎有点多余。它必须解决的问题是接受任何变量(可以计算的最小值),并根据我的方程,找出计算所需的其余变量。

这似乎是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.infered 字典中 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)

编辑2

可以设计一个类似的类来处理表示为文本的公式。 这将允许使用牛顿-拉夫森迭代近似的基本形式的项求解器(至少对于 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 计算的高度英寸,该高度由 bmi 和 weightKg 计算):

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

注意:公式表示为文本并使用 eval() 执行,这可能比其他解决方案慢得多。 此外,牛顿-拉夫森算法适用于线性方程,但对于具有正斜率和负斜率混合的曲线有其局限性。例如,我必须包括weightKg = heightM**2*bmi公式,因为根据bmi = weightKg/(heightM**2)获得weightKg需要解决牛顿-拉夫森似乎无法处理的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

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

[补充前一个的新答案]

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

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

例如:

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 类的改进版本,它使用 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        

牛顿-拉夫森没有用于任何这些计算,因为代数在尝试近似之前优先解决它们

最新更新