Skip to content

itertools: clearer all_equal recipe #92628

Closed
@pochmann

Description

@pochmann

The Itertools Recipes have this recipe:

def all_equal(iterable):
    "Returns True if all the elements are equal to each other"
    g = groupby(iterable)
    return next(g, True) and not next(g, False)

The next(g, True) tries to read a first group and if there isn't one, then... uh... actually next(g, True) is always true, so that's squeezed into the return expression just for the side effect of skipping the first group. Psych!

I think for official recipes, it would be better to write clearer code. Two ideas for replacing it:

Proposal 1:

def all_equal(iterable):
    "Returns True if all the elements are equal to each other"
    g = groupby(iterable)
    next(g, None)
    return not next(g, False)

This makes the first next call a statement that makes it obvious we're not using its result and just do it for skipping the first group. I also replaced True with None to further indicate this.

Proposal 2:

def all_equal(iterable):
    "Returns True if all the elements are equal to each other"
    g = groupby(iterable)
    return not next(g, False) or not next(g, False)

This reads as "There is no first group or there is no second group". This actually uses the result of the first next(g, False): If it shows that there is no first group, then the function returns True immediately. It doesn't even try to look for a second group (unlike the original recipe and my first proposal, which both do). Also, it's simpler and nice to have the exact same expression not next(g, False) twice.

Really my point is clarity, but for completeness let me also show benchmarks. My first proposal was faster than the original in every case, and the most significant difference is my second proposal being faster for empty iterables.

Python 3.10.4 on Debian (on a Google Compute Engine instance):

iterable = ()
 235 ns   235 ns   235 ns  proposal2 True
 265 ns   265 ns   265 ns  proposal1 True
 265 ns   265 ns   265 ns  original True

iterable = (1,)
 308 ns   308 ns   308 ns  proposal1 True
 314 ns   314 ns   314 ns  original True
 316 ns   316 ns   316 ns  proposal2 True

iterable = (1, 2)
 361 ns   361 ns   361 ns  proposal1 False
 366 ns   366 ns   366 ns  original False
 384 ns   384 ns   384 ns  proposal2 False

Python 3.10.4 on Windows (my laptop):

iterable = ()
 926 ns   928 ns   929 ns  proposal2 True
1063 ns  1065 ns  1065 ns  proposal1 True
1067 ns  1068 ns  1069 ns  original True

iterable = (1,)
1225 ns  1228 ns  1230 ns  proposal1 True
1251 ns  1253 ns  1253 ns  original True
1257 ns  1258 ns  1261 ns  proposal2 True

iterable = (1, 2)
1397 ns  1409 ns  1410 ns  proposal1 False
1417 ns  1417 ns  1418 ns  proposal2 False
1427 ns  1429 ns  1432 ns  original False

The benchmark code:

from timeit import timeit
from random import shuffle
from bisect import insort
from itertools import groupby

def original(iterable):
    "Returns True if all the elements are equal to each other"
    g = groupby(iterable)
    return next(g, True) and not next(g, False)

def proposal1(iterable):
    "Returns True if all the elements are equal to each other"
    g = groupby(iterable)
    next(g, None)
    return not next(g, False)

def proposal2(iterable):
    "Returns True if all the elements are equal to each other"
    g = groupby(iterable)
    return not next(g, False) or not next(g, False)

funcs = [original, proposal1, proposal2]

for iterable in (), (1,), (1, 2):
    print(f'{iterable = }')
    times = {func: [] for func in funcs}
    for _ in range(1000):
        shuffle(funcs)
        for func in funcs:
            number = 1000
            t = timeit(lambda: func(iterable), number=number) / number
            insort(times[func], t)
    for func in sorted(funcs, key=times.get):
        print(*('%4d ns ' % round(t * 1e9) for t in times[func][:3]), func.__name__, func(iterable))
    print()

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsDocumentation in the Doc dir

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions