Search code examples
pythongenerator

What is the purpose of the "send" function on Python generators?


I understand yield. But what does a generator's send function do? The documentation says:

generator.send(value)

Resumes the execution and “sends” a value into the generator function. The value argument becomes the result of the current yield expression. The send() method returns the next value yielded by the generator, or raises StopIteration if the generator exits without yielding another value.

What does that mean? I thought value was the input to the generator function? The phrase "The send() method returns the next value yielded by the generator" seems to be also the exact purpose of yield, which also returns the next value yielded by the generator.

Is there an example of a generator utilizing send that accomplishes something yield cannot?


Solution

  • It's used to send values into a generator that just yielded. Here is an artificial (non-useful) explanatory example:

    >>> def double_inputs():
    ...     while True:
    ...         x = yield
    ...         yield x * 2
    ...
    >>> gen = double_inputs()
    >>> next(gen)       # run up to the first yield
    >>> gen.send(10)    # goes into 'x' variable
    20
    >>> next(gen)       # run up to the next yield
    >>> gen.send(6)     # goes into 'x' again
    12
    >>> next(gen)       # run up to the next yield
    >>> gen.send(94.3)  # goes into 'x' again
    188.5999999999999
    

    You can't do this just with yield.

    As to why it's useful, one of the best use cases I've seen is Twisted's @defer.inlineCallbacks. Essentially it allows you to write a function like this:

    @defer.inlineCallbacks
    def doStuff():
        result = yield takesTwoSeconds()
        nextResult = yield takesTenSeconds(result * 10)
        defer.returnValue(nextResult / 10)
    

    What happens is that takesTwoSeconds() returns a Deferred, which is a value promising a value will be computed later. Twisted can run the computation in another thread. When the computation is done, it passes it into the deferred, and the value then gets sent back to the doStuff() function. Thus the doStuff() can end up looking more or less like a normal procedural function, except it can be doing all sorts of computations & callbacks etc. The alternative before this functionality would be to do something like:

    def doStuff():
        returnDeferred = defer.Deferred()
        def gotNextResult(nextResult):
            returnDeferred.callback(nextResult / 10)
        def gotResult(result):
            takesTenSeconds(result * 10).addCallback(gotNextResult)
        takesTwoSeconds().addCallback(gotResult)
        return returnDeferred
    

    It's a lot more convoluted and unwieldy.