As far as I can see, the or operator is documented to group left to right and isn’t documented to be chaining. So based on that, I would expect x or x or x to be equivalent to (x or x) or x. But it isn’t.
Ah, that was in 3.10 and it does happen in 3.12, interesting! To me the second behavior is more confusing because it should short-circuit inside the parentheses and then short-circuit the whole expression, so it only needs to evaluate __bool__ once.
In any case, the extra calls to __bool__ don’t mean that the logical result isn’t equivalent. And it seems like it might be dependent on the code, e.g. True or 1/0 or 1/0 doesn’t raise anything, so it’s not evaluating the latter 2 expressions.
I wonder though, which one should be the correct behavior? Is this documented somewhere? To me it’s more intuitive that bool(x) is evaluated twice in both cases.
from itertools import cycle
class WasAskedForTruthValueAnEvenNumberOfTimes:
def __init__(self):
self.truth = cycle((True, False))
def __bool__(self):
return next(self.truth)
def __str__(self):
return 'WasAskedForTruthValueAnEvenNumberOfTimes'
x = WasAskedForTruthValueAnEvenNumberOfTimes()
print(x or 2 or 3)
x = WasAskedForTruthValueAnEvenNumberOfTimes()
print((x or 2) or 3)
Broken objects are not too be taken into account when judging whether semantics are ok. Both behaviours are perfectly fine. You could argue that the evalute only one behavior is faster, so you might want to make a report about that.
Why do you call it “broken”? I see no justification for that, and I don’t think it is. And I think even for example a list could change its truth value between the two evaluations, if another thread clears it in between.
It’s broken because what is supposed to be a side effect free operation isn’t side effect free. Python makes assumptions about the behaviour of a few magic methods. This is one of them.
If an object is changed from a different thread, that’s a race condition then. That is always a danger in threaded code. Use locks.
If (x or x) were assigned to a variable, you wouldn’t expect __bool__ to be only called once, would you?
Not sure if this is explicitly mentioned somewhere. I would describe this as “common sense”. Similar to how reading an attribute shouldn’t have a side effect. Or calling __str__. Ofcourse, you can break these rules, but don’t be surprised if you get weird behavior.
Interesting. Though based on the documentation, I wouldn’t call that normal and would instead expect both cases to print twice.
(Btw I keep saying “based on the documentation” because from experience I expected x or x or x to print only once. Treating it as a “chain” and only evaluating the expressions and the truth values as far as needed. But I realized I don’t know the technical why, checked the docs, and now to me it looks like it should be equivalent to (x or x) or x and that that should check the truth value twice.)
Note that neither and nor or restrict the value and type they return to False and True, but rather return the last evaluated argument. This is sometimes useful, e.g., if s is a string that should be replaced by a default value if it is empty, the expression s or ‘foo’ yields the desired value. Because not has to create a new value, it returns a boolean value regardless of the type of its argument (for example, not ‘foo’ produces False rather than ‘’.)
And there are also some things about (),I think when we use (x or x) in expressions, does it first generate an x.
class X:
def __init__(self, val):
self.val = val
self.truth = 1
def __bool__(self):
print('val:', self.val)
return bool(self.truth)
x = X(0)
x.truth = 0
y = X(1)
z = X(2)
print((x or y or x or z).val)
print()
print(((x or y or x) or z).val)
print()
if x or y or x or z:
pass
print()
if (x or y or x) or z:
pass
print()
So, the brackets in (x or y) or z trigger an extra boolean evaluation in CPython 3.12. Is that the case? But this only seems to happen if those variables are instances of classes that define the __bool__ dunder. For builtin types it doesn’t seem to happen, for instance: