Avoiding re-computing the same expression in Python - python

In the following code, I use abs(v - i) three times on the same line. Is this expression computed three times when the code is run? Is there a way to avoid this without having to complicate the code?
x = sum(abs(v-i) if s == 1 else int((abs(v-i)*(abs(v-i)+1))/2) for v in list)

Is this expression computed three times when the code is run?
No, once or twice for every list value.
Is there a way to avoid this without having to complicate the code?
Depends on what you consider complicating the code.
You could use the idiom that even got optimized in Python 3.9:
x = sum(a if s == 1 else int((a*(a+1))/2)
for v in list_
for a in [abs(v-i)])
Or if your list values are ints, you could use math.comb:
x = sum(abs(v-i) if s == 1 else comb(abs(v-i)+1, 2) for v in list_)

While https://stackoverflow.com/a/70268402/1126841 regarding the assignment operator is correct, this is a case where I really dislike the assignment expression, as you have to hunt for where a is actually defined. I would probably ditch sum and accumulate the value in a for loop instead.
x = 0
for v in list_:
a = abs(v-i)
if s == 1:
x += a
else:
x += int(a*(a+1)/2)
However, since s never changes in the loop, I would refactor this into two separate loops chosen by the value of s, one of which can use sum without difficulty.
if s == 1:
x = sum(abs(v-i) for v in list_)
else:
x = 0
for v in list_:
a = abs(v-i)
x += int(a*(a+1)/2)

