Nested if's - what's more Pythonic? - python

Both functions do the same thing.
def function1(self):
a = self.get_a()
b = self.get_b()
c = self.get_c()
r = None
if a:
r = a
if b:
r = b
if c:
r = c
else:
print("c not set.")
else:
print("b not set.")
else:
print("a not set.")
return r
def function2(self):
a = self.get_a()
b = self.get_b()
c = self.get_c()
r = None
if not a:
print("a not set.")
return r
r = a
if not b:
print("b not set.")
return r
r = b
if not c:
print("c not set.")
r = c
return r
function1() creates very long lines the more if's are nested which conflicts with PEP8's line-length limit of 78.
function2() might be harder to read/understand and has more return statements. Line length is no problem here.
Which one is more pythonic?

One of the principals of Pythonic code is "flat is better than nested". On this basis, I'll say function2() is objectively more Pythonic. This can be seen in PEP-20: The Zen of Python:
The Zen of Python
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
This can be seen by typing import this inside the Python interpreter.

As #Will's answer suggests, flat is better. However the code doesn't look very pretty anyways. How about a more compact type of code?
looking at these quotes from #Will's answer:
Readability counts.
Beautiful is better than ugly.
from collections import OrderedDict
def function3():
my_dictionary=OrderedDict()
my_dictionary['a'] = self.get_a()
my_dictionary['b'] = self.get_b()
my_dictionary['c'] = self.get_c()
# ...
r = None
for name in my_dictionary.keys():
value = my_dictionary[name]
if not value:
print("%s not set." % name)
return r
r = value
return r
Surely this can be improved even more

You can use the evaluation rules of the and and or operators, for example:
>>> None or 4 or None or 5
4
>>> 4 and 5
5
So you'd have something like:
def function3(self):
a = self.get_a()
b = self.get_b()
c = self.get_c()
return (a and b and c) or (a and b) or a or None
And I'd recommend factoring out you I/O from your logical code.

I suggest function_4 displayed below together with the questions (non-idetnically working!) functions and one of DomTomCat's answer:
#! /usr/bin/env python
from __future__ import print_function
from collections import OrderedDict # Only used in function_3
def function_4(self):
"""Iterate over call results in FIFO on False or if sequence
exhausted, return None or previous value if that evaluates to true."""
functors = (
self.get_a,
self.get_b,
self.get_c,
)
request_targets = (
'a',
'b',
'c',
)
response_value = None
for functor, request_target in zip(functors, request_targets):
current_response = functor()
if not current_response:
print(request_target, "not set.")
return response_value
else:
response_value = current_response
return response_value
class Foo(object):
"""Mock the thingy ..."""
def __init__(self, a, b, c):
self._a, self._b, self._c = a, b, c
def __repr__(self):
return (
"Foo(" + str(self._a) + ", " + str(self._b) + ", " +
str(self._c) + ")")
def get_a(self):
return self._a
def get_b(self):
return self._b
def get_c(self):
return self._c
def function_1(self):
a = self.get_a()
b = self.get_b()
c = self.get_c()
r = None
if a:
r = a
if b:
r = b
if c:
r = c
else:
print("c not set.")
else:
print("b not set.")
else:
print("a not set.")
return r
def function_2(self):
a = self.get_a()
b = self.get_b()
c = self.get_c()
r = None
if not a:
print("a not set.")
return r
r = a
if not b:
print("b not set.")
return r
r = b
if not c:
print("c not set.")
r = c
return r
def function_3(self):
my_dictionary = OrderedDict()
my_dictionary['a'] = self.get_a()
my_dictionary['b'] = self.get_b()
my_dictionary['c'] = self.get_c()
# ...
r = None
for name in my_dictionary.keys():
value = my_dictionary[name]
if not value:
print("%s not set." % name)
return r
r = value
def main():
""""Drive the investigation."""
fixtures = (
(1, 42, 3.1415),
(0, 42, 3.1415),
(1, 0, 3.1415),
(1, 42, 0),
)
functors = (
function_1,
function_2,
function_3,
function_4,
)
for fixture in fixtures:
foo = Foo(*fixture)
print("\nFixture:", foo)
for i, functor in enumerate(functors, start=1):
print("Functor[%d]:" % (i,))
print(functor(foo))
if __name__ == '__main__':
main()
On my machine the fixtures produce the following behaviour/output when being called:
Fixture: Foo(1, 42, 3.1415)
Functor[1]:
3.1415
Functor[2]:
3.1415
Functor[3]:
None
Functor[4]:
3.1415
Fixture: Foo(0, 42, 3.1415)
Functor[1]:
a not set.
None
Functor[2]:
a not set.
None
Functor[3]:
a not set.
None
Functor[4]:
a not set.
None
Fixture: Foo(1, 0, 3.1415)
Functor[1]:
b not set.
1
Functor[2]:
b not set.
1
Functor[3]:
b not set.
1
Functor[4]:
b not set.
1
Fixture: Foo(1, 42, 0)
Functor[1]:
c not set.
42
Functor[2]:
c not set.
0
Functor[3]:
c not set.
42
Functor[4]:
c not set.
42
[Finished in 0.0s]

