Allow accessing / retrieving the current item of `itertools.count`

itertools.count is one of the few atomic constructs available (at least to the extent that the GIL’s protection can be considered atomic), which makes it useful as a shared sequence or counter, however this is hampered by the inability for an observer to access the value without incrementing, at least without tricks: it’s possible to retrieve the current value by either parsing the repr or using the pickling protocol:

>>> import itertools
>>> c = itertools.count()
>>> next(c); next(c); next(c)
0
1
2
>>> c
count(3)
>>> c.__reduce__()
(<class 'itertools.count'>, (3,))

A property to access the current value would make for much simpler e.g. task or work counting, without needing to adjust for retrieval updates or the like.

A straight addition would have to be protected by a mutex as it’s not atomic, a += ? compiling to (more or less)

LOAD a
LOAD ?
INPLACE_ADD
STORE a

with 3 suspention points between the loading and storage of a.

Possible alternative

Add basic atomic constructs (CAS, possibly load and store, maybe even operators like inc/dec/add/sub) to cpython, however it’s not entirely clear to me how that would work without language support, since the atomic constructs would need access to the storage location itself in order to atomically load/store while holding the GIL (or a finer lock if the GIL ends up getting removed).

Why not mutex

Mutexes are a lot more expensive in all dimensions: they’re pretty costly, they require more code, and they have a higher semantic overhead as the lock and what it protects are not technically directly linked. Though possibly an other alternative would be to make locks into (optional) containers à la Rust, and maybe add convenience / utility pseudo-atomics.

3 Likes

Bump. I came across such need as well. Very useful when needing to track size of consumed iterable. I also used __reduce__ hack.

@dg-pb I’d like to see your case where you still need the iterator afterwards and can’t just use next(c). Did you post that somewhere?

If you don’t like writing __reduce__, maybe try copy?

from itertools import count
from copy import copy

c = count()
print(next(c), next(c), next(copy(c)), next(c))

Output (Attempt This Online!):

0 1 2 2

No, not yet.

def iter_counter(iterable):
    counter = itl.count()
    def get_count():
        return counter.__reduce__()[1][0]
    return map(opr.itemgetter(0), zip(iterable, counter)), get_count

Using copy to retrieve it is 1 µs, __reduce__ has fairly good performance, although not perfect.

An alternative for that case:

def iter_counter(iterable):
    counter = itl.count()
    offset = itl.count()
    def get_count():
        return next(counter) - next(offset)
    return itl.compress(iterable, zip(counter)), get_count

Attempt This Online!

Fun version, perhaps faster:

def iter_counter(iterable):
    counter = itl.count()
    return (
        itl.compress(iterable, zip(counter)),
        map(opr.sub, counter, itl.count()).__next__
    )

Without my zip trick:

def iter_counter(iterable):
    counter = itl.count(1)
    return (
        itl.compress(iterable, counter),
        map(opr.sub, counter, itl.count(1)).__next__
    )
1 Like

Nice one. Compress was unexplored properly by me.

Much better without a zip trick. :slight_smile:

Nice recipe, personally very happy with it because it ended up faster than my version. However, logic is a bit involved, a bit too involved for what it does I would say.

Can I suggest that discussions about squeezing microsecond-level performance improvements out of Python code probably belong in the help topic? That sort of improvement is either something that can simply be submitted as a PR (if there are no backward compatibility issues) or something that will never happen (because that sort of benefit is way below the level to justify a language change). Either way, it’s not a feasible language change.

The ideas topic is specifically for language changes and this sort of micro-optimisation isn’t really a language design level discussion.

3 Likes

Isn’t it relevant to the OP though?

1 Like

Noted.

However, this was about not having to hack python to get current value from a counter.

There is nothing “micro” about 1 µs per iteration.

I love optimization. All kinds of it: macro, micro, downhill, cuckoos nest. One of my favourite parts. So you might see an increase in my comments about it when I am on “optimization break”. If it bothers many people I can try to refrain myself form commenting about it.

And I think optimization for python is very important. At least for the the level of applicability, functionality and modularity that I think software written in python could achieve.

Having that said, I appreciate that certain changes (such as JIT) might have a big impact on the status quo and some micro optimizations that I do might not be worth an effort.

Not sure if serious… :sweat_smile:

4 Likes

Iterable size is 100K… :scream:

So a 1 µs speedup per iteration saves you… 0.1 seconds?

I was mostly reacting to the fact that there is definitely something “micro” about microseconds. :smiley:

1 Like

This is why I multiplied it. :slight_smile:

What about… I am processing CD quality audio with buffer size 64…

No, but seriously 1µs per iteration just to track the count of number of items processed is a lot.

For what it’s worth, there’s more_itertools.countable, which wraps any iterable thing and exposes a count.

1 Like

Yeah, I would also have used more-itertools: it = peekable(count()), and then the count is just it.peek().

Anyway, I like this suggestion. It doesn’t hurt to expose the count in my opinion.

2 Likes

Is there a way to dynamically replace __next__ method?

I.e. iterator.__next__ = other_iterator.__next__

%timeit bti.consume(more_itertools.countable(a))      # 12.6 ms
# What constitutes this time?
1.4 ms - consume
2.2 ms - counting integers
7 ms - __next__ method
2 ms - actual iteration

e.g. @Stefan2’s solution is almost 4x faster:

%timeit bti.consume(iter_counter(a)[0]) #  3.4
1.4ms - consume
2 ms - actual iteration

I tried converting it to a class, but it results in 10.4 ms - only integer counting is made more efficient.

1 Like

Like this?

from itertools import count, compress
from operator import sub

def countable(iterable):
    counter = count(1)
    it = compress(iterable, counter)
    cnt = map(sub, counter, count(1))
    class IterCounter:
        __iter__ = it.__iter__
        __next__ = it.__next__
        @property
        def items_seen(self):
            for c in cnt:
                return c
    return IterCounter()

Attempt This Online!

1 Like