9

Consider the hypothetical function repeatcall, that takes as arguments a no-args callable func and a positive integer n, and returns a list whose members are obtained by executing func() n times. It supports an infinite stream of silly hijinks like:

>>> repeatcall(lambda: id(dict()), 5)
[45789920, 45788064, 45807216, 45634816, 45798640]

>>> urandom = lambda: struct.unpack('Q', open('/dev/urandom').read(8))[0]
>>> repeatcall(urandom, 3)
[3199039843823449742, 14990726001693341311L, 11583468019313082272L]

>>> class Counter(itertools.count): __call__ = itertools.count.next
>>> repeatcall(Counter(100, -2), 4)
[100, 98, 96, 94]

I could swear that I've seen a function like repeatcall somewhere in the Python 2.x standard libraries, but I can't find it. If I didn't dream this, where in the standard library can I find it?

PS: I know it's trivial to roll one's own, but I hate to reinvent wheels, especially those are already in the standard library. I am not asking how to roll my own.

Edit: made it even more explicit that I am not asking how to code repeatcall.

kjo
  • 33,683
  • 52
  • 148
  • 265

5 Answers5

16

You've seen this in the standard library docs, not the standard library itself.

It's repeatfunc from the itertools recipes:

def repeatfunc(func, times=None, *args):
    """Repeat calls to func with specified arguments.

    Example:  repeatfunc(random.random)
    """
    if times is None:
        return starmap(func, repeat(args))
    return starmap(func, repeat(args, times))

It allows arguments and should (theoretically) perform better than a list comprehension because func only has to be looked up once. repeat is also faster than range for when you're not actually using the counter.

