Two Walrus Operators in one If Statement
Asked Answered
P

2

6

Is there a correct way to have two walrus operators in 1 if statement?

if (three:= i%3==0) and (five:= i%5 ==0):
    arr.append("FizzBuzz")
elif three:
    arr.append("Fizz")
elif five:
    arr.append("Buzz")
else:
    arr.append(str(i-1))

This example works for three but five will be "not defined".

Pheasant answered 13/1, 2022 at 15:3 Comment(5)
Logical operators short circuit. So here for example, <first condition> and <second condition>, if <first condition> is false, then` <second condition>` is never evaluatedLina
The important part here is the short-circuiting of and, not the if statement. Are you wondering about the specific case of this if, or the generic problem of using :=+and?Daimon
Easiest option would be to assign to the variables before your if/else block.Octans
Just to clear up what people mean by and "short-circuiting" - If first part of AND statement is False, rest is never evaluated because we can already tell what will be the result. Same is with or and True values - first True value is enough to tell that whole OR will be True. If you all variables calulated anyway, you need to create them beforehand.Ijssel
Interesting question, but please take @khelwood's advice. Even in this simple example I struggle to see how this would improve code clarity.Trilemma
D
2

The logical operator and evaluates its second operand only conditionally. There is no correct way to have a conditional assignment that is unconditionally needed.

Instead use the "binary" operator &, which evaluates its second operand unconditionally.

arr = []
for i in range(1, 25):
    #                        v force evaluation of both operands
    if (three := i % 3 == 0) & (five := i % 5 == 0):
        arr.append("FizzBuzz")
    elif three:
        arr.append("Fizz")
    elif five:
        arr.append("Buzz")
    else:
        arr.append(str(i))

print(arr)
# ['1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz', 'Buzz', '11', ...]

Correspondingly, one can use | as an unconditional variant of or. In addition, the "xor" operator ^ has no equivalent with conditional evaluation at all.

Notably, the binary operators evaluate booleans as purely boolean - for example, False | True is True not 1 – but may work differently for other types. To evaluate arbitrary values such as lists in a boolean context with binary operators, convert them to bool after assignment:

#  |~~~ force list to boolean ~~| | force evaluation of both operands
#  v    v~ walrus-assign list ~vv v
if bool(lines := list(some_file)) & ((today := datetime.today()) == 0):
   ...

Since assignment expressions require parentheses for proper precedence, the common problem of different precedence between logical (and, or) and binary (&, |, ^) operators is irrelevant here.

Daimon answered 13/1, 2022 at 15:28 Comment(1)
Since & is a bit operation and, this will only work with some result for the assignment of three and five that produce bits that overlap. True satisfies that but if the assigned values did not have matching bits there may be a surprise. bool(3&4) is False for example where bool(3 and 4) is True.Beck
B
2

The issue you are having is that five is only assigned if three is True in this statement because of short circuiting:

if (three:= i%3==0) and (five:= i%5 ==0)

so five commonly is not assigned causing either a NameError or using a non-current value.

You can force a True value by forming a non-empty tuple with the walrus assignment inside of it then using three and five as you expect after that tuple.

It is no prettier than assigning three and five prior to the if but this works:

arr=[]
for i in range(1,26):
    if (three:=i%3==0, five:=i%5==0) and three and five:
        arr.append(f"{i} FizzBuzz")
    elif three:
        arr.append(f"{i} Fizz")
    elif five:
        arr.append(f"{i} Buzz")
    else:
        arr.append(f"{i}")

 >>> arr
 ['1', '2', '3 Fizz', '4', '5 Buzz', '6 Fizz', '7', '8', '9 Fizz', '10 Buzz', '11', '12 Fizz', '13', '14', '15 FizzBuzz', '16', '17', '18 Fizz', '19', '20 Buzz', '21 Fizz', '22', '23', '24 Fizz', '25 Buzz']

Any non-empty tuple is True in Python. Forming it causes (three:=i%3==0, five:=i%5==0) to always be truthy and three and five to be assigned each time. Since that tuple is true, the rest of the expression has to be evaluated with the correct values of three and five.

Alternatively, use if all((three:=i%3==0, five:=i%5==0)): since the tuple is formed prior to testing its contents -- even though all short circuits; that would only happen after the tuple is formed.

Either of these forms allows easy refactoring into comprehensions:

arr=[f"{i} FizzBuzz" if three and five 
             else f"{i} Fizz" if three 
             else f"{i} Buzz" if five 
             else f"{i}" 
                 for i in range(1,26) if (three:=i%3==0, five:=i%5==0)]

Or,

arr=[f"{i} FizzBuzz" if all((three:=i%3==0, five:=i%5==0)) 
                     else f"{i} Fizz" if three 
                     else f"{i} Buzz" if five 
                     else f"{i}" for i in range(1,26)]

Beware of the construction if (three := i % 3 == 0) & (five := i % 5 == 0): if the result of each element is not boolean. You can get some unexpected failures:

>>> bool((x:=3) & (y:=4))
False
>>> bool((x:=3) and (y:=4))
True   

The only way to fix that is have bool applied to each:

>>> bool(x:=3) & bool(y:=4)
True

BTW, speaking of tuples, a shorter way to do a FizzBuzz type challenge in Python:

fb={(True,True):"{} FizzBuzz", 
    (True,False):"{} Fizz", 
    (False,True):"{} Buzz", 
    (False,False):"{}"}

arr=[fb[(i%3==0,i%5==0)].format(i) for i in range(1,26)]

And if you are looking for something new this type of problem is a natural for Python 3.10+ pattern matching:

arr=[]
for i in range(1,26):
    s=f"{i}"
    match (i%3==0,i%5==0):
        case (True, (True | False) as oth):
            s+=" FizzBuzz" if oth else " Fizz"
        
        case (False, True):
            s+=" Buzz"
            
    arr.append(s)   
Beck answered 13/1, 2022 at 15:23 Comment(0)
D
2

The logical operator and evaluates its second operand only conditionally. There is no correct way to have a conditional assignment that is unconditionally needed.

Instead use the "binary" operator &, which evaluates its second operand unconditionally.

arr = []
for i in range(1, 25):
    #                        v force evaluation of both operands
    if (three := i % 3 == 0) & (five := i % 5 == 0):
        arr.append("FizzBuzz")
    elif three:
        arr.append("Fizz")
    elif five:
        arr.append("Buzz")
    else:
        arr.append(str(i))

print(arr)
# ['1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz', 'Buzz', '11', ...]

Correspondingly, one can use | as an unconditional variant of or. In addition, the "xor" operator ^ has no equivalent with conditional evaluation at all.

Notably, the binary operators evaluate booleans as purely boolean - for example, False | True is True not 1 – but may work differently for other types. To evaluate arbitrary values such as lists in a boolean context with binary operators, convert them to bool after assignment:

#  |~~~ force list to boolean ~~| | force evaluation of both operands
#  v    v~ walrus-assign list ~vv v
if bool(lines := list(some_file)) & ((today := datetime.today()) == 0):
   ...

Since assignment expressions require parentheses for proper precedence, the common problem of different precedence between logical (and, or) and binary (&, |, ^) operators is irrelevant here.

Daimon answered 13/1, 2022 at 15:28 Comment(1)
Since & is a bit operation and, this will only work with some result for the assignment of three and five that produce bits that overlap. True satisfies that but if the assigned values did not have matching bits there may be a surprise. bool(3&4) is False for example where bool(3 and 4) is True.Beck

© 2022 - 2024 — McMap. All rights reserved.