jose
jose

Reputation: 399

generate consecutive numbers in python while input doesnt change

i need to get consecutive numbers while an input number doesnt change.

so i get give(5)->1, give(5)->2, and so on, but then: give(6)->1 again, starting the count.

So far I solved it with an iterator function count() and a function give(num) like this:

def count(start=1):
    n=start
    while True:
        yield n
        n +=1
def give(num):
    global last
    global a
    if num==last:
        ret=a.next()
    else:
        a=count()
        ret=a.next()
    last=num
    return ret

It works, but its ugly: I have two globals and have to set them before I call give(num). I'd like to be able to call give(num) without setting previously the 'a=count()' and 'last=999' variables. I'm positive there's better way to do this...

edit: ty all for incredibly fast and varied responses, i've got a lot to study here..

Upvotes: 1

Views: 1373

Answers (6)

abarnert
abarnert

Reputation: 366073

In my first answer, I suggested that one solution was to transform the closure into an object. But I skipped a step—you're using global variables, not a closure, and that's part of what you didn't like about it.

Here's a simple way to transform any global state into encapsulated state:

def make_give():
    last, a = None, None
    def give(num):
        nonlocal last
        nonlocal a
        if num != last:
            a = count()
        last=num
        return a.next()
    return give
give = make_give()

Or, adapting my final version of Giver:

def make_giver():
    counters = defaultdict(count)
    def give(self, num):
        return next(counters[num])
    return give

If you're curious how this works:

>>> give.__closure__
(<cell at 0x10f0e2398: NoneType object at 0x10b40fc50>, <cell at 0x10f0e23d0: NoneType object at 0x10b40fc50>)
>>> give.__code__.co_freevars
('a', 'last')

Those cell objects are essentially references into the stack frame of the make_give call that created the give function.


This doesn't always work quite as well in Python 2.x as in 3.x. While closure cells work the same way, if you assign to a variable inside the function body and there's no global or nonlocal statement, it automatically becomes local, and Python 2 had no nonlocal statement. So, the second version works fine, but for the first version, you'd have to do something like state = {'a': None, 'last': None} and then write state['a'] = count instead of a = count.


This trick—creating a closure just to hide local variables—is very common in a few other languages, like JavaScript. In Python (partly because of the long history without the nonlocal statement, and partly because Python has alternatives that other languages don't), it's less common. It's usually more idiomatic to stash the state in a mutable default parameter value, or an attribute on the function—or, if there's a reasonable class to make the function a method of, as an attribute on the class instances. There are plenty of cases where a closure is pythonic, this just isn't usually one of them.

Upvotes: 1

abarnert
abarnert

Reputation: 366073

The obvious thing to do is to make give into an object rather than a function.* Any object can be made callable by defining a __call__ method.

While we're at it, your code can be simplified quite a bit, so let's do that.

class Giver(object):
    def __init__(self):
        self.last, self.a = object(), count()
    def __call__(self, num):
        if num != self.last:
            self.a = count(1)
        self.last = num
        return self.a.next()

give = Giver()

So:

>>> give(5)
1
>>> give(5)
2
>>> give(6)
1
>>> give(5)
1

This also lets you create multiple separate givers, each with its own, separate current state, if you have any need to do that.

If you want to expand it with more state, the state just goes into the instance variables. For example, you can replace last and a with a dictionary mapping previously-seen values to counters:

class Giver(object):
    def __init__(self):
        self.counters = defaultdict(count)
    def __call__(self, num):
        return next(self.counters[num])

And now:

>>> give(5)
1
>>> give(5)
2
>>> give(6)
1
>>> give(5)
3

* I sort of skipped a step here. You can always remove globals by putting the variables and everything that uses them (which may just be one function) inside a function or other scope, so they end up as free variables in the function's closure. But in your case, I think this would just make your code look "uglier" (in the same sense you thought it was ugly). But remember that objects and closures are effectively equivalent in what they can do, but different in what they look like—so when one looks horribly ugly, try the other.

Upvotes: 3

roippi
roippi

Reputation: 25974

Adding a second answer because this is rather radically different from my first.

What you are basically trying to accomplish is a coroutine - a generator that preserves state that at arbitrary time, values can be sent into. PEP 342 gives us a way to do that with the "yield expression". I'll jump right into how it looks:

from collections import defaultdict
from itertools import count
from functools import partial

def gen(x):
    _counters = defaultdict(partial(count,1))
    while True:
        out = next(_counters[x])
        sent = yield out
        if sent:
            x = sent

If the _counters line is confusing, see my other answer.

With a coroutine, you can send data into the generator. So you can do something like the following:

g = gen(5)

next(g)
Out[159]: 1

next(g)
Out[160]: 2

g.send(6)
Out[161]: 1

next(g)
Out[162]: 2

next(g)
Out[163]: 3

next(g)
Out[164]: 4

g.send(5)
Out[165]: 3

Notice how the generator preserves state and can switch between counters at will.

Upvotes: 1

roippi
roippi

Reputation: 25974

You can basically get what you want via combination of defaultdict and itertools.count:

from collections import defaultdict
from itertools import count

_counters = defaultdict(count)

next(_counters[5])
Out[116]: 0

next(_counters[5])
Out[117]: 1

next(_counters[5])
Out[118]: 2

next(_counters[5])
Out[119]: 3

next(_counters[6])
Out[120]: 0

next(_counters[6])
Out[121]: 1

next(_counters[6])
Out[122]: 2

If you need the counter to start at one, you can get that via functools.partial:

from functools import partial

_counters = defaultdict(partial(count,1))

next(_counters[5])
Out[125]: 1

next(_counters[5])
Out[126]: 2

next(_counters[5])
Out[127]: 3

next(_counters[6])
Out[128]: 1

Upvotes: 2

Thayne
Thayne

Reputation: 7032

You can achieve this with something like this:

def count(start=1):
    n = start
    while True:
        yield n
        n += 1

def give(num):
    if num not in give.memo:
        give.memo[num] = count()
    return next(give.memo[num])

give.memo = {}

Which produces:

>>> give(5)
1
>>> give(5)
2
>>> give(5)
3
>>> give(6)
1
>>> give(5)
4
>>> 

The two key points are using a dict to keep track of multiple iterators simultaneously, and setting a variable on the function itself. You can do this because functions are themselves objects in python. This is the equivalent of a static local variable in C.

Upvotes: 2

user2357112
user2357112

Reputation: 281853

Just keep track of the last returned value for each input. You can do this with an ordinary dict:

_counter = {}
def give(n):
    _counter[n] = _counter.get(n, 0) + 1
    return _counter[n]

The standard library has a Counter class that makes things a bit easier:

import collections
_counter = collections.Counter()
def give(n):
    _counter[n] += 1
    return _counter[n]

collections.defaultdict(int) works too.

Upvotes: 2

Related Questions