Here is what I would do without removing the print statements
def function1(self):
a = self.get_a()
b = self.get_b()
c = self.get_c()
r = None
inputValues = [a, b, c]
setValues = [i for i in inputValues if i]
for index, value in inputValues:
if len(setValues) <= index or setValues[index] != value:
print(f'{value} is not set.')
else:
r = value
return r
The function2 looks good enough to go.

Related

Is there a simpler way to change how a function works depending on inputs?

I have a function that I want to do/output something different depending on what inputs I give it. I've made something useful with if else logic but not elegant and I suspect there might be a shorter/more elegant way of doing it. What are the alternatives to the if else on None methods I've made?
e.g.,
def foo (a=None, b=None, c=None):
if a is None:
raise ValueError("a cannot be None!")
elif all([b is None, c is None]):
x = operations
return x
elif c is None:
x = operations
y = operations
return x, y
elif b is None:
x = operations
z = operations
return x, z
elif all([b is not None, c is not None]):
x = operations
y = operations
z = operations
return x, y, z
It depends on what exactly you mean by "better" and what you are trying to do inside your ifs and elifs but I myself am not a big fan of the code style in your example either. It can become hard to read and maintain.
There are languages that make this kind of thing easy, but unfortunately Python is not one of them. However, there are a few ways that I think are "better", depending on what you are trying to do.
Lots of elifs:
def show_food_description(food):
if food == "apple":
print("It's red and sweet")
elif food == "banana":
print("It's long and yellow")
elif food == "kiwi":
print("It's green and hairy")
else:
print("This food is unknown")
alternative:
def show_food_description(food):
descriptions = {
"apple": "It's red and sweet",
"banana": "It's long and yellow",
"kiwi": "It's green and hairy"
}
print(descriptions.get(food, "This food is unknown"))
Testing for equality with more than one value:
food = "apple"
if food == "apple" or food == "banana" or food == "kiwi":
print("It's a fruit!")
alternative:
food = "apple"
fruits = ["apple", "banana", "kiwi"]
if food in fruits:
print("It's a fruit!")
Execute different function
def add_one(x):
return x + 1
def add_two(x):
return x + 2
def add_three(x):
return x + 3
def perform_operation(x, operation):
if operation == "add_one":
return add_one(x)
if operation == "add_two":
return add_two(x)
if operation == "add_three":
return add_three(x)
else:
raise Exception("Unknown operation")
alternative:
def add_one(x):
return x + 1
def add_two(x):
return x + 2
def add_three(x):
return x + 3
def unknown_operation(x):
raise Exception("Unknown operation")
def perform_operation(x, operation):
operations = {
"add_one": add_one,
"add_two": add_two,
"add_three": add_three
}
selected_operation = operations.get(operation, unknown_operation)
return selected_operation(x)
It is also worth mentioning that in Python 3.10, there is a match statement available that is similar to switch statements in other languages. Here is an example on how to use it:
language = "Python"
match language:
case "C": print("It's C")
case "Python": print("It's Python")
case _: print("It doesn't matter")
If you have Python 3.10 installed you can use the match-case.
Please refer to PEP 634
Honestly code readability is very subjective, if you were looking for something like a switch-case logic in python, Your code would look like -
def foo (a=None, b=None, c=None):
match a, b, c:
# Add all your cases
case None, None, _:
# Do Your Operations and Return
return
case other:
print("Other Cases")
This is one of the best features that got released with 3.10. Hope this helps you.
If I understand your problem, you might find the new pattern matching syntax to be what you want (requires python >= 3.10, spec in PEP634, tutorial in PEP636).
def foo(a=None, b=None, c=None):
match a, b, c:
case a_, None, None:
r = 1 * a_
case None, b_, None:
r = 2 * b_
# add more patterns here
case _:
r = None
return r
foo(a=1) # 1
foo(b=5) # 10
foo(a=0, b=0) # None # no pattern for a and b
foo(c=8) # None # no pattern for c
NB. the same is achievable with if/elif/else.
def bar(a=None, b=None, c=None):
if a is not None and b is None and c is None:
r = 1 * a
elif a is None and b is not None and c is None:
r = 2 * b
# add more elifs here
else:
r = None
return r

Remove unused variables in Python source code

The Question
Is there a straightforward algorithm for figuring out if a variable is "used" within a given scope?
In a Python AST, I want to remove all assignments to variables that are not otherwise used anywhere, within a given scope.
Details
Motivating example
In the following code, it is obvious to me (a human), that _hy_anon_var_1 is unused, and therefore the _hy_anon_var_1 = None statements can be removed without changing the result:
# Before
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
_hy_anon_var_1 = None
else:
n = 3 * n + 1
_hy_anon_var_1 = None
yield n
# After
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
else:
n = 3 * n + 1
yield n
Bonus version
Extend this to []-lookups with string literals as keys.
In this example, I would expect _hyx_letXUffffX25['x'] to be eliminated as unused, because _hyx_letXUffffX25 is local to h, so _hyx_letXUffffX25['x'] is essentially the same thing as a local variable. I would then expect _hyx_letXUffffX25 itself to be eliminated once there are no more references to it.
# Before
def h():
_hyx_letXUffffX25 = {}
_hyx_letXUffffX25['x'] = 5
return 3
# After
def h():
return 3
From what I can tell, this is somewhat of an edge case, and I think the basic algorithmic problem is the same.
Definition of "used"
Assume that no dynamic name lookups are used in the code.
A name is used if any of these are true in a given scope:
It is referenced anywhere in an expression. Examples include: an expression in a return statement, an expression on the right-hand side of an assignment statement, a default argument in a function definition, being referenced inside a local function definition, etc.
It is referenced on the left-hand side of an "augmented assignment" statement, i.e. it is an augtarget therein. This might represent "useless work" in a lot of programs, but for the purpose of this task that's OK and distinct from being an entirely unused name.
It is nonlocal or global. These might be useless nonlocals or globals, but because they reach beyond the given scope, it is OK for my purposes to assume that they are "used".
Please let me know in the comments if this seems incorrect, or if you think I am missing something.
Examples of "used" and "unused"
Example 1: unused
Variable i in f is unused:
def f():
i = 0
return 5
Example 2: unused
Variable x in f is unused:
def f():
def g(x):
return x/5
x = 10
return g(100)
The name x does appear in g, but the variable x in g is local to g. It shadows the variable x created in f, but the two x names are not the same variable.
Variation
If g has no parameter x, then x is in fact used:
def f():
x = 10
def g():
return x/5
return g(100)
Example 3: used
Variable i in f is used:
def f():
i = 0
return i
Example 4: used
Variable accum in silly_map and silly_sum is used in both examples:
def silly_map(func, data):
data = iter(data)
accum = []
def _impl():
try:
value = next(data)
except StopIteration:
return accum
else:
accum.append(value)
return _impl()
return _impl()
def silly_any(func, data):
data = iter(data)
accum = False
def _impl():
nonlocal accum, data
try:
value = next(data)
except StopIteration:
return accum
else:
if value:
data = []
accum = True
else:
return _impl()
return _impl()
The solution below works in two parts. First, the syntax tree of the source is traversed and all unused target assignment statements are discovered. Second, the tree is traversed again via a custom ast.NodeTransformer class, which removes these offending assignment statements. The process is repeated until all unused assignment statements are removed. Once this is finished, the final source is written out.
The ast traverser class:
import ast, itertools, collections as cl
class AssgnCheck:
def __init__(self, scopes = None):
self.scopes = scopes or cl.defaultdict(list)
#classmethod
def eq_ast(cls, a1, a2):
#check that two `ast`s are the same
if type(a1) != type(a2):
return False
if isinstance(a1, list):
return all(cls.eq_ast(*i) for i in itertools.zip_longest(a1, a2))
if not isinstance(a1, ast.AST):
return a1 == a2
return all(cls.eq_ast(getattr(a1, i, None), getattr(a2, i, None))
for i in set(a1._fields)|set(a2._fields) if i != 'ctx')
def check_exist(self, t_ast, s_path):
#traverse the scope stack and remove scope assignments that are discovered in the `ast`
s_scopes = []
for _ast in t_ast:
for sid in s_path[::-1]:
s_scopes.extend(found:=[b for _, b in self.scopes[sid] if AssgnCheck.eq_ast(_ast, b) and \
all(not AssgnCheck.eq_ast(j, b) for j in s_scopes)])
self.scopes[sid] = [(a, b) for a, b in self.scopes[sid] if b not in found]
def traverse(self, _ast, s_path = [1]):
#walk the ast object itself
_t_ast = None
if isinstance(_ast, ast.Assign): #if assignment statement, add ast object to current scope
self.traverse(_ast.targets[0], s_path)
self.scopes[s_path[-1]].append((True, _ast.targets[0]))
_ast = _ast.value
if isinstance(_ast, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
s_path = [*s_path, (nid:=(1 if not self.scopes else max(self.scopes)+1))]
if isinstance(_ast, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.scopes[nid].extend([(False, ast.Name(i.arg)) for i in _ast.args.args])
_t_ast = [*_ast.args.defaults, *_ast.body]
self.check_exist(_t_ast if _t_ast is not None else [_ast], s_path) #determine if any assignment statement targets have previously defined names
if _t_ast is None:
for _b in _ast._fields:
if isinstance((b:=getattr(_ast, _b)), list):
for i in b:
self.traverse(i, s_path)
elif isinstance(b, ast.AST):
self.traverse(b, s_path)
else:
for _ast in _t_ast:
self.traverse(_ast, s_path)
Putting it all together:
class Visit(ast.NodeTransformer):
def __init__(self, asgn):
super().__init__()
self.asgn = asgn
def visit_Assign(self, node):
#remove assignment nodes marked as unused
if any(node.targets[0] == i for i in self.asgn):
return None
return node
def remove_assgn(f_name):
tree = ast.parse(open(f_name).read())
while True:
r = AssgnCheck()
r.traverse(tree)
if not (k:=[j for b in r.scopes.values() for k, j in b if k]):
break
v = Visit(k)
tree = v.visit(tree)
return ast.unparse(tree)
print(remove_assgn('test_name_assign.py'))
Output Samples
Contents of test_name_assign.py:
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
_hy_anon_var_1 = None
else:
n = 3 * n + 1
_hy_anon_var_1 = None
yield n
Output:
def hailstone_sequence(n: int) -> Iterable[int]:
while n != 1:
if 0 == n % 2:
n //= 2
else:
n = 3 * n + 1
yield n
Contents of test_name_assign.py:
def h():
_hyx_letXUffffX25 = {}
_hyx_letXUffffX25['x'] = 5
return 3
Output:
def h():
return 3
Contents of test_name_assign.py:
def f():
i = 0
return 5
Output:
def f():
return 5
Contents of test_name_assign.py:
def f():
x = 10
def g():
return x/5
return g(100)
Ouptut:
def f():
x = 10
def g():
return x / 5
return g(100)

Python - create objects of a class without repeating myself while creating

I am new to python and i have a quick question.
How can I avoid repeating my self when I declare the Class instances x1 x2 ..
I tried it with a list but then I wasn't able to create a file for each object after.
And not all parameters are the same for my objects, d[0] is counting up.
Any smart idea to get rid of repeating myself here?
thanks in advance
class TestClass(object):
def __init__(self, a, b, c: int):
self.a = a
self.b = b
self.c = c
def __str__(self):
return f" a= {self.a} b = {self.b} c = {self.c}"
def func1():
a = input("a: ")
b = input("b: ")
return a, b
def func2():
return 100, 90, 80, 70
c = func1()
d = func2()
x1 = TestClass(c[0], c[1], d[0])
x2 = TestClass(c[0], c[1], d[1])
x3 = TestClass(c[0], c[1], d[2])
x4 = TestClass(c[0], c[1], d[3])
h = {"a": x1,"b": x2, "c": x3, "d": x4}
for key, value in h.items():
with open(f"Name {key}.txt","w") as f:
f.write(str(value))
OUTPUT:
#a: Anton
#b: Bernd
#
# four files Name a - d.txt were created
# file 1: a= Anton b = Bernd c = 100
# file 2: a= Anton b = Bernd c = 90
# file 3: a= Anton b = Bernd c = 80
# file 4: a= Anton b = Bernd c = 70
You should iterate on the return value (tuple) of the func2 function (so on the d variable) with the enumerate function. The enumerate function returns the value and the related index of the iterator (Eg.: https://realpython.com/python-enumerate/). Then you can add the element for your (empty) dict. You should use the chr function to define the letters based on the index. The lowercase a is the 97.
Related code part:
c = func1()
d = func2()
h = {}
for idx, value in enumerate(d):
h[chr(97 + idx)] = TestClass(c[0], c[1], d[idx])
for key, value in h.items():
with open(f"Name {key}.txt", "w") as f:
f.write(str(value))
NOTE:
I have written a more compact version of code. You can check it if you are interested in it.
Code:
class TestClass(object):
def __init__(self, a, b, c: int):
self.a = a
self.b = b
self.c = c
def __str__(self):
return f" a= {self.a} b = {self.b} c = {self.c}"
a, b, h, d = input("a: "), input("b: "), {}, [100, 90, 80, 70]
result = [(chr(97 + idx), TestClass(a, b, d[idx])) for idx, value in enumerate(d)]
for item in result:
with open(f"Name {item[0]}.txt", "w") as f:
f.write(str(item[1]))
Quick answer
Use a Function, when you need to do something that's going to take you a lot of Typing or you need to do something repeatedly then pack it into a function.
def create_func(fun_1, fun_2):
result = {}
acii_n = 97
for item in fun_2:
name = chr(acii_n)
acii_n += 1
class_instance = TestClass(fun_1[0], fun_1[1], item)
result.setdefault(name, class_instance)
return result
h = create_func(c, d)
for key, value in h.items():
with open(f"Name {key}.txt","w") as f:
f.write(str(value))
chr(i) Function. You can see that I call the function starting at int 97.
That's because the ASCII value is the letter a --> asciitable.com.
Additional improvements
Funny enough the solution I gave, which is use a function, is also the exact opposite that I can suggest you to do for improve your script, which is remove the functions :).
class TestClass(object):
def __init__(self, a, b, c: int):
self.a = a
self.b = b
self.c = c
def __str__(self):
return f" a= {self.a} b = {self.b} c = {self.c}"
def create_instances(fun_2):
a = input("a: ")
b = input("b: ")
user_values = [a, b]
result = {}
ascii_n = 97
for item in fun_2:
name = chr(ascii_n)
ascii_n += 1 # Step on the next charactes
class_instance = TestClass(user_values[0], user_values[1], item)
result.setdefault(name, class_instance)
return result
int_values = [100, 90, 80, 70] # Just pack it into a list
all_instances = create_instances(int_values)
for key, value in all_instances.items():
with open(f"Name {key}.txt","w") as f:
f.write(str(value))
Using a Dictionary Comprehension
Very Powerful Tool, fast (can run Faster the For loops) and super Pythonic :) Python Dictionary Comprehension.
class TestClass(object):
def __init__(self, a, b, c: int):
self.a = a
self.b = b
self.c = c
def __str__(self):
return f" a= {self.a} b = {self.b} c = {self.c}"
int_values = [100, 90, 80, 70]
a = 'Python'
b = 'WOOW'
user_values = [a, b]
ascii_n = 97
result = {chr(ascii_n+idx): TestClass(user_values[0], user_values[1], item) for idx, item in enumerate(int_values)}
for key, value in result.items():
with open(f"Name {key}.txt","w") as f:
f.write(str(value))

Design pattern for relational and optional parameters?

I am to build a class that accepts a series of inputs via the constructor method, then perform a calculation with calculate() using these parameters. The trick here is that these parameters might be available sometimes and other times might not. There however, is a given equation between the variables, such that the missing ones can be calculated from the equations. Here is an example:
I know that:
a = b * c - d
c = e/f
I am to calculate always a+b+c+d+e+f
Here is what I have so far:
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)
then use as:
c = Calculation(e=4,f=6,b=7,d=2)
c.calculate()
however, some other time might have other variables like:
c = Calculation(b=5,c=6,d=7,e=3,f=6)
c.calculate()
My question is: What would be a good design pattern to use in my case? So far, it seems a bit redundant to make a #property for all variables. The problem it must solve is to accept any variables (minimum for which calculation is possible) and based on the equation I have, figure out the rest, needed for calculation.
This seems like a good candidate for the getattr function. You can store the keyword arguments directly in the class and use that dictionary to either return a known parameter as attribute or infer an unspecified value "on the fly" based on other formulas that you know of:
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)
Note that, if you have very complicated calculations for some of the parameters, you can use functions of the class as the implementation of the lambdas in the self.inferred dictionary.
If you're going to use this pattern for many formulas, you might want to centralize the boilerplate code in a base class. This will reduce the work needed for new calculation classes to only having to implement the inferred() function.:
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
}
With enough content in inferred(), you can even use this approach to obtain any value from a combination of the others:
valueF = Calculation(a=2,b=1,c=5,d=3,e=45,result=65).f
print(valueF) # 9.0
EDIT
If you want to make this even more sophisticated, you can improve getattr to allow specification of dependencies in the inferred() dictionary.
For example:
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
A similar class could be designed to process formulas expressed as text. This would allow a basic form of term solver using a newton-raphson iterative approximation (at least for 1 degree polynomial equations):
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
With this approach the BodyMassIndex calculator would be easier to read:
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)"
]
This lets you obtain/use terms for which the calculation formula is not explicitly stated in the list (e.g. heightInches computed from heightM which is computed from bmi and weightKg):
height = BodyMassIndex(bmi=42.45,weightKg=130).heightInches
print(height) # 68.8968097135968 (1.75 Meters)
Note: The formulas are expressed as text and executed using eval() which may be much slower than the other solution. Also, the Newton-Raphson algorithm is OK for linear equations but has its limitations for curves that have a mix of positive and negative slopes. For example, I had to include the weightKg = heightM**2*bmi formula because obtaining weightKg based on bmi = weightKg/(heightM**2) needs to solve a y = 1/x^2 equation which Newton-Raphson can't seem to handle.
Here's an example using your original problem:
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
Just precompute the missing values in __init__ (and since you know what the 5 values are, be explicit rather than trying to compress the code using 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
[New Answer to complement previous one]
I felt my answer was getting too big so I'm adding this improved solution in a separate one.
This is a basic algebra solver to for simple equations that will output an assignment statement for a different term of the input equation:
For example:
solveFor("d","a=b+c/d") # --> 'd=c/(a-b)'
With this function, you can further improve the SmartFormula class by attempting to use algebra before reverting to Newton-Raphson. This will provide more reliable results when the equation is simple enough for the solveFor() function.
The solveFor() function can solve the equation for any term that appears only once in the formula. It will "understand" the calculation as long as the components to solve are only related with basic operations (+, -, *, /, **). Any group in parentheses that does not contain the target term will be processed "as is" without being further interpreted. This allows you to place complex functions/operators in parentheses so that other terms can be solved even in the presence of these special calculations.
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
Example Uses:
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))'
Note that you can probably find much better algebra 'solvers' out there, this is just a simple/naive solution.
Here is an improved version of the SmartFormula class that uses solveFor() to attempt an algebra solution before reverting to Newton-Raphson approximations:
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
This allowed the example class (BodyMassIndex) to avoid specification of the "weightKg = heightM**2*bmi" calculation because the algebra solver can figure it out. The improved class also eliminates the need to indicate auto-solving term names ("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)
For the original question, this is simple as can be:
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 was not used in any of these calculations because the algebra solves them in priority before trying the approximations

Calculator Dependency Tree Python (sympy / numpy)

I would like to have users enter specific values and then the system computes numerous results based on what these - My program is getting very complicated with just a few functions. I have included an example with 3 simple functions and 6 variables with the following relationships:
The Code I have is as follows:
class MyCalculator:
def __init__(self):
self.a = None
self.b = None
self.c = None
self.d = None
self.e = None
self.f = None
def set(self, field, val):
if field == "a": self.a = val
if field == "b": self.b = val
if field == "c": self.c = val
if field == "d": self.d = val
if field == "e": self.e = val
for i in range(10): # circle round a few times to ensure everything has computed
if self.a and self.b:
self.c = self.a * self.b
if self.a and self.c:
self.b = self.c / self.a
if self.b and self.c:
self.a = self.c / self.b
if self.b and self.d:
self.e = self.b + self.d
if self.e and self.b:
self.d = self.e - self.b
if self.e and self.d:
self.b = self.e - self.d
if self.c and self.e:
self.f = self.c / self.e
if self.f and self.e:
self.e = self.f * self.e
if self.f and self.c:
self.e = self.c / self.f
def status(self):
print(f"a = {self.a} b = {self.b} c = {self.c} d = {self.d} e = {self.e} f = {self.f} ")
Then If i run the following code:
example1 = MyCalculator()
example1.set("a", 5)
example1.set("e", 7)
example1.set("c", 2)
example1.status()
This will print out a = 5.0 b = 0.40000000000000036 c = 2.0000000000000018 d = 6.6 e = 7.0 f = 0.285714285714286
I would like a much simpler way to achieve the same result using something like sympy and numpy but so far I cant find anything that will work
There's a live version of this solution online you can try for yourself
Here's a complete solution that uses Sympy. All you need to do is enter your desired expressions in the exprStr tuple at the top of the MyCalculator definition, and then all of the dependency satisfaction stuff should take care of itself:
from sympy import S, solveset, Symbol
from sympy.parsing.sympy_parser import parse_expr
class MyCalculator:
# sympy assumes all expressions are set equal to zero
exprStr = (
'a*b - c',
'b + d - e',
'c/e - f'
)
# parse the expression strings into actual expressions
expr = tuple(parse_expr(es) for es in exprStr)
# create a dictionary to lookup expressions based on the symbols they depend on
exprDep = {}
for e in expr:
for s in e.free_symbols:
exprDep.setdefault(s, set()).add(e)
# create a set of the used symbols for input validation
validSymb = set(exprDep.keys())
def __init__(self, usefloat=False):
"""usefloat: if set, store values as standard Python floats (instead of the Sympy numeric types)
"""
self.vals = {}
self.numify = float if usefloat else lambda x: x
def set(self, symb, val, _exclude=None):
# ensure that symb is a sympy Symbol object
if isinstance(symb, str): symb = Symbol(symb)
if symb not in self.validSymb:
raise ValueError("Invalid input symbol.\n"
"symb: %s, validSymb: %s" % (symb, self.validSymb))
# initialize the set of excluded expressions, if needed
if _exclude is None: _exclude = set()
# record the updated value of symb
self.vals[symb] = self.numify(val)
# loop over all of the expressions that depend on symb
for e in self.exprDep[symb]:
if e in _exclude:
# we've already calculated an update for e in an earlier recursion, skip it
continue
# mark that e should be skipped in future recursions
_exclude.add(e)
# determine the symbol and value of the next update (if any)
nextsymbval = self.calc(symb, e)
if nextsymbval is not None:
# there is another symbol to update, recursively call self.set
self.set(*nextsymbval, _exclude)
def calc(self, symb, e):
# find knowns and unknowns of the expression
known = [s for s in e.free_symbols if s in self.vals]
unknown = [s for s in e.free_symbols if s not in known]
if len(unknown) > 1:
# too many unknowns, can't do anything with this expression right now
return None
elif len(unknown) == 1:
# solve for the single unknown
nextsymb = unknown[0]
else:
# solve for the first known that isn't the symbol that was just changed
nextsymb = known[0] if known[0] != symb else known[1]
# do the actual solving
sol = solveset(e, nextsymb, domain=S.Reals)
# evaluate the solution given the known values, then return a tuple of (next-symbol, result)
return nextsymb, sol.subs(self.vals).args[0]
def __str__(self):
return ' '.join(sorted('{} = {}'.format(k,v) for k,v in self.vals.items()))
Testing it out:
mycalc = MyCalculator()
mycalc.set("a", 5)
mycalc.set("e", 7)
mycalc.set("c", 2)
print(mycalc)
Output:
a = 5 b = 2/5 c = 2 d = 33/5 e = 7 f = 2/7
One of the neat things about Sympy is that it uses rational math, which avoids any weird rounding errors in, for example, 2/7. If you'd prefer to get your results as standard Python float values, you can pass the usefloat flag to MyCalculator:
mycalc = MyCalculator(usefloat=True)
mycalc.set("a", 5)
mycalc.set("e", 7)
mycalc.set("c", 2)
print(mycalc)
Output:
a = 5.0 b = 0.4 c = 2.0 d = 6.6 e = 7.0 f = 0.2857142857142857
In [107]: a=2.
In [108]: a=5.
In [109]: b=0.4
In [110]: c=lambda: a*b
In [111]: d=6.6
In [112]: e=lambda: b+d
In [113]: f=lambda: c()/e()
In [114]: print(a,b,c(), d, e(), f())
5.0 0.4 2.0 6.6 7.0 0.2857142857142857
You can probably capture the above logic in a class.
It would be possible to hold 'variables' as _a, _b and _d. Then a(), b() and d() could be functions that return _a etc...
More a pointer than a whole answer but it may help.
Using a structure like that below it would be possible to create a situation where you always call a function and not need to know when to use a and c() but always use a() and c().
In [121]: def var(init=0.0):
...: def func(v=None):
...: nonlocal init
...: if v==None: return init
...: init=v
...: return func
...:
In [122]: a=var(100.)
In [123]: a()
Out[123]: 100.0
In [124]: a(25.)
In [125]: a()
Out[125]: 25.0

Categories

Resources