NOTE: Check GitHub - nineteendo/for-any-each for the latest version.
The problem
Recently, #118946 was merged, replacing any()
with a for loop:
-return any(key in m for m in self.maps)
+for mapping in self.maps:
+ if key in mapping:
+ return True
+return False
Why? Because any()
is slower in this case:
script
# for_all_any.py
def all_base(stop):
return all(value >= 0 for value in range(stop))
def any_base(stop):
return any(value < 0 for value in range(stop))
def all_true(stop):
return all(False for value in range(stop) if value < 0)
def any_true(stop):
return any(True for value in range(stop) if value < 0)
def all_loop(stop):
for value in range(stop):
if value < 0:
return False
return True
def any_loop(stop):
for value in range(stop):
if value < 0:
return True
return False
# for_all_any.sh
echo 1 item
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_base(1)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_base(1)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_true(1)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_true(1)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_loop(1)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_loop(1)"
echo 10 items
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_base(10)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_base(10)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_true(10)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_true(10)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_loop(10)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_loop(10)"
echo 100 items
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_base(100)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_base(100)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_true(100)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_true(100)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_loop(100)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_loop(100)"
echo 1000 items
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_base(1000)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_base(1000)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_true(1000)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_true(1000)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.all_loop(1000)"
main/python.exe -m timeit -s "import for_all_any" "for_all_any.any_loop(1000)"
2.56x slower for 1 item
1000000 loops, best of 5: 361 nsec per loop # all_base
1000000 loops, best of 5: 358 nsec per loop # any_base
1000000 loops, best of 5: 333 nsec per loop # all_true
1000000 loops, best of 5: 337 nsec per loop # any_true
2000000 loops, best of 5: 142 nsec per loop # all_loop
2000000 loops, best of 5: 141 nsec per loop # any_loop
2.30x slower for 10 items
10 items
500000 loops, best of 5: 650 nsec per loop # all_base
500000 loops, best of 5: 667 nsec per loop # any_base
500000 loops, best of 5: 480 nsec per loop # all_true
500000 loops, best of 5: 483 nsec per loop # any_true
1000000 loops, best of 5: 283 nsec per loop # all_loop
1000000 loops, best of 5: 283 nsec per loop # any_loop
2.12x slower for 100 items
100 items
100000 loops, best of 5: 3.41 usec per loop # all_base
100000 loops, best of 5: 3.43 usec per loop # any_base
200000 loops, best of 5: 1.77 usec per loop # all_true
200000 loops, best of 5: 1.78 usec per loop # any_true
200000 loops, best of 5: 1.61 usec per loop # all_loop
200000 loops, best of 5: 1.61 usec per loop # any_loop
1.68x slower for 1000 items
1000 items
5000 loops, best of 5: 40.5 usec per loop # all_base
5000 loops, best of 5: 40.9 usec per loop # any_base
10000 loops, best of 5: 23.8 usec per loop # all_true
10000 loops, best of 5: 24 usec per loop # any_true
10000 loops, best of 5: 24.3 usec per loop # all_loop
10000 loops, best of 5: 24.1 usec per loop # any_loop
Quoting the Zen of Python:
There should be one-- and preferably only one --obvious way to do it.
This is clearly not the case here, the more code you use the faster it gets.
Therefore, I propose a syntax for all()
& any()
with a loop, which the compiler can optimize:
result = value < 0 for any value in range(stop)
result = value >= 0 for every value in range(stop)
Syntax
comprehension ::= assignment_expression comp_for
comp_for ::= comp_unquantified_for | comp_quantified_for
comp_unquantified_for ::= ["async"] "for" target_list "in" or_test
[comp_iter]
comp_quantified_for ::= ["async"] "for" quantifier target_list "in"
or_test [comp_quantified_for]
quantifier ::= any | every
comp_iter ::= comp_for | comp_if
comp_if ::= "if" or_test [comp_iter]
Can always be extended later, but only useful expressions should be allowed.
Examples
expand
Example 1
result = value < 0 for any value in range(stop)
Would be roughly equivalent to:
def func(stop):
for value in range(stop):
if value < 0:
return True
return False
result = func(stop)
Example 2
result = value >= 0 for every value in range(stop)
Would be roughly equivalent to:
def func(stop):
for value in range(stop):
if value < 0:
return False
return True
result = func(stop)
Example 3
result = (
value < 0
for any stop in range(stops)
for any value in range(stop)
)
Would be roughly equivalent to:
def func(stops):
for stop in range(stops):
for value in range(stop):
if value < 0:
return True
return False
result = func(stops)
Example 4
result = [
value < 0
for stop in range(stops)
for any value in range(stop)
]
Would be roughly equivalent to:
def func(stop):
for value in range(stop):
if value < 0:
return True
return False
result = []
for stop in range(stops)
result.append(func(stop))
Other discussions about comprehensions
expand
Itertools.takewhile but in a list comprehension
result = [value for value in range(stop) while value < 10]
Would be roughly equivalent to:
result = []
for value in range(stop):
if value >= 10:
break
result.append(value)
Feedback:
- Cognitive overload
- Uncommon
- Intuitive
- Breaks duality with for loops
Why no tuple comprehension?
(... for ... in ...)
is already used for generator expressions- Uncommon
- Tuples are immutable
Exception handling syntax in comprehensions
result = [1 / value for value in range(stop) except ZeroDivisionError]
Would be roughly equivalent to:
result = []
for value in range(stop):
try:
result.append(1 / value)
except ZeroDivisionError:
pass
Feedback:
- Less expressive than PEP 463
- Uncommon
Since comprehension is now inline-ed, shall python allow “yield inside comprehension” again?
def func(stop):
while True:
L = [(yield) for _ in range(stop)]
print(L)
Would be roughly equivalent to:
def func(stop):
while True:
L = []
for _ in range(stop):
L.append((yield))
print(L)
Feedback:
- Breaks expectation that comprehensions work in one go
- If
(yield)
is forbidden in generator expression it should be forbidden in other comprehensions too - Uncommon
Allow comprehension syntax in loop header of for loop
for value in range(stop) if value % 3:
print(value)
Would be roughly equivalent to:
for value in range(stop):
if value % 3:
print(value)
Feedback:
- Not readable
- You can use a generator expression
- There should be one-- and preferably only one --obvious way to do it
Proposal: for in match
within list comprehension
result = [name for city in cities match City(country='BR', name=name)]
Would be roughly equivalent to:
result = []
for city in cities:
match city:
case City(country='BR', name=name):
results.append(name)
Feedback:
- Hard to read
- You can use duck typing
- Doesn’t offer many optimisation opportunities
Using unpacking to generalize comprehensions with multiple elements
result = [*(value, value + 1) for value in range(1, stop, 3)]
Would be roughly equivalent to:
result = []
for value in range(1, stop, 3):
result.extend((value, value + 1))
Feedback:
- You can use nested for loops
- Not intuitive
Yield comprehension
def func1(stop):
yield value ** 2 for value in range(stop)
Would be roughly equivalent to:
def func1(stop):
for value in range(stop):
value ** 2
Feedback:
- Acts differently with parentheses
- You can use
yield from
- Where does
send(...)
go?
where
clauses in List comprehension
result = [string for string in strings if number % 2 where number = int(string)]
Would be roughly equivalent to:
result = []
for string in strings:
number = int(string)
if number % 2:
result.append(string)
Feedback:
- You can use
:=
- There should be one-- and preferably only one --obvious way to do it