agf
  • 171,228
  • 44
  • 289
  • 238
  • 1
    Technically yours isn't the exact answer either, unless you are saying that it does not exist in the stdlib. I don't think a recipe making use of `itertools.starmap` is any different than rolling your own. Though I suppose by showing this, you are in a round about way saying you have to roll your own. – jdi Apr 06 '12 at 20:52
  • 3
    @jdi Yes, I'm staying it doesn't exist in the standard library. He remembers seeing it because it's in the docs. That is the answer to his question. – agf Apr 06 '12 at 20:56
  • 1
    Then I suppose 90% of what you wrote isn't necessary if he isn't interested in seeing any implementations of the code. So really, that 90% is just like the others. – jdi Apr 06 '12 at 20:58
  • 3
    @jdi That 90% is included simply so people don't have to click through to the `itertools` docs to see what I'm talking about. SO answers are supposed to stand on their own as much as possible, not depend on external resources. – agf Apr 06 '12 at 21:01
  • Ok then lets just say 30% isn't needed, since the explanation at the end was just frosting :-) – jdi Apr 06 '12 at 21:04
  • @jdi Yes, that's really a comment on my own answer rather than part of the answer itself :) – agf Apr 06 '12 at 21:05
  • I guess the return should be wrapped in `list()` to actually return a list with the results right ? – Mr_and_Mrs_D Jun 09 '17 at 19:09
  • @Mr_and_Mrs_D If you actually want a list with all of the results, sure -- one of the big advantages of itertools is you can get just one thing at a time if you don't need them all at once. – agf Jun 09 '17 at 22:56
  • @Mr_and_Mrs_D Yes, if you're calling the function a **finite** number of times, and you're OK with waiting until the _last_ instance of the function to finish before seeing the _first_. But if you want to iterate back over the function outputs as they occur (for example, if using [epoll](https://docs.python.org/3/library/select.html): `for fn, ev in ( event for events in repeatfunc(E.poll) for event in events ):`), then the liberty to consume the generator only as-needed can be a godsend. – JamesTheAwesomeDude May 21 '21 at 15:40
4

There's a reason this doesn't exist: the idiomatic way to code a function that doesn't take arguments on each invocation, and returns something new is to code it as a generator.

You would then use a list comprehension or generator expression to call it as many times as you like: [next(gen) for i in xrange(5)]. Better yet, gen can itself be the result of a generator expression like (id(dict()) for i in (itertools.repeat(None))).

Thus, python has no library support for this because it supports it syntactically.

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • You're right, that is more "Pythonic" than the solution in the `itertools` recipes. But, like all the others, this isn't an answer to the question. He was asking where he'd seen an implementation, not how to do it. This would have been a good comment. – agf Apr 17 '12 at 20:53
  • 2
    @agf Not really. This requires at leas two paragraphs, and it does explain why there is no solution of the form that OP asks for. – Marcin Apr 17 '12 at 23:15
  • His question isn't "is this in the standard library", it's "I've seen this before; I think it was in the standard library but I can't find it. Where is it?" A comment fits 500 characters; your post has room to spare, and no formatted blocks that you couldn't display in a comment. – agf Apr 17 '12 at 23:22
  • 1
    @agf If you feel that strongly, flag it. Why are you so angry at everyone else who has posted an answer? – Marcin Apr 18 '12 at 08:08
  • There's no "anger" in it. When an answer doesn't address the question, it's noise. When I've made a mistake and posted something not on-point, I delete it, and I appreciate it when others point out my mistakes. Unless an answer contains _no information_ I don't flag it; the decision to remove it or not is the poster's, see [my meta question on the subject](http://meta.stackexchange.com/questions/108143/moderator-deletion-of-equivalent-answers). And I don't downvote because the intention was to provide useful information, and the information isn't wrong. – agf Apr 18 '12 at 08:13
  • Clearly, you have some anger though, since you just downvoted my (correct) answer. I just saw [this answer and comment](http://stackoverflow.com/questions/10172286/how-come-i-cant-get-the-exactly-result-to-pip-install-by-manually-python-set/10172319#comment13058111_10172319) -- I'm surprised you'd downvote an answer that was correct after a disagreement, since that comment makes it pretty clear you think that's wrong. – agf Apr 18 '12 at 08:14
  • @agf I downvoted your answer because you have complained about every single other answer on flimsy grounds. That's not cool. – Marcin Apr 18 '12 at 09:27
  • What about for a for loop, though? I'm looking for the same answer, and while generator or list comprehensions are good, `for n in (func() for i in range(10))` seems a bit awkward – Lucretiel Aug 24 '12 at 22:00
  • @Lucretiel I'm not sure what you mean. There is no reason for you to have a for loop and generator expression in your example. Just write: `for i in range(10): func()`, or if you want to have an iterable to pass elsewhere, use `(func() for i in range(10))`. – Marcin Aug 25 '12 at 11:59
3

Do you mean something like this?:

>> from random import random
>> print [random() for x in range(5)]
[0.015015074309405185,
 0.7877023608913573,
 0.2940706206824023,
 0.7140457069245207,
 0.07868376815555878]

Seems succinct enough no?

Rian Rizvi
  • 9,989
  • 2
  • 37
  • 33
  • 3
    "I know it's trivial to roll one's own, but I hate to reinvent wheels" -- this doesn't answer the question. – agf Apr 06 '12 at 20:47
2

You can use the apply built-in function for this purpose

>>> def repeatcall(func,n):
    [apply(func) for i in range(0,n)]

>>> repeatcall(lambda: id(dict()), 5)
[56422096, 56422240, 56447024, 56447168, 56447312]

>>> import itertools
>>> class Counter(itertools.count): __call__ = itertools.count.next

>>> repeatcall(Counter(100, -2), 4)
[100, 98, 96, 94]
>>> 

Note** From the manual The use of apply() is equivalent to function(*args, **keywords).

So repeatcall can also be written as

>>> def repeatcall(func,n):
    [func() for i in range(0,n)]
Abhijit
  • 62,056
  • 18
  • 131
  • 204
  • 1
    `apply` has been deprecated since python2.3. You can just call the function with its *args and **kwargs – jdi Apr 06 '12 at 20:43
  • the function can be called directly, even if it's name is passed in as an argument: def repeatcall(func,n): return [func() for i in range(0,n)] – Rian Rizvi Apr 06 '12 at 20:44
  • yes apply is completely unnecessary here and everywhere else. – Jochen Ritzel Apr 06 '12 at 20:45
  • 1
    This doesn't answer the question -- "I know it's trivial to roll one's own, but I hate to reinvent wheels". He didn't want an implementation. – agf Apr 06 '12 at 20:49
  • @agf: The postscript was added 15 mins after the original question :-) – Abhijit Apr 06 '12 at 20:52
  • 1
    No, it wasn't. Look at the [edit history](http://stackoverflow.com/posts/10048916/revisions); it's in the original version, just not bold. – agf Apr 06 '12 at 20:55
0

While reading comments here I realized that iter(foo, expr) will create an infinite iterator of the result of foo as long as it never equals expr.

for the example in the question, iter(lambda: id(dict()), None) will create an infinite iterator of ids of dicts.

While not a direct answer to this question, I found it useful since in my usecase I wanted it to be part of a finite zip expression anyways and just needed the iterator to keep producing elements.

hopefully this might help others that stumble upon this question

NivPgir
  • 63
  • 4