My answer only shows that it's possible to do what you want with a one-liner, but I would still advise to use a longer approach with an explicit if/else + caching the value, or using numpy arrays and masks.
You can use the walrus operator := to store the value as a variable. This line is equivalent to your original code and will only compute a = abs(v-i) once per loop instead of 1-2 times:
x = sum(a if ((a := abs(v-i)) is not None) and s == 1 else int(a*(a+1)/2) for v in list_)
The problem is that the walrus operator can only be used in a if check, so we need to add a check that's always true... It really doesn't help reading comprehension.
"Long" approach:
v = np.array(list_)
a = np.abs(v - i)
x = np.sum(a if s == 1 else np.int(a*(a+1)/2))```

Can't you just save abs(v-i) to a variable and then substitute in that variable?
I would create a variable called my_calc = abs(v-i) then use that name. This will clean up your code

Related

Python condensed if statements

I am experimenting with how to condense if statements within my code. I have a project I am working on that has several "if" statements (too many to keep track of) and want to figure out a way to condense them. Obviously this involves a for loop, but I am having trouble adding additional operations within this loop.
I came up with the following working example to demonstrate my issue:
num=6
if_options = [num==5, num==6]
for i in range(len(if_options)):
if if_options[i]:
print(num)
I want to add an additional piece to the code. This additional piece will execute an operation within the if statement. See following non-working example as a framework for what I am trying to accomplish:
num=6
if_options = [num==5, num==6]
operations = [num=num+1, num=num-1]
for i in range(len(if_options)):
if if_options[i]:
operations[i]
print(num)
For whatever reason, it will not execute the operation portion of the code and fails with a syntax error. It does not let me declare the command "num=num+1" (without quotes) within a list, however this declaration is necessary for executing the command. I feel like I am missing one little thing and it should be an easy fix. Thank you in advance!!
The problem here is that the operations are evaluated when you create the list of them. You want to write them as strings, and then eval/exec them in the loop. I will assume you also want the conditions evaluated in the loop.
num = 6
if_options = ['num==5', 'num==6']
operations = ['num=num+1', 'num=num-1']
for i in range(len(if_options)):
if eval(if_options[i]):
exec(operations[i])
print(num)
why not functions?
def check(inp):
#you can do some logic and type checking here
return type(inp)==int # for example, or return arguments to pass to the operatiins
def operation(inp2):
if inp2: # check if true or not empty, as an example
#do some operations
# and then you do something like
for x in input_of_things:
operation( check( x ) )
You could use lambda expressions too.
num = 6
if_option_checks = [lambda x: x == 5, lambda x: x == 6]
operations = [lambda x: x + 1, lambda x: x - 1]
for check, operation in zip(if_option_checks, operations):
if check(num):
num = operation(num)
Or you could use dictionaries and lambda expressions
num = 6
if_option_checks = {"add": lambda x: x == 5, "sub": lambda x: x == 6}
operations = {"add": lambda x: x + 1, "sub": lambda x: x - 1}
for key, check in if_option_checks.items():
if check(num):
num = operations[key](num)
Perhaps a switch statement structure could help.
First define a switch function:
def switch(v): yield lambda *c: v in c
Then use is in a one-iteration for loop that yields a case function for the switch value:
for case in switch(num):
if case(5):
num = num + 1
break
if case(6):
num = num - 1
break
...

Why doesn't this string append with a for loop work?

I want to make a for loop that checks if list d has elements from list ident and if they don't, I want to add them. When I run this code part by part it works, but I suspect my False statement is not working, because it just doesn't append.
import pandas as pd
ident=['bla','me','you']
d=['bla']
for x in range(len(ident)):
if ident[x] in d == False:
d.append(ident[x])
Here is how I want the output to be:
d=['bla','me','you']
There might be a duplicate for this that I can't find, but just to explain the problem:
Python comparison operators chain. That is why you can write 1 < 2 < 3 < 4 in Python and it will evaluate as (1 < 2) and (2 < 3) and (3 < 4).
By the same mechanism, when you write
if ident[x] in d == False:
it is broadly equivalent to
if (ident[x] in d) and (d == False):
Most of the time if you're tempted to write == False or == True, there's probably a better way to express the condition.
In this case, it should be:
if ident[x] not in d:
I understand that you didn't ask that. But let me try to optimize your code a bit. Because these two lists could have equal length and the search could be very long - up to O(n^2). So here are my small improvements that will allow you to make the full algorithm in O(n) time + fixing your error.
ident=['bla','me','you']
new_ident = dict(zip(ident, ident))
d=['bla']
for i in d:
if i not in new_ident: new_ident[i] = True
ident = new_ident.keys()
This should produce the expected output.
for x in range(len(ident)):
if ident[x] not in d:
d.append(ident[x])
merge = d + list(set(ident) - set(d)) + list(set(d) - set(ident))
it is different way

fixing a for loop in python where you don't use the variable

I have a 'for' loop where I need t iterate through a list but do not need the iterative variable named 'node'. What would be a more elegant way to do this?
for node in NODES:
if i % 2 == 0:
tex_pairs_to_swap_dict[NODES[i]] = NODES[i+1]
i += 1
Looks like this is a case of actually needing the index in NODES. You get this using range(len(NODES)) but range() also supports an optional parameter of step that would allow you step through this 2 at a time (note: you also have to include the start if you want the step):
for i in range(0, len(NODES), 2):
tex_pairs_to_swap_dict[NODES[i]] = NODES[i+1]
This assumes there is an even number of entries in the list and will raise an IndexError if it isn't.
Alternatively, if all you are trying to do is step through the list 2 at a time you could also use:
it = iter(NODES)
for k, v in zip(it, it):
tex_pairs_to_swap_dict[k] = v
This is equivalent to the above without creating the it variable:
for k, v in zip(*[iter(NODES)]*2):
tex_pairs_to_swap_dict[k] = v
This will silently ignore the last value in an odd sized list.
The most effective way to do this would be to use a range. I would recommend you do this:
for i in range(len(NODES)):
if i % 2 == 0:
tex_pairs_to_swap_dict[NODES[i]] = NODES[i+1]
You can use the enumerate function, which returns a tuple with the index and the item.
for i, _ in enumerate(NODES):
if i % 2 == 0:
tex_pairs_to_swap_dict[NODES[i]] = NODES[i+1]
In python you can define a for loop without a variable and still access the contents of that. For example:
x = [1,2,3]
for _ in x: print (_)
will provide you an output of:
1
2
3
You can also do this with dictionaries. For example:
x = {1:10, 2:20}
for _,__ in x.items(): print (_,__)
The output of this will be:
1 10
2 20
In summary, you can use _ as a variable and reference it. While you may think there is no variable defined, it is still a throwaway variable. More details about _ can be found in this post: What is the purpose of the single underscore "_" variable in Python?
Based on this, you can rewrite your code as follows:
for _ in NODES:
if i % 2 == 0:
tex_pairs_to_swap_dict[NODES[i]] = NODES[i+1]
i += 1
With this, you don't need to use the variable node

Python command working but why?

I have a simple code that goes like this in Python:
a = [1,2,3]
b = [2,4,6]
def union(a,b):
pos = 0
while pos < len(b):
n = b[pos]
if n in a is not 'True':
a = a
else:
a.append(n)
pos = pos +1
return a
print union(a,b)
As you can see, the first IF statement makes no sense. However, if I code it this way:
if n in a is 'True':
a.append(n)
it does not work. The first code segment changes a = [1,2,4,6] - only adding numbers from list 'b' that are not in list 'a' already. If I change the 'IF' snippet to "is 'True" as suggested, it does not work.
While this function does what I intended it to do, I feel it is not clean and I have no idea why "if n in a is 'True':" would not behave equal to the else part of the "if n in a is not 'True':" function.
Can somebody please help me understand this?
It is not a very pythonic way to use boolean check and then compare it with a string, so it would be better to do it this way:
a = [1,2,3]
b = [2,4,6]
def union(x,y):
for v in y:
if v not in x:
x.append(v)
return x
print union(a,b)
OR:
a.extend(set(b).difference(set(a)))
print a
>>> [1, 2, 3, 4, 6]
OR in case you don't care about new objects creating than:
print list(set(a).union(b))
in and is/is not are both relational operators, and in Python relational operators are chained. Therefore n in a is not 'True' is equivalent to n in a and a is not 'True', and n in a is 'True' is equivalent to n in a and a is 'True'. Clearly these are not negations of each other since they both have n in a.
But don't use is unless you know you need it, and never compare against a boolean either (unless yadda yadda).
You should just use True not the string 'True'
or better yet, just
if n not in a:
a.append(n)
If you are a beginner, you may not realise that Python has a builtin type called set
set objects already have methods for intersection/union etc.
You can use
if n in a
or
if n not in a
instead of the is 'True'.

Expanding elements in a list

I'm looking for a "nice" way to process a list where some elements need to be expanded into more elements (only once, no expansion on the results).
Standard iterative way would be to do:
i=0
while i < len(l):
if needs_expanding(l[i]):
new_is = expand(l[i])
l[i:i] = new_is
i += len(new_is)
else:
i += 1
which is pretty ugly. I could rewrite the contents into a new list with:
nl = []
for x in l:
if needs_expanding(x):
nl += expand(x)
else:
nl.append(x)
But they both seem too long. Or I could simply do 2 passes and flatten the list later:
flatten(expand(x) if needs_expanding(x) else x for x in l)
# or
def try_expanding(x)....
flatten(try_expanding(x) for x in l)
but this doesn't feel "right" either.
Are there any other clear ways of doing this?
Your last two answers are what I would do. I'm not familiar with flatten() though, but if you have such a function then that looks ideal. You can also use the built-in sum():
sum(expand(x) if needs_expanding(x) else [x] for x in l, [])
sum(needs_expanding(x) and expand(x) or [x] for x in l, [])
If you do not need random access in the list you are generating, you could also use write a generator.
def iter_new_list(old_list):
for x in old_list:
if needs_expanding(x):
for y in expand(x):
yield y
else:
yield x
new_list = list(iter_new_list(old_list))
This is functionally equivalent to your second example, but it might be more readable in your real-world situation.
Also, Python coding standards forbid the use of lowercase-L as a variable name, as it is nearly indistinguishable from the numeral one.
The last one is probably your most pythonic, but you could try an implied loop (or in py3, generator) with map:
flatten(map(lambda x: expand(x) if needs_expanding(x) else x, l))
flatten(map(try_expanding, l))

Categories

Resources